about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/docx/oxml
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/docx/oxml
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/docx/oxml')
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/__init__.py243
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/coreprops.py298
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/document.py88
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/drawing.py11
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/exceptions.py10
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/ns.py109
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/numbering.py109
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/parser.py62
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/section.py537
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/settings.py138
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/shape.py302
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/shared.py53
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/simpletypes.py381
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/styles.py320
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/table.py977
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/text/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/text/font.py356
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/text/hyperlink.py45
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/text/pagebreak.py284
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/text/paragraph.py106
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/text/parfmt.py387
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/text/run.py276
-rw-r--r--.venv/lib/python3.12/site-packages/docx/oxml/xmlchemy.py709
23 files changed, 5801 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/__init__.py b/.venv/lib/python3.12/site-packages/docx/oxml/__init__.py
new file mode 100644
index 00000000..bf32932f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/__init__.py
@@ -0,0 +1,243 @@
+"""Initializes oxml sub-package.
+
+This including registering custom element classes corresponding to Open XML elements.
+"""
+
+from __future__ import annotations
+
+from docx.oxml.drawing import CT_Drawing
+from docx.oxml.parser import OxmlElement, parse_xml, register_element_cls
+from docx.oxml.shape import (
+    CT_Anchor,
+    CT_Blip,
+    CT_BlipFillProperties,
+    CT_GraphicalObject,
+    CT_GraphicalObjectData,
+    CT_Inline,
+    CT_NonVisualDrawingProps,
+    CT_Picture,
+    CT_PictureNonVisual,
+    CT_Point2D,
+    CT_PositiveSize2D,
+    CT_ShapeProperties,
+    CT_Transform2D,
+)
+from docx.oxml.shared import CT_DecimalNumber, CT_OnOff, CT_String
+from docx.oxml.text.hyperlink import CT_Hyperlink
+from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak
+from docx.oxml.text.run import (
+    CT_R,
+    CT_Br,
+    CT_Cr,
+    CT_NoBreakHyphen,
+    CT_PTab,
+    CT_Text,
+)
+
+# -- `OxmlElement` and `parse_xml()` are not used in this module but several downstream
+# -- "extension" packages expect to find them here and there's no compelling reason
+# -- not to republish them here so those keep working.
+__all__ = ["OxmlElement", "parse_xml"]
+
+# ---------------------------------------------------------------------------
+# DrawingML-related elements
+
+register_element_cls("a:blip", CT_Blip)
+register_element_cls("a:ext", CT_PositiveSize2D)
+register_element_cls("a:graphic", CT_GraphicalObject)
+register_element_cls("a:graphicData", CT_GraphicalObjectData)
+register_element_cls("a:off", CT_Point2D)
+register_element_cls("a:xfrm", CT_Transform2D)
+register_element_cls("pic:blipFill", CT_BlipFillProperties)
+register_element_cls("pic:cNvPr", CT_NonVisualDrawingProps)
+register_element_cls("pic:nvPicPr", CT_PictureNonVisual)
+register_element_cls("pic:pic", CT_Picture)
+register_element_cls("pic:spPr", CT_ShapeProperties)
+register_element_cls("w:drawing", CT_Drawing)
+register_element_cls("wp:anchor", CT_Anchor)
+register_element_cls("wp:docPr", CT_NonVisualDrawingProps)
+register_element_cls("wp:extent", CT_PositiveSize2D)
+register_element_cls("wp:inline", CT_Inline)
+
+# ---------------------------------------------------------------------------
+# hyperlink-related elements
+
+register_element_cls("w:hyperlink", CT_Hyperlink)
+
+# ---------------------------------------------------------------------------
+# text-related elements
+
+register_element_cls("w:br", CT_Br)
+register_element_cls("w:cr", CT_Cr)
+register_element_cls("w:lastRenderedPageBreak", CT_LastRenderedPageBreak)
+register_element_cls("w:noBreakHyphen", CT_NoBreakHyphen)
+register_element_cls("w:ptab", CT_PTab)
+register_element_cls("w:r", CT_R)
+register_element_cls("w:t", CT_Text)
+
+# ---------------------------------------------------------------------------
+# header/footer-related mappings
+
+register_element_cls("w:evenAndOddHeaders", CT_OnOff)
+register_element_cls("w:titlePg", CT_OnOff)
+
+# ---------------------------------------------------------------------------
+# other custom element class mappings
+
+from .coreprops import CT_CoreProperties  # noqa
+
+register_element_cls("cp:coreProperties", CT_CoreProperties)
+
+from .document import CT_Body, CT_Document  # noqa
+
+register_element_cls("w:body", CT_Body)
+register_element_cls("w:document", CT_Document)
+
+from .numbering import CT_Num, CT_Numbering, CT_NumLvl, CT_NumPr  # noqa
+
+register_element_cls("w:abstractNumId", CT_DecimalNumber)
+register_element_cls("w:ilvl", CT_DecimalNumber)
+register_element_cls("w:lvlOverride", CT_NumLvl)
+register_element_cls("w:num", CT_Num)
+register_element_cls("w:numId", CT_DecimalNumber)
+register_element_cls("w:numPr", CT_NumPr)
+register_element_cls("w:numbering", CT_Numbering)
+register_element_cls("w:startOverride", CT_DecimalNumber)
+
+from .section import (  # noqa
+    CT_HdrFtr,
+    CT_HdrFtrRef,
+    CT_PageMar,
+    CT_PageSz,
+    CT_SectPr,
+    CT_SectType,
+)
+
+register_element_cls("w:footerReference", CT_HdrFtrRef)
+register_element_cls("w:ftr", CT_HdrFtr)
+register_element_cls("w:hdr", CT_HdrFtr)
+register_element_cls("w:headerReference", CT_HdrFtrRef)
+register_element_cls("w:pgMar", CT_PageMar)
+register_element_cls("w:pgSz", CT_PageSz)
+register_element_cls("w:sectPr", CT_SectPr)
+register_element_cls("w:type", CT_SectType)
+
+from .settings import CT_Settings  # noqa
+
+register_element_cls("w:settings", CT_Settings)
+
+from .styles import CT_LatentStyles, CT_LsdException, CT_Style, CT_Styles  # noqa
+
+register_element_cls("w:basedOn", CT_String)
+register_element_cls("w:latentStyles", CT_LatentStyles)
+register_element_cls("w:locked", CT_OnOff)
+register_element_cls("w:lsdException", CT_LsdException)
+register_element_cls("w:name", CT_String)
+register_element_cls("w:next", CT_String)
+register_element_cls("w:qFormat", CT_OnOff)
+register_element_cls("w:semiHidden", CT_OnOff)
+register_element_cls("w:style", CT_Style)
+register_element_cls("w:styles", CT_Styles)
+register_element_cls("w:uiPriority", CT_DecimalNumber)
+register_element_cls("w:unhideWhenUsed", CT_OnOff)
+
+from .table import (  # noqa
+    CT_Height,
+    CT_Row,
+    CT_Tbl,
+    CT_TblGrid,
+    CT_TblGridCol,
+    CT_TblLayoutType,
+    CT_TblPr,
+    CT_TblPrEx,
+    CT_TblWidth,
+    CT_Tc,
+    CT_TcPr,
+    CT_TrPr,
+    CT_VMerge,
+    CT_VerticalJc,
+)
+
+register_element_cls("w:bidiVisual", CT_OnOff)
+register_element_cls("w:gridAfter", CT_DecimalNumber)
+register_element_cls("w:gridBefore", CT_DecimalNumber)
+register_element_cls("w:gridCol", CT_TblGridCol)
+register_element_cls("w:gridSpan", CT_DecimalNumber)
+register_element_cls("w:tbl", CT_Tbl)
+register_element_cls("w:tblGrid", CT_TblGrid)
+register_element_cls("w:tblLayout", CT_TblLayoutType)
+register_element_cls("w:tblPr", CT_TblPr)
+register_element_cls("w:tblPrEx", CT_TblPrEx)
+register_element_cls("w:tblStyle", CT_String)
+register_element_cls("w:tc", CT_Tc)
+register_element_cls("w:tcPr", CT_TcPr)
+register_element_cls("w:tcW", CT_TblWidth)
+register_element_cls("w:tr", CT_Row)
+register_element_cls("w:trHeight", CT_Height)
+register_element_cls("w:trPr", CT_TrPr)
+register_element_cls("w:vAlign", CT_VerticalJc)
+register_element_cls("w:vMerge", CT_VMerge)
+
+from .text.font import (  # noqa
+    CT_Color,
+    CT_Fonts,
+    CT_Highlight,
+    CT_HpsMeasure,
+    CT_RPr,
+    CT_Underline,
+    CT_VerticalAlignRun,
+)
+
+register_element_cls("w:b", CT_OnOff)
+register_element_cls("w:bCs", CT_OnOff)
+register_element_cls("w:caps", CT_OnOff)
+register_element_cls("w:color", CT_Color)
+register_element_cls("w:cs", CT_OnOff)
+register_element_cls("w:dstrike", CT_OnOff)
+register_element_cls("w:emboss", CT_OnOff)
+register_element_cls("w:highlight", CT_Highlight)
+register_element_cls("w:i", CT_OnOff)
+register_element_cls("w:iCs", CT_OnOff)
+register_element_cls("w:imprint", CT_OnOff)
+register_element_cls("w:noProof", CT_OnOff)
+register_element_cls("w:oMath", CT_OnOff)
+register_element_cls("w:outline", CT_OnOff)
+register_element_cls("w:rFonts", CT_Fonts)
+register_element_cls("w:rPr", CT_RPr)
+register_element_cls("w:rStyle", CT_String)
+register_element_cls("w:rtl", CT_OnOff)
+register_element_cls("w:shadow", CT_OnOff)
+register_element_cls("w:smallCaps", CT_OnOff)
+register_element_cls("w:snapToGrid", CT_OnOff)
+register_element_cls("w:specVanish", CT_OnOff)
+register_element_cls("w:strike", CT_OnOff)
+register_element_cls("w:sz", CT_HpsMeasure)
+register_element_cls("w:u", CT_Underline)
+register_element_cls("w:vanish", CT_OnOff)
+register_element_cls("w:vertAlign", CT_VerticalAlignRun)
+register_element_cls("w:webHidden", CT_OnOff)
+
+from .text.paragraph import CT_P  # noqa
+
+register_element_cls("w:p", CT_P)
+
+from .text.parfmt import (  # noqa
+    CT_Ind,
+    CT_Jc,
+    CT_PPr,
+    CT_Spacing,
+    CT_TabStop,
+    CT_TabStops,
+)
+
+register_element_cls("w:ind", CT_Ind)
+register_element_cls("w:jc", CT_Jc)
+register_element_cls("w:keepLines", CT_OnOff)
+register_element_cls("w:keepNext", CT_OnOff)
+register_element_cls("w:pageBreakBefore", CT_OnOff)
+register_element_cls("w:pPr", CT_PPr)
+register_element_cls("w:pStyle", CT_String)
+register_element_cls("w:spacing", CT_Spacing)
+register_element_cls("w:tab", CT_TabStop)
+register_element_cls("w:tabs", CT_TabStops)
+register_element_cls("w:widowControl", CT_OnOff)
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/coreprops.py b/.venv/lib/python3.12/site-packages/docx/oxml/coreprops.py
new file mode 100644
index 00000000..8ba9ff42
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/coreprops.py
@@ -0,0 +1,298 @@
+"""Custom element classes for core properties-related XML elements."""
+
+from __future__ import annotations
+
+import datetime as dt
+import re
+from typing import TYPE_CHECKING, Any, Callable
+
+from docx.oxml.ns import nsdecls, qn
+from docx.oxml.parser import parse_xml
+from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne
+
+if TYPE_CHECKING:
+    from lxml.etree import _Element as etree_Element  # pyright: ignore[reportPrivateUsage]
+
+
+class CT_CoreProperties(BaseOxmlElement):
+    """`<cp:coreProperties>` element, the root element of the Core Properties part.
+
+    Stored as `/docProps/core.xml`. Implements many of the Dublin Core document metadata
+    elements. String elements resolve to an empty string ("") if the element is not
+    present in the XML. String elements are limited in length to 255 unicode characters.
+    """
+
+    get_or_add_revision: Callable[[], etree_Element]
+
+    category = ZeroOrOne("cp:category", successors=())
+    contentStatus = ZeroOrOne("cp:contentStatus", successors=())
+    created = ZeroOrOne("dcterms:created", successors=())
+    creator = ZeroOrOne("dc:creator", successors=())
+    description = ZeroOrOne("dc:description", successors=())
+    identifier = ZeroOrOne("dc:identifier", successors=())
+    keywords = ZeroOrOne("cp:keywords", successors=())
+    language = ZeroOrOne("dc:language", successors=())
+    lastModifiedBy = ZeroOrOne("cp:lastModifiedBy", successors=())
+    lastPrinted = ZeroOrOne("cp:lastPrinted", successors=())
+    modified = ZeroOrOne("dcterms:modified", successors=())
+    revision: etree_Element | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "cp:revision", successors=()
+    )
+    subject = ZeroOrOne("dc:subject", successors=())
+    title = ZeroOrOne("dc:title", successors=())
+    version = ZeroOrOne("cp:version", successors=())
+
+    _coreProperties_tmpl = "<cp:coreProperties %s/>\n" % nsdecls("cp", "dc", "dcterms")
+
+    @classmethod
+    def new(cls):
+        """Return a new `<cp:coreProperties>` element."""
+        xml = cls._coreProperties_tmpl
+        coreProperties = parse_xml(xml)
+        return coreProperties
+
+    @property
+    def author_text(self):
+        """The text in the `dc:creator` child element."""
+        return self._text_of_element("creator")
+
+    @author_text.setter
+    def author_text(self, value: str):
+        self._set_element_text("creator", value)
+
+    @property
+    def category_text(self) -> str:
+        return self._text_of_element("category")
+
+    @category_text.setter
+    def category_text(self, value: str):
+        self._set_element_text("category", value)
+
+    @property
+    def comments_text(self) -> str:
+        return self._text_of_element("description")
+
+    @comments_text.setter
+    def comments_text(self, value: str):
+        self._set_element_text("description", value)
+
+    @property
+    def contentStatus_text(self):
+        return self._text_of_element("contentStatus")
+
+    @contentStatus_text.setter
+    def contentStatus_text(self, value: str):
+        self._set_element_text("contentStatus", value)
+
+    @property
+    def created_datetime(self):
+        return self._datetime_of_element("created")
+
+    @created_datetime.setter
+    def created_datetime(self, value: dt.datetime):
+        self._set_element_datetime("created", value)
+
+    @property
+    def identifier_text(self):
+        return self._text_of_element("identifier")
+
+    @identifier_text.setter
+    def identifier_text(self, value: str):
+        self._set_element_text("identifier", value)
+
+    @property
+    def keywords_text(self):
+        return self._text_of_element("keywords")
+
+    @keywords_text.setter
+    def keywords_text(self, value: str):
+        self._set_element_text("keywords", value)
+
+    @property
+    def language_text(self):
+        return self._text_of_element("language")
+
+    @language_text.setter
+    def language_text(self, value: str):
+        self._set_element_text("language", value)
+
+    @property
+    def lastModifiedBy_text(self):
+        return self._text_of_element("lastModifiedBy")
+
+    @lastModifiedBy_text.setter
+    def lastModifiedBy_text(self, value: str):
+        self._set_element_text("lastModifiedBy", value)
+
+    @property
+    def lastPrinted_datetime(self):
+        return self._datetime_of_element("lastPrinted")
+
+    @lastPrinted_datetime.setter
+    def lastPrinted_datetime(self, value: dt.datetime):
+        self._set_element_datetime("lastPrinted", value)
+
+    @property
+    def modified_datetime(self) -> dt.datetime | None:
+        return self._datetime_of_element("modified")
+
+    @modified_datetime.setter
+    def modified_datetime(self, value: dt.datetime):
+        self._set_element_datetime("modified", value)
+
+    @property
+    def revision_number(self):
+        """Integer value of revision property."""
+        revision = self.revision
+        if revision is None:
+            return 0
+        revision_str = str(revision.text)
+        try:
+            revision = int(revision_str)
+        except ValueError:
+            # non-integer revision strings also resolve to 0
+            revision = 0
+        # as do negative integers
+        if revision < 0:
+            revision = 0
+        return revision
+
+    @revision_number.setter
+    def revision_number(self, value: int):
+        """Set revision property to string value of integer `value`."""
+        if not isinstance(value, int) or value < 1:  # pyright: ignore[reportUnnecessaryIsInstance]
+            tmpl = "revision property requires positive int, got '%s'"
+            raise ValueError(tmpl % value)
+        revision = self.get_or_add_revision()
+        revision.text = str(value)
+
+    @property
+    def subject_text(self):
+        return self._text_of_element("subject")
+
+    @subject_text.setter
+    def subject_text(self, value: str):
+        self._set_element_text("subject", value)
+
+    @property
+    def title_text(self):
+        return self._text_of_element("title")
+
+    @title_text.setter
+    def title_text(self, value: str):
+        self._set_element_text("title", value)
+
+    @property
+    def version_text(self):
+        return self._text_of_element("version")
+
+    @version_text.setter
+    def version_text(self, value: str):
+        self._set_element_text("version", value)
+
+    def _datetime_of_element(self, property_name: str) -> dt.datetime | None:
+        element = getattr(self, property_name)
+        if element is None:
+            return None
+        datetime_str = element.text
+        try:
+            return self._parse_W3CDTF_to_datetime(datetime_str)
+        except ValueError:
+            # invalid datetime strings are ignored
+            return None
+
+    def _get_or_add(self, prop_name: str) -> BaseOxmlElement:
+        """Return element returned by "get_or_add_" method for `prop_name`."""
+        get_or_add_method_name = "get_or_add_%s" % prop_name
+        get_or_add_method = getattr(self, get_or_add_method_name)
+        element = get_or_add_method()
+        return element
+
+    @classmethod
+    def _offset_dt(cls, dt_: dt.datetime, offset_str: str) -> dt.datetime:
+        """A |datetime| instance offset from `dt_` by timezone offset in `offset_str`.
+
+        `offset_str` is like `"-07:00"`.
+        """
+        match = cls._offset_pattern.match(offset_str)
+        if match is None:
+            raise ValueError("'%s' is not a valid offset string" % offset_str)
+        sign, hours_str, minutes_str = match.groups()
+        sign_factor = -1 if sign == "+" else 1
+        hours = int(hours_str) * sign_factor
+        minutes = int(minutes_str) * sign_factor
+        td = dt.timedelta(hours=hours, minutes=minutes)
+        return dt_ + td
+
+    _offset_pattern = re.compile(r"([+-])(\d\d):(\d\d)")
+
+    @classmethod
+    def _parse_W3CDTF_to_datetime(cls, w3cdtf_str: str) -> dt.datetime:
+        # valid W3CDTF date cases:
+        # yyyy e.g. "2003"
+        # yyyy-mm e.g. "2003-12"
+        # yyyy-mm-dd e.g. "2003-12-31"
+        # UTC timezone e.g. "2003-12-31T10:14:55Z"
+        # numeric timezone e.g. "2003-12-31T10:14:55-08:00"
+        templates = (
+            "%Y-%m-%dT%H:%M:%S",
+            "%Y-%m-%d",
+            "%Y-%m",
+            "%Y",
+        )
+        # strptime isn't smart enough to parse literal timezone offsets like
+        # "-07:30", so we have to do it ourselves
+        parseable_part = w3cdtf_str[:19]
+        offset_str = w3cdtf_str[19:]
+        dt_ = None
+        for tmpl in templates:
+            try:
+                dt_ = dt.datetime.strptime(parseable_part, tmpl)
+            except ValueError:
+                continue
+        if dt_ is None:
+            tmpl = "could not parse W3CDTF datetime string '%s'"
+            raise ValueError(tmpl % w3cdtf_str)
+        if len(offset_str) == 6:
+            dt_ = cls._offset_dt(dt_, offset_str)
+        return dt_.replace(tzinfo=dt.timezone.utc)
+
+    def _set_element_datetime(self, prop_name: str, value: dt.datetime):
+        """Set date/time value of child element having `prop_name` to `value`."""
+        if not isinstance(value, dt.datetime):  # pyright: ignore[reportUnnecessaryIsInstance]
+            tmpl = "property requires <type 'datetime.datetime'> object, got %s"
+            raise ValueError(tmpl % type(value))
+        element = self._get_or_add(prop_name)
+        dt_str = value.strftime("%Y-%m-%dT%H:%M:%SZ")
+        element.text = dt_str
+        if prop_name in ("created", "modified"):
+            # These two require an explicit "xsi:type="dcterms:W3CDTF""
+            # attribute. The first and last line are a hack required to add
+            # the xsi namespace to the root element rather than each child
+            # element in which it is referenced
+            self.set(qn("xsi:foo"), "bar")
+            element.set(qn("xsi:type"), "dcterms:W3CDTF")
+            del self.attrib[qn("xsi:foo")]
+
+    def _set_element_text(self, prop_name: str, value: Any) -> None:
+        """Set string value of `name` property to `value`."""
+        if not isinstance(value, str):
+            value = str(value)
+
+        if len(value) > 255:
+            tmpl = "exceeded 255 char limit for property, got:\n\n'%s'"
+            raise ValueError(tmpl % value)
+        element = self._get_or_add(prop_name)
+        element.text = value
+
+    def _text_of_element(self, property_name: str) -> str:
+        """The text in the element matching `property_name`.
+
+        The empty string if the element is not present or contains no text.
+        """
+        element = getattr(self, property_name)
+        if element is None:
+            return ""
+        if element.text is None:
+            return ""
+        return element.text
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/document.py b/.venv/lib/python3.12/site-packages/docx/oxml/document.py
new file mode 100644
index 00000000..36819ef7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/document.py
@@ -0,0 +1,88 @@
+"""Custom element classes that correspond to the document part, e.g. <w:document>."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, List
+
+from docx.oxml.section import CT_SectPr
+from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrMore, ZeroOrOne
+
+if TYPE_CHECKING:
+    from docx.oxml.table import CT_Tbl
+    from docx.oxml.text.paragraph import CT_P
+
+
+class CT_Document(BaseOxmlElement):
+    """``<w:document>`` element, the root element of a document.xml file."""
+
+    body: CT_Body = ZeroOrOne("w:body")  # pyright: ignore[reportAssignmentType]
+
+    @property
+    def sectPr_lst(self) -> List[CT_SectPr]:
+        """All `w:sectPr` elements directly accessible from document element.
+
+        Note this does not include a `sectPr` child in a paragraphs wrapped in
+        revision marks or other intervening layer, perhaps `w:sdt` or customXml
+        elements.
+
+        `w:sectPr` elements appear in document order. The last one is always
+        `w:body/w:sectPr`, all preceding are `w:p/w:pPr/w:sectPr`.
+        """
+        xpath = "./w:body/w:p/w:pPr/w:sectPr | ./w:body/w:sectPr"
+        return self.xpath(xpath)
+
+
+class CT_Body(BaseOxmlElement):
+    """`w:body`, the container element for the main document story in `document.xml`."""
+
+    add_p: Callable[[], CT_P]
+    get_or_add_sectPr: Callable[[], CT_SectPr]
+    p_lst: List[CT_P]
+    tbl_lst: List[CT_Tbl]
+
+    _insert_tbl: Callable[[CT_Tbl], CT_Tbl]
+
+    p = ZeroOrMore("w:p", successors=("w:sectPr",))
+    tbl = ZeroOrMore("w:tbl", successors=("w:sectPr",))
+    sectPr: CT_SectPr | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:sectPr", successors=()
+    )
+
+    def add_section_break(self) -> CT_SectPr:
+        """Return `w:sectPr` element for new section added at end of document.
+
+        The last `w:sectPr` becomes the second-to-last, with the new `w:sectPr` being an
+        exact clone of the previous one, except that all header and footer references
+        are removed (and are therefore now "inherited" from the prior section).
+
+        A copy of the previously-last `w:sectPr` will now appear in a new `w:p` at the
+        end of the document. The returned `w:sectPr` is the sentinel `w:sectPr` for the
+        document (and as implemented, `is` the prior sentinel `w:sectPr` with headers
+        and footers removed).
+        """
+        # ---get the sectPr at file-end, which controls last section (sections[-1])---
+        sentinel_sectPr = self.get_or_add_sectPr()
+        # ---add exact copy to new `w:p` element; that is now second-to last section---
+        self.add_p().set_sectPr(sentinel_sectPr.clone())
+        # ---remove any header or footer references from "new" last section---
+        for hdrftr_ref in sentinel_sectPr.xpath("w:headerReference|w:footerReference"):
+            sentinel_sectPr.remove(hdrftr_ref)
+        # ---the sentinel `w:sectPr` now controls the new last section---
+        return sentinel_sectPr
+
+    def clear_content(self):
+        """Remove all content child elements from this <w:body> element.
+
+        Leave the <w:sectPr> element if it is present.
+        """
+        for content_elm in self.xpath("./*[not(self::w:sectPr)]"):
+            self.remove(content_elm)
+
+    @property
+    def inner_content_elements(self) -> List[CT_P | CT_Tbl]:
+        """Generate all `w:p` and `w:tbl` elements in this document-body.
+
+        Elements appear in document order. Elements shaded by nesting in a `w:ins` or
+        other "wrapper" element will not be included.
+        """
+        return self.xpath("./w:p | ./w:tbl")
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/drawing.py b/.venv/lib/python3.12/site-packages/docx/oxml/drawing.py
new file mode 100644
index 00000000..5b627f97
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/drawing.py
@@ -0,0 +1,11 @@
+"""Custom element-classes for DrawingML-related elements like `<w:drawing>`.
+
+For legacy reasons, many DrawingML-related elements are in `docx.oxml.shape`. Expect
+those to move over here as we have reason to touch them.
+"""
+
+from docx.oxml.xmlchemy import BaseOxmlElement
+
+
+class CT_Drawing(BaseOxmlElement):
+    """`<w:drawing>` element, containing a DrawingML object like a picture or chart."""
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/exceptions.py b/.venv/lib/python3.12/site-packages/docx/oxml/exceptions.py
new file mode 100644
index 00000000..8919239a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/exceptions.py
@@ -0,0 +1,10 @@
+"""Exceptions for oxml sub-package."""
+
+
+class XmlchemyError(Exception):
+    """Generic error class."""
+
+
+class InvalidXmlError(XmlchemyError):
+    """Raised when invalid XML is encountered, such as on attempt to access a missing
+    required child element."""
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/ns.py b/.venv/lib/python3.12/site-packages/docx/oxml/ns.py
new file mode 100644
index 00000000..5bed1e6a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/ns.py
@@ -0,0 +1,109 @@
+"""Namespace-related objects."""
+
+from __future__ import annotations
+
+from typing import Any, Dict
+
+nsmap = {
+    "a": "http://schemas.openxmlformats.org/drawingml/2006/main",
+    "c": "http://schemas.openxmlformats.org/drawingml/2006/chart",
+    "cp": "http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
+    "dc": "http://purl.org/dc/elements/1.1/",
+    "dcmitype": "http://purl.org/dc/dcmitype/",
+    "dcterms": "http://purl.org/dc/terms/",
+    "dgm": "http://schemas.openxmlformats.org/drawingml/2006/diagram",
+    "m": "http://schemas.openxmlformats.org/officeDocument/2006/math",
+    "pic": "http://schemas.openxmlformats.org/drawingml/2006/picture",
+    "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
+    "sl": "http://schemas.openxmlformats.org/schemaLibrary/2006/main",
+    "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
+    "w14": "http://schemas.microsoft.com/office/word/2010/wordml",
+    "wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
+    "xml": "http://www.w3.org/XML/1998/namespace",
+    "xsi": "http://www.w3.org/2001/XMLSchema-instance",
+}
+
+pfxmap = {value: key for key, value in nsmap.items()}
+
+
+class NamespacePrefixedTag(str):
+    """Value object that knows the semantics of an XML tag having a namespace prefix."""
+
+    def __new__(cls, nstag: str, *args: Any):
+        return super(NamespacePrefixedTag, cls).__new__(cls, nstag)
+
+    def __init__(self, nstag: str):
+        self._pfx, self._local_part = nstag.split(":")
+        self._ns_uri = nsmap[self._pfx]
+
+    @property
+    def clark_name(self) -> str:
+        return "{%s}%s" % (self._ns_uri, self._local_part)
+
+    @classmethod
+    def from_clark_name(cls, clark_name: str) -> NamespacePrefixedTag:
+        nsuri, local_name = clark_name[1:].split("}")
+        nstag = "%s:%s" % (pfxmap[nsuri], local_name)
+        return cls(nstag)
+
+    @property
+    def local_part(self) -> str:
+        """The local part of this tag.
+
+        E.g. "foobar" is returned for tag "f:foobar".
+        """
+        return self._local_part
+
+    @property
+    def nsmap(self) -> Dict[str, str]:
+        """Single-member dict mapping prefix of this tag to it's namespace name.
+
+        Example: `{"f": "http://foo/bar"}`. This is handy for passing to xpath calls
+        and other uses.
+        """
+        return {self._pfx: self._ns_uri}
+
+    @property
+    def nspfx(self) -> str:
+        """The namespace-prefix for this tag.
+
+        For example, "f" is returned for tag "f:foobar".
+        """
+        return self._pfx
+
+    @property
+    def nsuri(self) -> str:
+        """The namespace URI for this tag.
+
+        For example, "http://foo/bar" would be returned for tag "f:foobar" if the "f"
+        prefix maps to "http://foo/bar" in nsmap.
+        """
+        return self._ns_uri
+
+
+def nsdecls(*prefixes: str) -> str:
+    """Namespace declaration including each namespace-prefix in `prefixes`.
+
+    Handy for adding required namespace declarations to a tree root element.
+    """
+    return " ".join(['xmlns:%s="%s"' % (pfx, nsmap[pfx]) for pfx in prefixes])
+
+
+def nspfxmap(*nspfxs: str) -> Dict[str, str]:
+    """Subset namespace-prefix mappings specified by *nspfxs*.
+
+    Any number of namespace prefixes can be supplied, e.g. namespaces("a", "r", "p").
+    """
+    return {pfx: nsmap[pfx] for pfx in nspfxs}
+
+
+def qn(tag: str) -> str:
+    """Stands for "qualified name".
+
+    This utility function converts a familiar namespace-prefixed tag name like "w:p"
+    into a Clark-notation qualified tag name for lxml. For example, `qn("w:p")` returns
+    "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p".
+    """
+    prefix, tagroot = tag.split(":")
+    uri = nsmap[prefix]
+    return "{%s}%s" % (uri, tagroot)
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/numbering.py b/.venv/lib/python3.12/site-packages/docx/oxml/numbering.py
new file mode 100644
index 00000000..3512de65
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/numbering.py
@@ -0,0 +1,109 @@
+"""Custom element classes related to the numbering part."""
+
+from docx.oxml.parser import OxmlElement
+from docx.oxml.shared import CT_DecimalNumber
+from docx.oxml.simpletypes import ST_DecimalNumber
+from docx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OneAndOnlyOne,
+    RequiredAttribute,
+    ZeroOrMore,
+    ZeroOrOne,
+)
+
+
+class CT_Num(BaseOxmlElement):
+    """``<w:num>`` element, which represents a concrete list definition instance, having
+    a required child <w:abstractNumId> that references an abstract numbering definition
+    that defines most of the formatting details."""
+
+    abstractNumId = OneAndOnlyOne("w:abstractNumId")
+    lvlOverride = ZeroOrMore("w:lvlOverride")
+    numId = RequiredAttribute("w:numId", ST_DecimalNumber)
+
+    def add_lvlOverride(self, ilvl):
+        """Return a newly added CT_NumLvl (<w:lvlOverride>) element having its ``ilvl``
+        attribute set to `ilvl`."""
+        return self._add_lvlOverride(ilvl=ilvl)
+
+    @classmethod
+    def new(cls, num_id, abstractNum_id):
+        """Return a new ``<w:num>`` element having numId of `num_id` and having a
+        ``<w:abstractNumId>`` child with val attribute set to `abstractNum_id`."""
+        num = OxmlElement("w:num")
+        num.numId = num_id
+        abstractNumId = CT_DecimalNumber.new("w:abstractNumId", abstractNum_id)
+        num.append(abstractNumId)
+        return num
+
+
+class CT_NumLvl(BaseOxmlElement):
+    """``<w:lvlOverride>`` element, which identifies a level in a list definition to
+    override with settings it contains."""
+
+    startOverride = ZeroOrOne("w:startOverride", successors=("w:lvl",))
+    ilvl = RequiredAttribute("w:ilvl", ST_DecimalNumber)
+
+    def add_startOverride(self, val):
+        """Return a newly added CT_DecimalNumber element having tagname
+        ``w:startOverride`` and ``val`` attribute set to `val`."""
+        return self._add_startOverride(val=val)
+
+
+class CT_NumPr(BaseOxmlElement):
+    """A ``<w:numPr>`` element, a container for numbering properties applied to a
+    paragraph."""
+
+    ilvl = ZeroOrOne("w:ilvl", successors=("w:numId", "w:numberingChange", "w:ins"))
+    numId = ZeroOrOne("w:numId", successors=("w:numberingChange", "w:ins"))
+
+    # @ilvl.setter
+    # def _set_ilvl(self, val):
+    #     """
+    #     Get or add a <w:ilvl> child and set its ``w:val`` attribute to `val`.
+    #     """
+    #     ilvl = self.get_or_add_ilvl()
+    #     ilvl.val = val
+
+    # @numId.setter
+    # def numId(self, val):
+    #     """
+    #     Get or add a <w:numId> child and set its ``w:val`` attribute to
+    #     `val`.
+    #     """
+    #     numId = self.get_or_add_numId()
+    #     numId.val = val
+
+
+class CT_Numbering(BaseOxmlElement):
+    """``<w:numbering>`` element, the root element of a numbering part, i.e.
+    numbering.xml."""
+
+    num = ZeroOrMore("w:num", successors=("w:numIdMacAtCleanup",))
+
+    def add_num(self, abstractNum_id):
+        """Return a newly added CT_Num (<w:num>) element referencing the abstract
+        numbering definition identified by `abstractNum_id`."""
+        next_num_id = self._next_numId
+        num = CT_Num.new(next_num_id, abstractNum_id)
+        return self._insert_num(num)
+
+    def num_having_numId(self, numId):
+        """Return the ``<w:num>`` child element having ``numId`` attribute matching
+        `numId`."""
+        xpath = './w:num[@w:numId="%d"]' % numId
+        try:
+            return self.xpath(xpath)[0]
+        except IndexError:
+            raise KeyError("no <w:num> element with numId %d" % numId)
+
+    @property
+    def _next_numId(self):
+        """The first ``numId`` unused by a ``<w:num>`` element, starting at 1 and
+        filling any gaps in numbering between existing ``<w:num>`` elements."""
+        numId_strs = self.xpath("./w:num/@w:numId")
+        num_ids = [int(numId_str) for numId_str in numId_strs]
+        for num in range(1, len(num_ids) + 2):
+            if num not in num_ids:
+                break
+        return num
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/parser.py b/.venv/lib/python3.12/site-packages/docx/oxml/parser.py
new file mode 100644
index 00000000..e16ba30b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/parser.py
@@ -0,0 +1,62 @@
+# pyright: reportImportCycles=false
+
+"""XML parser for python-docx."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Dict, Type, cast
+
+from lxml import etree
+
+from docx.oxml.ns import NamespacePrefixedTag, nsmap
+
+if TYPE_CHECKING:
+    from docx.oxml.xmlchemy import BaseOxmlElement
+
+
+# -- configure XML parser --
+element_class_lookup = etree.ElementNamespaceClassLookup()
+oxml_parser = etree.XMLParser(remove_blank_text=True, resolve_entities=False)
+oxml_parser.set_element_class_lookup(element_class_lookup)
+
+
+def parse_xml(xml: str | bytes) -> "BaseOxmlElement":
+    """Root lxml element obtained by parsing XML character string `xml`.
+
+    The custom parser is used, so custom element classes are produced for elements in
+    `xml` that have them.
+    """
+    return cast("BaseOxmlElement", etree.fromstring(xml, oxml_parser))
+
+
+def register_element_cls(tag: str, cls: Type["BaseOxmlElement"]):
+    """Register an lxml custom element-class to use for `tag`.
+
+    A instance of `cls` to be constructed when the oxml parser encounters an element
+    with matching `tag`. `tag` is a string of the form `nspfx:tagroot`, e.g.
+    `'w:document'`.
+    """
+    nspfx, tagroot = tag.split(":")
+    namespace = element_class_lookup.get_namespace(nsmap[nspfx])
+    namespace[tagroot] = cls
+
+
+def OxmlElement(
+    nsptag_str: str,
+    attrs: Dict[str, str] | None = None,
+    nsdecls: Dict[str, str] | None = None,
+) -> BaseOxmlElement | etree._Element:  # pyright: ignore[reportPrivateUsage]
+    """Return a 'loose' lxml element having the tag specified by `nsptag_str`.
+
+    The tag in `nsptag_str` must contain the standard namespace prefix, e.g. `a:tbl`.
+    The resulting element is an instance of the custom element class for this tag name
+    if one is defined. A dictionary of attribute values may be provided as `attrs`; they
+    are set if present. All namespaces defined in the dict `nsdecls` are declared in the
+    element using the key as the prefix and the value as the namespace name. If
+    `nsdecls` is not provided, a single namespace declaration is added based on the
+    prefix on `nsptag_str`.
+    """
+    nsptag = NamespacePrefixedTag(nsptag_str)
+    if nsdecls is None:
+        nsdecls = nsptag.nsmap
+    return oxml_parser.makeelement(nsptag.clark_name, attrib=attrs, nsmap=nsdecls)
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/section.py b/.venv/lib/python3.12/site-packages/docx/oxml/section.py
new file mode 100644
index 00000000..71072e2d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/section.py
@@ -0,0 +1,537 @@
+"""Section-related custom element classes."""
+
+from __future__ import annotations
+
+from copy import deepcopy
+from typing import Callable, Iterator, List, Sequence, cast
+
+from lxml import etree
+from typing_extensions import TypeAlias
+
+from docx.enum.section import WD_HEADER_FOOTER, WD_ORIENTATION, WD_SECTION_START
+from docx.oxml.ns import nsmap
+from docx.oxml.shared import CT_OnOff
+from docx.oxml.simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure, XsdString
+from docx.oxml.table import CT_Tbl
+from docx.oxml.text.paragraph import CT_P
+from docx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrMore,
+    ZeroOrOne,
+)
+from docx.shared import Length, lazyproperty
+
+BlockElement: TypeAlias = "CT_P | CT_Tbl"
+
+
+class CT_HdrFtr(BaseOxmlElement):
+    """`w:hdr` and `w:ftr`, the root element for header and footer part respectively."""
+
+    add_p: Callable[[], CT_P]
+    p_lst: List[CT_P]
+    tbl_lst: List[CT_Tbl]
+
+    _insert_tbl: Callable[[CT_Tbl], CT_Tbl]
+
+    p = ZeroOrMore("w:p", successors=())
+    tbl = ZeroOrMore("w:tbl", successors=())
+
+    @property
+    def inner_content_elements(self) -> List[CT_P | CT_Tbl]:
+        """Generate all `w:p` and `w:tbl` elements in this header or footer.
+
+        Elements appear in document order. Elements shaded by nesting in a `w:ins` or
+        other "wrapper" element will not be included.
+        """
+        return self.xpath("./w:p | ./w:tbl")
+
+
+class CT_HdrFtrRef(BaseOxmlElement):
+    """`w:headerReference` and `w:footerReference` elements."""
+
+    type_: WD_HEADER_FOOTER = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:type", WD_HEADER_FOOTER
+    )
+    rId: str = RequiredAttribute("r:id", XsdString)  # pyright: ignore[reportAssignmentType]
+
+
+class CT_PageMar(BaseOxmlElement):
+    """``<w:pgMar>`` element, defining page margins."""
+
+    top: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:top", ST_SignedTwipsMeasure
+    )
+    right: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:right", ST_TwipsMeasure
+    )
+    bottom: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:bottom", ST_SignedTwipsMeasure
+    )
+    left: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:left", ST_TwipsMeasure
+    )
+    header: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:header", ST_TwipsMeasure
+    )
+    footer: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:footer", ST_TwipsMeasure
+    )
+    gutter: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:gutter", ST_TwipsMeasure
+    )
+
+
+class CT_PageSz(BaseOxmlElement):
+    """``<w:pgSz>`` element, defining page dimensions and orientation."""
+
+    w: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:w", ST_TwipsMeasure
+    )
+    h: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:h", ST_TwipsMeasure
+    )
+    orient: WD_ORIENTATION = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:orient", WD_ORIENTATION, default=WD_ORIENTATION.PORTRAIT
+    )
+
+
+class CT_SectPr(BaseOxmlElement):
+    """`w:sectPr` element, the container element for section properties."""
+
+    get_or_add_pgMar: Callable[[], CT_PageMar]
+    get_or_add_pgSz: Callable[[], CT_PageSz]
+    get_or_add_titlePg: Callable[[], CT_OnOff]
+    get_or_add_type: Callable[[], CT_SectType]
+    _add_footerReference: Callable[[], CT_HdrFtrRef]
+    _add_headerReference: Callable[[], CT_HdrFtrRef]
+    _remove_titlePg: Callable[[], None]
+    _remove_type: Callable[[], None]
+
+    _tag_seq = (
+        "w:footnotePr",
+        "w:endnotePr",
+        "w:type",
+        "w:pgSz",
+        "w:pgMar",
+        "w:paperSrc",
+        "w:pgBorders",
+        "w:lnNumType",
+        "w:pgNumType",
+        "w:cols",
+        "w:formProt",
+        "w:vAlign",
+        "w:noEndnote",
+        "w:titlePg",
+        "w:textDirection",
+        "w:bidi",
+        "w:rtlGutter",
+        "w:docGrid",
+        "w:printerSettings",
+        "w:sectPrChange",
+    )
+    headerReference = ZeroOrMore("w:headerReference", successors=_tag_seq)
+    footerReference = ZeroOrMore("w:footerReference", successors=_tag_seq)
+    type: CT_SectType | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:type", successors=_tag_seq[3:]
+    )
+    pgSz: CT_PageSz | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:pgSz", successors=_tag_seq[4:]
+    )
+    pgMar: CT_PageMar | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:pgMar", successors=_tag_seq[5:]
+    )
+    titlePg: CT_OnOff | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:titlePg", successors=_tag_seq[14:]
+    )
+    del _tag_seq
+
+    def add_footerReference(self, type_: WD_HEADER_FOOTER, rId: str) -> CT_HdrFtrRef:
+        """Return newly added CT_HdrFtrRef element of `type_` with `rId`.
+
+        The element tag is `w:footerReference`.
+        """
+        footerReference = self._add_footerReference()
+        footerReference.type_ = type_
+        footerReference.rId = rId
+        return footerReference
+
+    def add_headerReference(self, type_: WD_HEADER_FOOTER, rId: str) -> CT_HdrFtrRef:
+        """Return newly added CT_HdrFtrRef element of `type_` with `rId`.
+
+        The element tag is `w:headerReference`.
+        """
+        headerReference = self._add_headerReference()
+        headerReference.type_ = type_
+        headerReference.rId = rId
+        return headerReference
+
+    @property
+    def bottom_margin(self) -> Length | None:
+        """Value of the `w:bottom` attr of `<w:pgMar>` child element, as |Length|.
+
+        |None| when either the element or the attribute is not present.
+        """
+        pgMar = self.pgMar
+        if pgMar is None:
+            return None
+        return pgMar.bottom
+
+    @bottom_margin.setter
+    def bottom_margin(self, value: int | Length | None):
+        pgMar = self.get_or_add_pgMar()
+        pgMar.bottom = value if value is None or isinstance(value, Length) else Length(value)
+
+    def clone(self) -> CT_SectPr:
+        """Return an exact duplicate of this ``<w:sectPr>`` element tree suitable for
+        use in adding a section break.
+
+        All rsid* attributes are removed from the root ``<w:sectPr>`` element.
+        """
+        cloned_sectPr = deepcopy(self)
+        cloned_sectPr.attrib.clear()
+        return cloned_sectPr
+
+    @property
+    def footer(self) -> Length | None:
+        """Distance from bottom edge of page to bottom edge of the footer.
+
+        This is the value of the `w:footer` attribute in the `w:pgMar` child element,
+        as a |Length| object, or |None| if either the element or the attribute is not
+        present.
+        """
+        pgMar = self.pgMar
+        if pgMar is None:
+            return None
+        return pgMar.footer
+
+    @footer.setter
+    def footer(self, value: int | Length | None):
+        pgMar = self.get_or_add_pgMar()
+        pgMar.footer = value if value is None or isinstance(value, Length) else Length(value)
+
+    def get_footerReference(self, type_: WD_HEADER_FOOTER) -> CT_HdrFtrRef | None:
+        """Return footerReference element of `type_` or None if not present."""
+        path = "./w:footerReference[@w:type='%s']" % WD_HEADER_FOOTER.to_xml(type_)
+        footerReferences = self.xpath(path)
+        if not footerReferences:
+            return None
+        return footerReferences[0]
+
+    def get_headerReference(self, type_: WD_HEADER_FOOTER) -> CT_HdrFtrRef | None:
+        """Return headerReference element of `type_` or None if not present."""
+        matching_headerReferences = self.xpath(
+            "./w:headerReference[@w:type='%s']" % WD_HEADER_FOOTER.to_xml(type_)
+        )
+        if len(matching_headerReferences) == 0:
+            return None
+        return matching_headerReferences[0]
+
+    @property
+    def gutter(self) -> Length | None:
+        """The value of the ``w:gutter`` attribute in the ``<w:pgMar>`` child element,
+        as a |Length| object, or |None| if either the element or the attribute is not
+        present."""
+        pgMar = self.pgMar
+        if pgMar is None:
+            return None
+        return pgMar.gutter
+
+    @gutter.setter
+    def gutter(self, value: int | Length | None):
+        pgMar = self.get_or_add_pgMar()
+        pgMar.gutter = value if value is None or isinstance(value, Length) else Length(value)
+
+    @property
+    def header(self) -> Length | None:
+        """Distance from top edge of page to top edge of header.
+
+        This value comes from the `w:header` attribute on the `w:pgMar` child element.
+        |None| if either the element or the attribute is not present.
+        """
+        pgMar = self.pgMar
+        if pgMar is None:
+            return None
+        return pgMar.header
+
+    @header.setter
+    def header(self, value: int | Length | None):
+        pgMar = self.get_or_add_pgMar()
+        pgMar.header = value if value is None or isinstance(value, Length) else Length(value)
+
+    def iter_inner_content(self) -> Iterator[CT_P | CT_Tbl]:
+        """Generate all `w:p` and `w:tbl` elements in this section.
+
+        Elements appear in document order. Elements shaded by nesting in a `w:ins` or
+        other "wrapper" element will not be included.
+        """
+        return _SectBlockElementIterator.iter_sect_block_elements(self)
+
+    @property
+    def left_margin(self) -> Length | None:
+        """The value of the ``w:left`` attribute in the ``<w:pgMar>`` child element, as
+        a |Length| object, or |None| if either the element or the attribute is not
+        present."""
+        pgMar = self.pgMar
+        if pgMar is None:
+            return None
+        return pgMar.left
+
+    @left_margin.setter
+    def left_margin(self, value: int | Length | None):
+        pgMar = self.get_or_add_pgMar()
+        pgMar.left = value if value is None or isinstance(value, Length) else Length(value)
+
+    @property
+    def orientation(self) -> WD_ORIENTATION:
+        """`WD_ORIENTATION` member indicating page-orientation for this section.
+
+        This is the value of the `orient` attribute on the `w:pgSz` child, or
+        `WD_ORIENTATION.PORTRAIT` if not present.
+        """
+        pgSz = self.pgSz
+        if pgSz is None:
+            return WD_ORIENTATION.PORTRAIT
+        return pgSz.orient
+
+    @orientation.setter
+    def orientation(self, value: WD_ORIENTATION | None):
+        pgSz = self.get_or_add_pgSz()
+        pgSz.orient = value if value else WD_ORIENTATION.PORTRAIT
+
+    @property
+    def page_height(self) -> Length | None:
+        """Value in EMU of the `h` attribute of the `w:pgSz` child element.
+
+        |None| if not present.
+        """
+        pgSz = self.pgSz
+        if pgSz is None:
+            return None
+        return pgSz.h
+
+    @page_height.setter
+    def page_height(self, value: Length | None):
+        pgSz = self.get_or_add_pgSz()
+        pgSz.h = value
+
+    @property
+    def page_width(self) -> Length | None:
+        """Value in EMU of the ``w`` attribute of the ``<w:pgSz>`` child element.
+
+        |None| if not present.
+        """
+        pgSz = self.pgSz
+        if pgSz is None:
+            return None
+        return pgSz.w
+
+    @page_width.setter
+    def page_width(self, value: Length | None):
+        pgSz = self.get_or_add_pgSz()
+        pgSz.w = value
+
+    @property
+    def preceding_sectPr(self) -> CT_SectPr | None:
+        """SectPr immediately preceding this one or None if this is the first."""
+        # -- [1] predicate returns list of zero or one value --
+        preceding_sectPrs = self.xpath("./preceding::w:sectPr[1]")
+        return preceding_sectPrs[0] if len(preceding_sectPrs) > 0 else None
+
+    def remove_footerReference(self, type_: WD_HEADER_FOOTER) -> str:
+        """Return rId of w:footerReference child of `type_` after removing it."""
+        footerReference = self.get_footerReference(type_)
+        if footerReference is None:
+            # -- should never happen, but to satisfy type-check and just in case --
+            raise ValueError("CT_SectPr has no footer reference")
+        rId = footerReference.rId
+        self.remove(footerReference)
+        return rId
+
+    def remove_headerReference(self, type_: WD_HEADER_FOOTER):
+        """Return rId of w:headerReference child of `type_` after removing it."""
+        headerReference = self.get_headerReference(type_)
+        if headerReference is None:
+            # -- should never happen, but to satisfy type-check and just in case --
+            raise ValueError("CT_SectPr has no header reference")
+        rId = headerReference.rId
+        self.remove(headerReference)
+        return rId
+
+    @property
+    def right_margin(self) -> Length | None:
+        """The value of the ``w:right`` attribute in the ``<w:pgMar>`` child element, as
+        a |Length| object, or |None| if either the element or the attribute is not
+        present."""
+        pgMar = self.pgMar
+        if pgMar is None:
+            return None
+        return pgMar.right
+
+    @right_margin.setter
+    def right_margin(self, value: Length | None):
+        pgMar = self.get_or_add_pgMar()
+        pgMar.right = value
+
+    @property
+    def start_type(self) -> WD_SECTION_START:
+        """The member of the ``WD_SECTION_START`` enumeration corresponding to the value
+        of the ``val`` attribute of the ``<w:type>`` child element, or
+        ``WD_SECTION_START.NEW_PAGE`` if not present."""
+        type = self.type
+        if type is None or type.val is None:
+            return WD_SECTION_START.NEW_PAGE
+        return type.val
+
+    @start_type.setter
+    def start_type(self, value: WD_SECTION_START | None):
+        if value is None or value is WD_SECTION_START.NEW_PAGE:
+            self._remove_type()
+            return
+        type = self.get_or_add_type()
+        type.val = value
+
+    @property
+    def titlePg_val(self) -> bool:
+        """Value of `w:titlePg/@val` or |False| if `./w:titlePg` is not present."""
+        titlePg = self.titlePg
+        if titlePg is None:
+            return False
+        return titlePg.val
+
+    @titlePg_val.setter
+    def titlePg_val(self, value: bool | None):
+        if value in [None, False]:
+            self._remove_titlePg()
+        else:
+            self.get_or_add_titlePg().val = True
+
+    @property
+    def top_margin(self) -> Length | None:
+        """The value of the ``w:top`` attribute in the ``<w:pgMar>`` child element, as a
+        |Length| object, or |None| if either the element or the attribute is not
+        present."""
+        pgMar = self.pgMar
+        if pgMar is None:
+            return None
+        return pgMar.top
+
+    @top_margin.setter
+    def top_margin(self, value: Length | None):
+        pgMar = self.get_or_add_pgMar()
+        pgMar.top = value
+
+
+class CT_SectType(BaseOxmlElement):
+    """``<w:sectType>`` element, defining the section start type."""
+
+    val: WD_SECTION_START | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:val", WD_SECTION_START
+    )
+
+
+# == HELPERS =========================================================================
+
+
+class _SectBlockElementIterator:
+    """Generates the block-item XML elements in a section.
+
+    A block-item element is a `CT_P` (paragraph) or a `CT_Tbl` (table).
+    """
+
+    _compiled_blocks_xpath: etree.XPath | None = None
+    _compiled_count_xpath: etree.XPath | None = None
+
+    def __init__(self, sectPr: CT_SectPr):
+        self._sectPr = sectPr
+
+    @classmethod
+    def iter_sect_block_elements(cls, sectPr: CT_SectPr) -> Iterator[BlockElement]:
+        """Generate each CT_P or CT_Tbl element within extents governed by `sectPr`."""
+        return cls(sectPr)._iter_sect_block_elements()
+
+    def _iter_sect_block_elements(self) -> Iterator[BlockElement]:
+        """Generate each CT_P or CT_Tbl element in section."""
+        # -- General strategy is to get all block (<w;p> and <w:tbl>) elements from
+        # -- start of doc to and including this section, then compute the count of those
+        # -- elements that came from prior sections and skip that many to leave only the
+        # -- ones in this section. It's possible to express this "between here and
+        # -- there" (end of prior section and end of this one) concept in XPath, but it
+        # -- would be harder to follow because there are special cases (e.g. no prior
+        # -- section) and the boundary expressions are fairly hairy. I also believe it
+        # -- would be computationally more expensive than doing it this straighforward
+        # -- albeit (theoretically) slightly wasteful way.
+
+        sectPr, sectPrs = self._sectPr, self._sectPrs
+        sectPr_idx = sectPrs.index(sectPr)
+
+        # -- count block items belonging to prior sections --
+        n_blks_to_skip = (
+            0
+            if sectPr_idx == 0
+            else self._count_of_blocks_in_and_above_section(sectPrs[sectPr_idx - 1])
+        )
+
+        # -- and skip those in set of all blks from doc start to end of this section --
+        for element in self._blocks_in_and_above_section(sectPr)[n_blks_to_skip:]:
+            yield element
+
+    def _blocks_in_and_above_section(self, sectPr: CT_SectPr) -> Sequence[BlockElement]:
+        """All ps and tbls in section defined by `sectPr` and all prior sections."""
+        if self._compiled_blocks_xpath is None:
+            self._compiled_blocks_xpath = etree.XPath(
+                self._blocks_in_and_above_section_xpath,
+                namespaces=nsmap,
+                regexp=False,
+            )
+        xpath = self._compiled_blocks_xpath
+        # -- XPath callable results are Any (basically), so need a cast. --
+        return cast(Sequence[BlockElement], xpath(sectPr))
+
+    @lazyproperty
+    def _blocks_in_and_above_section_xpath(self) -> str:
+        """XPath expr for ps and tbls in context of a sectPr and all prior sectPrs."""
+        # -- "p_sect" is a section with sectPr located at w:p/w:pPr/w:sectPr.
+        # -- "body_sect" is a section with sectPr located at w:body/w:sectPr. The last
+        # -- section in the document is a "body_sect". All others are of the "p_sect"
+        # -- variety. "term" means "terminal", like the last p or tbl in the section.
+        # -- "pred" means "predecessor", like a preceding p or tbl in the section.
+
+        # -- the terminal block in a p-based sect is the p the sectPr appears in --
+        p_sect_term_block = "./parent::w:pPr/parent::w:p"
+        # -- the terminus of a body-based sect is the sectPr itself (not a block) --
+        body_sect_term = "self::w:sectPr[parent::w:body]"
+        # -- all the ps and tbls preceding (but not including) the context node --
+        pred_ps_and_tbls = "preceding-sibling::*[self::w:p | self::w:tbl]"
+
+        # -- p_sect_term_block and body_sect_term(inus) are mutually exclusive. So the
+        # -- result is either the union of nodes found by the first two selectors or the
+        # -- nodes found by the last selector, never both.
+        return (
+            # -- include the p containing a sectPr --
+            f"{p_sect_term_block}"
+            # -- along with all the blocks that precede it --
+            f" | {p_sect_term_block}/{pred_ps_and_tbls}"
+            # -- or all the preceding blocks if sectPr is body-based (last sectPr) --
+            f" | {body_sect_term}/{pred_ps_and_tbls}"
+        )
+
+    def _count_of_blocks_in_and_above_section(self, sectPr: CT_SectPr) -> int:
+        """All ps and tbls in section defined by `sectPr` and all prior sections."""
+        if self._compiled_count_xpath is None:
+            self._compiled_count_xpath = etree.XPath(
+                f"count({self._blocks_in_and_above_section_xpath})",
+                namespaces=nsmap,
+                regexp=False,
+            )
+        xpath = self._compiled_count_xpath
+        # -- numeric XPath results are always float, so need an int() conversion --
+        return int(cast(float, xpath(sectPr)))
+
+    @lazyproperty
+    def _sectPrs(self) -> Sequence[CT_SectPr]:
+        """All w:sectPr elements in document, in document-order."""
+        return self._sectPr.xpath(
+            "/w:document/w:body/w:p/w:pPr/w:sectPr | /w:document/w:body/w:sectPr",
+        )
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/settings.py b/.venv/lib/python3.12/site-packages/docx/oxml/settings.py
new file mode 100644
index 00000000..d5bb41a6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/settings.py
@@ -0,0 +1,138 @@
+"""Custom element classes related to document settings."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable
+
+from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne
+
+if TYPE_CHECKING:
+    from docx.oxml.shared import CT_OnOff
+
+
+class CT_Settings(BaseOxmlElement):
+    """`w:settings` element, root element for the settings part."""
+
+    get_or_add_evenAndOddHeaders: Callable[[], CT_OnOff]
+    _remove_evenAndOddHeaders: Callable[[], None]
+
+    _tag_seq = (
+        "w:writeProtection",
+        "w:view",
+        "w:zoom",
+        "w:removePersonalInformation",
+        "w:removeDateAndTime",
+        "w:doNotDisplayPageBoundaries",
+        "w:displayBackgroundShape",
+        "w:printPostScriptOverText",
+        "w:printFractionalCharacterWidth",
+        "w:printFormsData",
+        "w:embedTrueTypeFonts",
+        "w:embedSystemFonts",
+        "w:saveSubsetFonts",
+        "w:saveFormsData",
+        "w:mirrorMargins",
+        "w:alignBordersAndEdges",
+        "w:bordersDoNotSurroundHeader",
+        "w:bordersDoNotSurroundFooter",
+        "w:gutterAtTop",
+        "w:hideSpellingErrors",
+        "w:hideGrammaticalErrors",
+        "w:activeWritingStyle",
+        "w:proofState",
+        "w:formsDesign",
+        "w:attachedTemplate",
+        "w:linkStyles",
+        "w:stylePaneFormatFilter",
+        "w:stylePaneSortMethod",
+        "w:documentType",
+        "w:mailMerge",
+        "w:revisionView",
+        "w:trackRevisions",
+        "w:doNotTrackMoves",
+        "w:doNotTrackFormatting",
+        "w:documentProtection",
+        "w:autoFormatOverride",
+        "w:styleLockTheme",
+        "w:styleLockQFSet",
+        "w:defaultTabStop",
+        "w:autoHyphenation",
+        "w:consecutiveHyphenLimit",
+        "w:hyphenationZone",
+        "w:doNotHyphenateCaps",
+        "w:showEnvelope",
+        "w:summaryLength",
+        "w:clickAndTypeStyle",
+        "w:defaultTableStyle",
+        "w:evenAndOddHeaders",
+        "w:bookFoldRevPrinting",
+        "w:bookFoldPrinting",
+        "w:bookFoldPrintingSheets",
+        "w:drawingGridHorizontalSpacing",
+        "w:drawingGridVerticalSpacing",
+        "w:displayHorizontalDrawingGridEvery",
+        "w:displayVerticalDrawingGridEvery",
+        "w:doNotUseMarginsForDrawingGridOrigin",
+        "w:drawingGridHorizontalOrigin",
+        "w:drawingGridVerticalOrigin",
+        "w:doNotShadeFormData",
+        "w:noPunctuationKerning",
+        "w:characterSpacingControl",
+        "w:printTwoOnOne",
+        "w:strictFirstAndLastChars",
+        "w:noLineBreaksAfter",
+        "w:noLineBreaksBefore",
+        "w:savePreviewPicture",
+        "w:doNotValidateAgainstSchema",
+        "w:saveInvalidXml",
+        "w:ignoreMixedContent",
+        "w:alwaysShowPlaceholderText",
+        "w:doNotDemarcateInvalidXml",
+        "w:saveXmlDataOnly",
+        "w:useXSLTWhenSaving",
+        "w:saveThroughXslt",
+        "w:showXMLTags",
+        "w:alwaysMergeEmptyNamespace",
+        "w:updateFields",
+        "w:hdrShapeDefaults",
+        "w:footnotePr",
+        "w:endnotePr",
+        "w:compat",
+        "w:docVars",
+        "w:rsids",
+        "m:mathPr",
+        "w:attachedSchema",
+        "w:themeFontLang",
+        "w:clrSchemeMapping",
+        "w:doNotIncludeSubdocsInStats",
+        "w:doNotAutoCompressPictures",
+        "w:forceUpgrade",
+        "w:captions",
+        "w:readModeInkLockDown",
+        "w:smartTagType",
+        "sl:schemaLibrary",
+        "w:shapeDefaults",
+        "w:doNotEmbedSmartTags",
+        "w:decimalSymbol",
+        "w:listSeparator",
+    )
+    evenAndOddHeaders: CT_OnOff | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:evenAndOddHeaders", successors=_tag_seq[48:]
+    )
+    del _tag_seq
+
+    @property
+    def evenAndOddHeaders_val(self) -> bool:
+        """Value of `w:evenAndOddHeaders/@w:val` or |None| if not present."""
+        evenAndOddHeaders = self.evenAndOddHeaders
+        if evenAndOddHeaders is None:
+            return False
+        return evenAndOddHeaders.val
+
+    @evenAndOddHeaders_val.setter
+    def evenAndOddHeaders_val(self, value: bool | None):
+        if value is None or value is False:
+            self._remove_evenAndOddHeaders()
+            return
+
+        self.get_or_add_evenAndOddHeaders().val = value
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/shape.py b/.venv/lib/python3.12/site-packages/docx/oxml/shape.py
new file mode 100644
index 00000000..289d3557
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/shape.py
@@ -0,0 +1,302 @@
+"""Custom element classes for shape-related elements like `<w:inline>`."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+from docx.oxml.ns import nsdecls
+from docx.oxml.parser import parse_xml
+from docx.oxml.simpletypes import (
+    ST_Coordinate,
+    ST_DrawingElementId,
+    ST_PositiveCoordinate,
+    ST_RelationshipId,
+    XsdString,
+    XsdToken,
+)
+from docx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OneAndOnlyOne,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrOne,
+)
+
+if TYPE_CHECKING:
+    from docx.shared import Length
+
+
+class CT_Anchor(BaseOxmlElement):
+    """`<wp:anchor>` element, container for a "floating" shape."""
+
+
+class CT_Blip(BaseOxmlElement):
+    """``<a:blip>`` element, specifies image source and adjustments such as alpha and
+    tint."""
+
+    embed: str | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "r:embed", ST_RelationshipId
+    )
+    link: str | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "r:link", ST_RelationshipId
+    )
+
+
+class CT_BlipFillProperties(BaseOxmlElement):
+    """``<pic:blipFill>`` element, specifies picture properties."""
+
+    blip: CT_Blip = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:blip", successors=("a:srcRect", "a:tile", "a:stretch")
+    )
+
+
+class CT_GraphicalObject(BaseOxmlElement):
+    """``<a:graphic>`` element, container for a DrawingML object."""
+
+    graphicData: CT_GraphicalObjectData = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "a:graphicData"
+    )
+
+
+class CT_GraphicalObjectData(BaseOxmlElement):
+    """``<a:graphicData>`` element, container for the XML of a DrawingML object."""
+
+    pic: CT_Picture = ZeroOrOne("pic:pic")  # pyright: ignore[reportAssignmentType]
+    uri: str = RequiredAttribute("uri", XsdToken)  # pyright: ignore[reportAssignmentType]
+
+
+class CT_Inline(BaseOxmlElement):
+    """`<wp:inline>` element, container for an inline shape."""
+
+    extent: CT_PositiveSize2D = OneAndOnlyOne("wp:extent")  # pyright: ignore[reportAssignmentType]
+    docPr: CT_NonVisualDrawingProps = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "wp:docPr"
+    )
+    graphic: CT_GraphicalObject = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "a:graphic"
+    )
+
+    @classmethod
+    def new(cls, cx: Length, cy: Length, shape_id: int, pic: CT_Picture) -> CT_Inline:
+        """Return a new ``<wp:inline>`` element populated with the values passed as
+        parameters."""
+        inline = cast(CT_Inline, parse_xml(cls._inline_xml()))
+        inline.extent.cx = cx
+        inline.extent.cy = cy
+        inline.docPr.id = shape_id
+        inline.docPr.name = "Picture %d" % shape_id
+        inline.graphic.graphicData.uri = "http://schemas.openxmlformats.org/drawingml/2006/picture"
+        inline.graphic.graphicData._insert_pic(pic)
+        return inline
+
+    @classmethod
+    def new_pic_inline(
+        cls, shape_id: int, rId: str, filename: str, cx: Length, cy: Length
+    ) -> CT_Inline:
+        """Create `wp:inline` element containing a `pic:pic` element.
+
+        The contents of the `pic:pic` element is taken from the argument values.
+        """
+        pic_id = 0  # Word doesn't seem to use this, but does not omit it
+        pic = CT_Picture.new(pic_id, filename, rId, cx, cy)
+        inline = cls.new(cx, cy, shape_id, pic)
+        inline.graphic.graphicData._insert_pic(pic)
+        return inline
+
+    @classmethod
+    def _inline_xml(cls):
+        return (
+            "<wp:inline %s>\n"
+            '  <wp:extent cx="914400" cy="914400"/>\n'
+            '  <wp:docPr id="666" name="unnamed"/>\n'
+            "  <wp:cNvGraphicFramePr>\n"
+            '    <a:graphicFrameLocks noChangeAspect="1"/>\n'
+            "  </wp:cNvGraphicFramePr>\n"
+            "  <a:graphic>\n"
+            '    <a:graphicData uri="URI not set"/>\n'
+            "  </a:graphic>\n"
+            "</wp:inline>" % nsdecls("wp", "a", "pic", "r")
+        )
+
+
+class CT_NonVisualDrawingProps(BaseOxmlElement):
+    """Used for ``<wp:docPr>`` element, and perhaps others.
+
+    Specifies the id and name of a DrawingML drawing.
+    """
+
+    id = RequiredAttribute("id", ST_DrawingElementId)
+    name = RequiredAttribute("name", XsdString)
+
+
+class CT_NonVisualPictureProperties(BaseOxmlElement):
+    """``<pic:cNvPicPr>`` element, specifies picture locking and resize behaviors."""
+
+
+class CT_Picture(BaseOxmlElement):
+    """``<pic:pic>`` element, a DrawingML picture."""
+
+    nvPicPr: CT_PictureNonVisual = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "pic:nvPicPr"
+    )
+    blipFill: CT_BlipFillProperties = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "pic:blipFill"
+    )
+    spPr: CT_ShapeProperties = OneAndOnlyOne("pic:spPr")  # pyright: ignore[reportAssignmentType]
+
+    @classmethod
+    def new(cls, pic_id, filename, rId, cx, cy):
+        """Return a new ``<pic:pic>`` element populated with the minimal contents
+        required to define a viable picture element, based on the values passed as
+        parameters."""
+        pic = parse_xml(cls._pic_xml())
+        pic.nvPicPr.cNvPr.id = pic_id
+        pic.nvPicPr.cNvPr.name = filename
+        pic.blipFill.blip.embed = rId
+        pic.spPr.cx = cx
+        pic.spPr.cy = cy
+        return pic
+
+    @classmethod
+    def _pic_xml(cls):
+        return (
+            "<pic:pic %s>\n"
+            "  <pic:nvPicPr>\n"
+            '    <pic:cNvPr id="666" name="unnamed"/>\n'
+            "    <pic:cNvPicPr/>\n"
+            "  </pic:nvPicPr>\n"
+            "  <pic:blipFill>\n"
+            "    <a:blip/>\n"
+            "    <a:stretch>\n"
+            "      <a:fillRect/>\n"
+            "    </a:stretch>\n"
+            "  </pic:blipFill>\n"
+            "  <pic:spPr>\n"
+            "    <a:xfrm>\n"
+            '      <a:off x="0" y="0"/>\n'
+            '      <a:ext cx="914400" cy="914400"/>\n'
+            "    </a:xfrm>\n"
+            '    <a:prstGeom prst="rect"/>\n'
+            "  </pic:spPr>\n"
+            "</pic:pic>" % nsdecls("pic", "a", "r")
+        )
+
+
+class CT_PictureNonVisual(BaseOxmlElement):
+    """``<pic:nvPicPr>`` element, non-visual picture properties."""
+
+    cNvPr = OneAndOnlyOne("pic:cNvPr")
+
+
+class CT_Point2D(BaseOxmlElement):
+    """Used for ``<a:off>`` element, and perhaps others.
+
+    Specifies an x, y coordinate (point).
+    """
+
+    x = RequiredAttribute("x", ST_Coordinate)
+    y = RequiredAttribute("y", ST_Coordinate)
+
+
+class CT_PositiveSize2D(BaseOxmlElement):
+    """Used for ``<wp:extent>`` element, and perhaps others later.
+
+    Specifies the size of a DrawingML drawing.
+    """
+
+    cx: Length = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "cx", ST_PositiveCoordinate
+    )
+    cy: Length = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "cy", ST_PositiveCoordinate
+    )
+
+
+class CT_PresetGeometry2D(BaseOxmlElement):
+    """``<a:prstGeom>`` element, specifies an preset autoshape geometry, such as
+    ``rect``."""
+
+
+class CT_RelativeRect(BaseOxmlElement):
+    """``<a:fillRect>`` element, specifying picture should fill containing rectangle
+    shape."""
+
+
+class CT_ShapeProperties(BaseOxmlElement):
+    """``<pic:spPr>`` element, specifies size and shape of picture container."""
+
+    xfrm = ZeroOrOne(
+        "a:xfrm",
+        successors=(
+            "a:custGeom",
+            "a:prstGeom",
+            "a:ln",
+            "a:effectLst",
+            "a:effectDag",
+            "a:scene3d",
+            "a:sp3d",
+            "a:extLst",
+        ),
+    )
+
+    @property
+    def cx(self):
+        """Shape width as an instance of Emu, or None if not present."""
+        xfrm = self.xfrm
+        if xfrm is None:
+            return None
+        return xfrm.cx
+
+    @cx.setter
+    def cx(self, value):
+        xfrm = self.get_or_add_xfrm()
+        xfrm.cx = value
+
+    @property
+    def cy(self):
+        """Shape height as an instance of Emu, or None if not present."""
+        xfrm = self.xfrm
+        if xfrm is None:
+            return None
+        return xfrm.cy
+
+    @cy.setter
+    def cy(self, value):
+        xfrm = self.get_or_add_xfrm()
+        xfrm.cy = value
+
+
+class CT_StretchInfoProperties(BaseOxmlElement):
+    """``<a:stretch>`` element, specifies how picture should fill its containing
+    shape."""
+
+
+class CT_Transform2D(BaseOxmlElement):
+    """``<a:xfrm>`` element, specifies size and shape of picture container."""
+
+    off = ZeroOrOne("a:off", successors=("a:ext",))
+    ext = ZeroOrOne("a:ext", successors=())
+
+    @property
+    def cx(self):
+        ext = self.ext
+        if ext is None:
+            return None
+        return ext.cx
+
+    @cx.setter
+    def cx(self, value):
+        ext = self.get_or_add_ext()
+        ext.cx = value
+
+    @property
+    def cy(self):
+        ext = self.ext
+        if ext is None:
+            return None
+        return ext.cy
+
+    @cy.setter
+    def cy(self, value):
+        ext = self.get_or_add_ext()
+        ext.cy = value
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/shared.py b/.venv/lib/python3.12/site-packages/docx/oxml/shared.py
new file mode 100644
index 00000000..8c2ebc9a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/shared.py
@@ -0,0 +1,53 @@
+"""Objects shared by modules in the docx.oxml subpackage."""
+
+from __future__ import annotations
+
+from typing import cast
+
+from docx.oxml.ns import qn
+from docx.oxml.parser import OxmlElement
+from docx.oxml.simpletypes import ST_DecimalNumber, ST_OnOff, ST_String
+from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, RequiredAttribute
+
+
+class CT_DecimalNumber(BaseOxmlElement):
+    """Used for ``<w:numId>``, ``<w:ilvl>``, ``<w:abstractNumId>`` and several others,
+    containing a text representation of a decimal number (e.g. 42) in its ``val``
+    attribute."""
+
+    val: int = RequiredAttribute("w:val", ST_DecimalNumber)  # pyright: ignore[reportAssignmentType]
+
+    @classmethod
+    def new(cls, nsptagname: str, val: int):
+        """Return a new ``CT_DecimalNumber`` element having tagname `nsptagname` and
+        ``val`` attribute set to `val`."""
+        return OxmlElement(nsptagname, attrs={qn("w:val"): str(val)})
+
+
+class CT_OnOff(BaseOxmlElement):
+    """Used for `w:b`, `w:i` elements and others.
+
+    Contains a bool-ish string in its `val` attribute, xsd:boolean plus "on" and
+    "off". Defaults to `True`, so `<w:b>` for example means "bold is turned on".
+    """
+
+    val: bool = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:val", ST_OnOff, default=True
+    )
+
+
+class CT_String(BaseOxmlElement):
+    """Used for `w:pStyle` and `w:tblStyle` elements and others.
+
+    In those cases, it containing a style name in its `val` attribute.
+    """
+
+    val: str = RequiredAttribute("w:val", ST_String)  # pyright: ignore[reportAssignmentType]
+
+    @classmethod
+    def new(cls, nsptagname: str, val: str):
+        """Return a new ``CT_String`` element with tagname `nsptagname` and ``val``
+        attribute set to `val`."""
+        elm = cast(CT_String, OxmlElement(nsptagname))
+        elm.val = val
+        return elm
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/simpletypes.py b/.venv/lib/python3.12/site-packages/docx/oxml/simpletypes.py
new file mode 100644
index 00000000..dd10ab91
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/simpletypes.py
@@ -0,0 +1,381 @@
+# pyright: reportImportCycles=false
+
+"""Simple-type classes, corresponding to ST_* schema items.
+
+These provide validation and format translation for values stored in XML element
+attributes. Naming generally corresponds to the simple type in the associated XML
+schema.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, Tuple
+
+from docx.exceptions import InvalidXmlError
+from docx.shared import Emu, Pt, RGBColor, Twips
+
+if TYPE_CHECKING:
+    from docx.shared import Length
+
+
+class BaseSimpleType:
+    """Base class for simple-types."""
+
+    @classmethod
+    def from_xml(cls, xml_value: str) -> Any:
+        return cls.convert_from_xml(xml_value)
+
+    @classmethod
+    def to_xml(cls, value: Any) -> str:
+        cls.validate(value)
+        str_value = cls.convert_to_xml(value)
+        return str_value
+
+    @classmethod
+    def convert_from_xml(cls, str_value: str) -> Any:
+        return int(str_value)
+
+    @classmethod
+    def convert_to_xml(cls, value: Any) -> str: ...
+
+    @classmethod
+    def validate(cls, value: Any) -> None: ...
+
+    @classmethod
+    def validate_int(cls, value: object):
+        if not isinstance(value, int):
+            raise TypeError("value must be <type 'int'>, got %s" % type(value))
+
+    @classmethod
+    def validate_int_in_range(cls, value: int, min_inclusive: int, max_inclusive: int) -> None:
+        cls.validate_int(value)
+        if value < min_inclusive or value > max_inclusive:
+            raise ValueError(
+                "value must be in range %d to %d inclusive, got %d"
+                % (min_inclusive, max_inclusive, value)
+            )
+
+    @classmethod
+    def validate_string(cls, value: Any) -> str:
+        if not isinstance(value, str):
+            raise TypeError("value must be a string, got %s" % type(value))
+        return value
+
+
+class BaseIntType(BaseSimpleType):
+    @classmethod
+    def convert_from_xml(cls, str_value: str) -> int:
+        return int(str_value)
+
+    @classmethod
+    def convert_to_xml(cls, value: int) -> str:
+        return str(value)
+
+    @classmethod
+    def validate(cls, value: Any) -> None:
+        cls.validate_int(value)
+
+
+class BaseStringType(BaseSimpleType):
+    @classmethod
+    def convert_from_xml(cls, str_value: str) -> str:
+        return str_value
+
+    @classmethod
+    def convert_to_xml(cls, value: str) -> str:
+        return value
+
+    @classmethod
+    def validate(cls, value: str):
+        cls.validate_string(value)
+
+
+class BaseStringEnumerationType(BaseStringType):
+    _members: Tuple[str, ...]
+
+    @classmethod
+    def validate(cls, value: Any) -> None:
+        cls.validate_string(value)
+        if value not in cls._members:
+            raise ValueError("must be one of %s, got '%s'" % (cls._members, value))
+
+
+class XsdAnyUri(BaseStringType):
+    """There's a regex in the spec this is supposed to meet...
+
+    but current assessment is that spending cycles on validating wouldn't be worth it
+    for the number of programming errors it would catch.
+    """
+
+
+class XsdBoolean(BaseSimpleType):
+    @classmethod
+    def convert_from_xml(cls, str_value: str) -> bool:
+        if str_value not in ("1", "0", "true", "false"):
+            raise InvalidXmlError(
+                "value must be one of '1', '0', 'true' or 'false', got '%s'" % str_value
+            )
+        return str_value in ("1", "true")
+
+    @classmethod
+    def convert_to_xml(cls, value: bool) -> str:
+        return {True: "1", False: "0"}[value]
+
+    @classmethod
+    def validate(cls, value: Any) -> None:
+        if value not in (True, False):
+            raise TypeError(
+                "only True or False (and possibly None) may be assigned, got" " '%s'" % value
+            )
+
+
+class XsdId(BaseStringType):
+    """String that must begin with a letter or underscore and cannot contain any colons.
+
+    Not fully validated because not used in external API.
+    """
+
+    pass
+
+
+class XsdInt(BaseIntType):
+    @classmethod
+    def validate(cls, value: Any) -> None:
+        cls.validate_int_in_range(value, -2147483648, 2147483647)
+
+
+class XsdLong(BaseIntType):
+    @classmethod
+    def validate(cls, value: Any) -> None:
+        cls.validate_int_in_range(value, -9223372036854775808, 9223372036854775807)
+
+
+class XsdString(BaseStringType):
+    pass
+
+
+class XsdStringEnumeration(BaseStringEnumerationType):
+    """Set of enumerated xsd:string values."""
+
+
+class XsdToken(BaseStringType):
+    """Xsd:string with whitespace collapsing, e.g. multiple spaces reduced to one,
+    leading and trailing space stripped."""
+
+    pass
+
+
+class XsdUnsignedInt(BaseIntType):
+    @classmethod
+    def validate(cls, value: Any) -> None:
+        cls.validate_int_in_range(value, 0, 4294967295)
+
+
+class XsdUnsignedLong(BaseIntType):
+    @classmethod
+    def validate(cls, value: Any) -> None:
+        cls.validate_int_in_range(value, 0, 18446744073709551615)
+
+
+class ST_BrClear(XsdString):
+    @classmethod
+    def validate(cls, value: str) -> None:
+        cls.validate_string(value)
+        valid_values = ("none", "left", "right", "all")
+        if value not in valid_values:
+            raise ValueError("must be one of %s, got '%s'" % (valid_values, value))
+
+
+class ST_BrType(XsdString):
+    @classmethod
+    def validate(cls, value: Any) -> None:
+        cls.validate_string(value)
+        valid_values = ("page", "column", "textWrapping")
+        if value not in valid_values:
+            raise ValueError("must be one of %s, got '%s'" % (valid_values, value))
+
+
+class ST_Coordinate(BaseIntType):
+    @classmethod
+    def convert_from_xml(cls, str_value: str) -> Length:
+        if "i" in str_value or "m" in str_value or "p" in str_value:
+            return ST_UniversalMeasure.convert_from_xml(str_value)
+        return Emu(int(str_value))
+
+    @classmethod
+    def validate(cls, value: Any) -> None:
+        ST_CoordinateUnqualified.validate(value)
+
+
+class ST_CoordinateUnqualified(XsdLong):
+    @classmethod
+    def validate(cls, value: Any) -> None:
+        cls.validate_int_in_range(value, -27273042329600, 27273042316900)
+
+
+class ST_DecimalNumber(XsdInt):
+    pass
+
+
+class ST_DrawingElementId(XsdUnsignedInt):
+    pass
+
+
+class ST_HexColor(BaseStringType):
+    @classmethod
+    def convert_from_xml(  # pyright: ignore[reportIncompatibleMethodOverride]
+        cls, str_value: str
+    ) -> RGBColor | str:
+        if str_value == "auto":
+            return ST_HexColorAuto.AUTO
+        return RGBColor.from_string(str_value)
+
+    @classmethod
+    def convert_to_xml(  # pyright: ignore[reportIncompatibleMethodOverride]
+        cls, value: RGBColor
+    ) -> str:
+        """Keep alpha hex numerals all uppercase just for consistency."""
+        # expecting 3-tuple of ints in range 0-255
+        return "%02X%02X%02X" % value
+
+    @classmethod
+    def validate(cls, value: Any) -> None:
+        # must be an RGBColor object ---
+        if not isinstance(value, RGBColor):
+            raise ValueError(
+                "rgb color value must be RGBColor object, got %s %s" % (type(value), value)
+            )
+
+
+class ST_HexColorAuto(XsdStringEnumeration):
+    """Value for `w:color/[@val="auto"] attribute setting."""
+
+    AUTO = "auto"
+
+    _members = (AUTO,)
+
+
+class ST_HpsMeasure(XsdUnsignedLong):
+    """Half-point measure, e.g. 24.0 represents 12.0 points."""
+
+    @classmethod
+    def convert_from_xml(cls, str_value: str) -> Length:
+        if "m" in str_value or "n" in str_value or "p" in str_value:
+            return ST_UniversalMeasure.convert_from_xml(str_value)
+        return Pt(int(str_value) / 2.0)
+
+    @classmethod
+    def convert_to_xml(cls, value: int | Length) -> str:
+        emu = Emu(value)
+        half_points = int(emu.pt * 2)
+        return str(half_points)
+
+
+class ST_Merge(XsdStringEnumeration):
+    """Valid values for <w:xMerge val=""> attribute."""
+
+    CONTINUE = "continue"
+    RESTART = "restart"
+
+    _members = (CONTINUE, RESTART)
+
+
+class ST_OnOff(XsdBoolean):
+    @classmethod
+    def convert_from_xml(cls, str_value: str) -> bool:
+        if str_value not in ("1", "0", "true", "false", "on", "off"):
+            raise InvalidXmlError(
+                "value must be one of '1', '0', 'true', 'false', 'on', or 'o"
+                "ff', got '%s'" % str_value
+            )
+        return str_value in ("1", "true", "on")
+
+
+class ST_PositiveCoordinate(XsdLong):
+    @classmethod
+    def convert_from_xml(cls, str_value: str) -> Length:
+        return Emu(int(str_value))
+
+    @classmethod
+    def validate(cls, value: Any) -> None:
+        cls.validate_int_in_range(value, 0, 27273042316900)
+
+
+class ST_RelationshipId(XsdString):
+    pass
+
+
+class ST_SignedTwipsMeasure(XsdInt):
+    @classmethod
+    def convert_from_xml(cls, str_value: str) -> Length:
+        if "i" in str_value or "m" in str_value or "p" in str_value:
+            return ST_UniversalMeasure.convert_from_xml(str_value)
+        return Twips(int(round(float(str_value))))
+
+    @classmethod
+    def convert_to_xml(cls, value: int | Length) -> str:
+        emu = Emu(value)
+        twips = emu.twips
+        return str(twips)
+
+
+class ST_String(XsdString):
+    pass
+
+
+class ST_TblLayoutType(XsdString):
+    @classmethod
+    def validate(cls, value: Any) -> None:
+        cls.validate_string(value)
+        valid_values = ("fixed", "autofit")
+        if value not in valid_values:
+            raise ValueError("must be one of %s, got '%s'" % (valid_values, value))
+
+
+class ST_TblWidth(XsdString):
+    @classmethod
+    def validate(cls, value: Any) -> None:
+        cls.validate_string(value)
+        valid_values = ("auto", "dxa", "nil", "pct")
+        if value not in valid_values:
+            raise ValueError("must be one of %s, got '%s'" % (valid_values, value))
+
+
+class ST_TwipsMeasure(XsdUnsignedLong):
+    @classmethod
+    def convert_from_xml(cls, str_value: str) -> Length:
+        if "i" in str_value or "m" in str_value or "p" in str_value:
+            return ST_UniversalMeasure.convert_from_xml(str_value)
+        return Twips(int(str_value))
+
+    @classmethod
+    def convert_to_xml(cls, value: int | Length) -> str:
+        emu = Emu(value)
+        twips = emu.twips
+        return str(twips)
+
+
+class ST_UniversalMeasure(BaseSimpleType):
+    @classmethod
+    def convert_from_xml(cls, str_value: str) -> Emu:
+        float_part, units_part = str_value[:-2], str_value[-2:]
+        quantity = float(float_part)
+        multiplier = {
+            "mm": 36000,
+            "cm": 360000,
+            "in": 914400,
+            "pt": 12700,
+            "pc": 152400,
+            "pi": 152400,
+        }[units_part]
+        return Emu(int(round(quantity * multiplier)))
+
+
+class ST_VerticalAlignRun(XsdStringEnumeration):
+    """Valid values for `w:vertAlign/@val`."""
+
+    BASELINE = "baseline"
+    SUPERSCRIPT = "superscript"
+    SUBSCRIPT = "subscript"
+
+    _members = (BASELINE, SUPERSCRIPT, SUBSCRIPT)
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/styles.py b/.venv/lib/python3.12/site-packages/docx/oxml/styles.py
new file mode 100644
index 00000000..fb0e5d0d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/styles.py
@@ -0,0 +1,320 @@
+"""Custom element classes related to the styles part."""
+
+from __future__ import annotations
+
+from docx.enum.style import WD_STYLE_TYPE
+from docx.oxml.simpletypes import ST_DecimalNumber, ST_OnOff, ST_String
+from docx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrMore,
+    ZeroOrOne,
+)
+
+
+def styleId_from_name(name):
+    """Return the style id corresponding to `name`, taking into account special-case
+    names such as 'Heading 1'."""
+    return {
+        "caption": "Caption",
+        "heading 1": "Heading1",
+        "heading 2": "Heading2",
+        "heading 3": "Heading3",
+        "heading 4": "Heading4",
+        "heading 5": "Heading5",
+        "heading 6": "Heading6",
+        "heading 7": "Heading7",
+        "heading 8": "Heading8",
+        "heading 9": "Heading9",
+    }.get(name, name.replace(" ", ""))
+
+
+class CT_LatentStyles(BaseOxmlElement):
+    """`w:latentStyles` element, defining behavior defaults for latent styles and
+    containing `w:lsdException` child elements that each override those defaults for a
+    named latent style."""
+
+    lsdException = ZeroOrMore("w:lsdException", successors=())
+
+    count = OptionalAttribute("w:count", ST_DecimalNumber)
+    defLockedState = OptionalAttribute("w:defLockedState", ST_OnOff)
+    defQFormat = OptionalAttribute("w:defQFormat", ST_OnOff)
+    defSemiHidden = OptionalAttribute("w:defSemiHidden", ST_OnOff)
+    defUIPriority = OptionalAttribute("w:defUIPriority", ST_DecimalNumber)
+    defUnhideWhenUsed = OptionalAttribute("w:defUnhideWhenUsed", ST_OnOff)
+
+    def bool_prop(self, attr_name):
+        """Return the boolean value of the attribute having `attr_name`, or |False| if
+        not present."""
+        value = getattr(self, attr_name)
+        if value is None:
+            return False
+        return value
+
+    def get_by_name(self, name):
+        """Return the `w:lsdException` child having `name`, or |None| if not found."""
+        found = self.xpath('w:lsdException[@w:name="%s"]' % name)
+        if not found:
+            return None
+        return found[0]
+
+    def set_bool_prop(self, attr_name, value):
+        """Set the on/off attribute having `attr_name` to `value`."""
+        setattr(self, attr_name, bool(value))
+
+
+class CT_LsdException(BaseOxmlElement):
+    """``<w:lsdException>`` element, defining override visibility behaviors for a named
+    latent style."""
+
+    locked = OptionalAttribute("w:locked", ST_OnOff)
+    name = RequiredAttribute("w:name", ST_String)
+    qFormat = OptionalAttribute("w:qFormat", ST_OnOff)
+    semiHidden = OptionalAttribute("w:semiHidden", ST_OnOff)
+    uiPriority = OptionalAttribute("w:uiPriority", ST_DecimalNumber)
+    unhideWhenUsed = OptionalAttribute("w:unhideWhenUsed", ST_OnOff)
+
+    def delete(self):
+        """Remove this `w:lsdException` element from the XML document."""
+        self.getparent().remove(self)
+
+    def on_off_prop(self, attr_name):
+        """Return the boolean value of the attribute having `attr_name`, or |None| if
+        not present."""
+        return getattr(self, attr_name)
+
+    def set_on_off_prop(self, attr_name, value):
+        """Set the on/off attribute having `attr_name` to `value`."""
+        setattr(self, attr_name, value)
+
+
+class CT_Style(BaseOxmlElement):
+    """A ``<w:style>`` element, representing a style definition."""
+
+    _tag_seq = (
+        "w:name",
+        "w:aliases",
+        "w:basedOn",
+        "w:next",
+        "w:link",
+        "w:autoRedefine",
+        "w:hidden",
+        "w:uiPriority",
+        "w:semiHidden",
+        "w:unhideWhenUsed",
+        "w:qFormat",
+        "w:locked",
+        "w:personal",
+        "w:personalCompose",
+        "w:personalReply",
+        "w:rsid",
+        "w:pPr",
+        "w:rPr",
+        "w:tblPr",
+        "w:trPr",
+        "w:tcPr",
+        "w:tblStylePr",
+    )
+    name = ZeroOrOne("w:name", successors=_tag_seq[1:])
+    basedOn = ZeroOrOne("w:basedOn", successors=_tag_seq[3:])
+    next = ZeroOrOne("w:next", successors=_tag_seq[4:])
+    uiPriority = ZeroOrOne("w:uiPriority", successors=_tag_seq[8:])
+    semiHidden = ZeroOrOne("w:semiHidden", successors=_tag_seq[9:])
+    unhideWhenUsed = ZeroOrOne("w:unhideWhenUsed", successors=_tag_seq[10:])
+    qFormat = ZeroOrOne("w:qFormat", successors=_tag_seq[11:])
+    locked = ZeroOrOne("w:locked", successors=_tag_seq[12:])
+    pPr = ZeroOrOne("w:pPr", successors=_tag_seq[17:])
+    rPr = ZeroOrOne("w:rPr", successors=_tag_seq[18:])
+    del _tag_seq
+
+    type: WD_STYLE_TYPE | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:type", WD_STYLE_TYPE
+    )
+    styleId: str | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:styleId", ST_String
+    )
+    default = OptionalAttribute("w:default", ST_OnOff)
+    customStyle = OptionalAttribute("w:customStyle", ST_OnOff)
+
+    @property
+    def basedOn_val(self):
+        """Value of `w:basedOn/@w:val` or |None| if not present."""
+        basedOn = self.basedOn
+        if basedOn is None:
+            return None
+        return basedOn.val
+
+    @basedOn_val.setter
+    def basedOn_val(self, value):
+        if value is None:
+            self._remove_basedOn()
+        else:
+            self.get_or_add_basedOn().val = value
+
+    @property
+    def base_style(self):
+        """Sibling CT_Style element this style is based on or |None| if no base style or
+        base style not found."""
+        basedOn = self.basedOn
+        if basedOn is None:
+            return None
+        styles = self.getparent()
+        base_style = styles.get_by_id(basedOn.val)
+        if base_style is None:
+            return None
+        return base_style
+
+    def delete(self):
+        """Remove this `w:style` element from its parent `w:styles` element."""
+        self.getparent().remove(self)
+
+    @property
+    def locked_val(self):
+        """Value of `w:locked/@w:val` or |False| if not present."""
+        locked = self.locked
+        if locked is None:
+            return False
+        return locked.val
+
+    @locked_val.setter
+    def locked_val(self, value):
+        self._remove_locked()
+        if bool(value) is True:
+            locked = self._add_locked()
+            locked.val = value
+
+    @property
+    def name_val(self):
+        """Value of ``<w:name>`` child or |None| if not present."""
+        name = self.name
+        if name is None:
+            return None
+        return name.val
+
+    @name_val.setter
+    def name_val(self, value):
+        self._remove_name()
+        if value is not None:
+            name = self._add_name()
+            name.val = value
+
+    @property
+    def next_style(self):
+        """Sibling CT_Style element identified by the value of `w:name/@w:val` or |None|
+        if no value is present or no style with that style id is found."""
+        next = self.next
+        if next is None:
+            return None
+        styles = self.getparent()
+        return styles.get_by_id(next.val)  # None if not found
+
+    @property
+    def qFormat_val(self):
+        """Value of `w:qFormat/@w:val` or |False| if not present."""
+        qFormat = self.qFormat
+        if qFormat is None:
+            return False
+        return qFormat.val
+
+    @qFormat_val.setter
+    def qFormat_val(self, value):
+        self._remove_qFormat()
+        if bool(value):
+            self._add_qFormat()
+
+    @property
+    def semiHidden_val(self):
+        """Value of ``<w:semiHidden>`` child or |False| if not present."""
+        semiHidden = self.semiHidden
+        if semiHidden is None:
+            return False
+        return semiHidden.val
+
+    @semiHidden_val.setter
+    def semiHidden_val(self, value):
+        self._remove_semiHidden()
+        if bool(value) is True:
+            semiHidden = self._add_semiHidden()
+            semiHidden.val = value
+
+    @property
+    def uiPriority_val(self):
+        """Value of ``<w:uiPriority>`` child or |None| if not present."""
+        uiPriority = self.uiPriority
+        if uiPriority is None:
+            return None
+        return uiPriority.val
+
+    @uiPriority_val.setter
+    def uiPriority_val(self, value):
+        self._remove_uiPriority()
+        if value is not None:
+            uiPriority = self._add_uiPriority()
+            uiPriority.val = value
+
+    @property
+    def unhideWhenUsed_val(self):
+        """Value of `w:unhideWhenUsed/@w:val` or |False| if not present."""
+        unhideWhenUsed = self.unhideWhenUsed
+        if unhideWhenUsed is None:
+            return False
+        return unhideWhenUsed.val
+
+    @unhideWhenUsed_val.setter
+    def unhideWhenUsed_val(self, value):
+        self._remove_unhideWhenUsed()
+        if bool(value) is True:
+            unhideWhenUsed = self._add_unhideWhenUsed()
+            unhideWhenUsed.val = value
+
+
+class CT_Styles(BaseOxmlElement):
+    """``<w:styles>`` element, the root element of a styles part, i.e. styles.xml."""
+
+    _tag_seq = ("w:docDefaults", "w:latentStyles", "w:style")
+    latentStyles = ZeroOrOne("w:latentStyles", successors=_tag_seq[2:])
+    style = ZeroOrMore("w:style", successors=())
+    del _tag_seq
+
+    def add_style_of_type(self, name, style_type, builtin):
+        """Return a newly added `w:style` element having `name` and `style_type`.
+
+        `w:style/@customStyle` is set based on the value of `builtin`.
+        """
+        style = self.add_style()
+        style.type = style_type
+        style.customStyle = None if builtin else True
+        style.styleId = styleId_from_name(name)
+        style.name_val = name
+        return style
+
+    def default_for(self, style_type):
+        """Return `w:style[@w:type="*{style_type}*][-1]` or |None| if not found."""
+        default_styles_for_type = [
+            s for s in self._iter_styles() if s.type == style_type and s.default
+        ]
+        if not default_styles_for_type:
+            return None
+        # spec calls for last default in document order
+        return default_styles_for_type[-1]
+
+    def get_by_id(self, styleId: str) -> CT_Style | None:
+        """`w:style` child where @styleId = `styleId`.
+
+        |None| if not found.
+        """
+        xpath = f'w:style[@w:styleId="{styleId}"]'
+        return next(iter(self.xpath(xpath)), None)
+
+    def get_by_name(self, name: str) -> CT_Style | None:
+        """`w:style` child with `w:name` grandchild having value `name`.
+
+        |None| if not found.
+        """
+        xpath = 'w:style[w:name/@w:val="%s"]' % name
+        return next(iter(self.xpath(xpath)), None)
+
+    def _iter_styles(self):
+        """Generate each of the `w:style` child elements in document order."""
+        return (style for style in self.xpath("w:style"))
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/table.py b/.venv/lib/python3.12/site-packages/docx/oxml/table.py
new file mode 100644
index 00000000..e38d5856
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/table.py
@@ -0,0 +1,977 @@
+"""Custom element classes for tables."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, cast
+
+from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_ROW_HEIGHT_RULE, WD_TABLE_DIRECTION
+from docx.exceptions import InvalidSpanError
+from docx.oxml.ns import nsdecls, qn
+from docx.oxml.parser import parse_xml
+from docx.oxml.shared import CT_DecimalNumber
+from docx.oxml.simpletypes import (
+    ST_Merge,
+    ST_TblLayoutType,
+    ST_TblWidth,
+    ST_TwipsMeasure,
+    XsdInt,
+)
+from docx.oxml.text.paragraph import CT_P
+from docx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OneAndOnlyOne,
+    OneOrMore,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrMore,
+    ZeroOrOne,
+)
+from docx.shared import Emu, Length, Twips
+
+if TYPE_CHECKING:
+    from docx.enum.table import WD_TABLE_ALIGNMENT
+    from docx.enum.text import WD_ALIGN_PARAGRAPH
+    from docx.oxml.shared import CT_OnOff, CT_String
+    from docx.oxml.text.parfmt import CT_Jc
+
+
+class CT_Height(BaseOxmlElement):
+    """Used for `w:trHeight` to specify a row height and row height rule."""
+
+    val: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:val", ST_TwipsMeasure
+    )
+    hRule: WD_ROW_HEIGHT_RULE | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:hRule", WD_ROW_HEIGHT_RULE
+    )
+
+
+class CT_Row(BaseOxmlElement):
+    """``<w:tr>`` element."""
+
+    add_tc: Callable[[], CT_Tc]
+    get_or_add_trPr: Callable[[], CT_TrPr]
+    _add_trPr: Callable[[], CT_TrPr]
+
+    tc_lst: list[CT_Tc]
+    # -- custom inserter below --
+    tblPrEx: CT_TblPrEx | None = ZeroOrOne("w:tblPrEx")  # pyright: ignore[reportAssignmentType]
+    # -- custom inserter below --
+    trPr: CT_TrPr | None = ZeroOrOne("w:trPr")  # pyright: ignore[reportAssignmentType]
+    tc = ZeroOrMore("w:tc")
+
+    @property
+    def grid_after(self) -> int:
+        """The number of unpopulated layout-grid cells at the end of this row."""
+        trPr = self.trPr
+        if trPr is None:
+            return 0
+        return trPr.grid_after
+
+    @property
+    def grid_before(self) -> int:
+        """The number of unpopulated layout-grid cells at the start of this row."""
+        trPr = self.trPr
+        if trPr is None:
+            return 0
+        return trPr.grid_before
+
+    def tc_at_grid_offset(self, grid_offset: int) -> CT_Tc:
+        """The `tc` element in this tr at exact `grid offset`.
+
+        Raises ValueError when this `w:tr` contains no `w:tc` with exact starting `grid_offset`.
+        """
+        # -- account for omitted cells at the start of the row --
+        remaining_offset = grid_offset - self.grid_before
+
+        for tc in self.tc_lst:
+            # -- We've gone past grid_offset without finding a tc, no sense searching further. --
+            if remaining_offset < 0:
+                break
+            # -- We've arrived at grid_offset, this is the `w:tc` we're looking for. --
+            if remaining_offset == 0:
+                return tc
+            # -- We're not there yet, skip forward the number of layout-grid cells this cell
+            # -- occupies.
+            remaining_offset -= tc.grid_span
+
+        raise ValueError(f"no `tc` element at grid_offset={grid_offset}")
+
+    @property
+    def tr_idx(self) -> int:
+        """Index of this `w:tr` element within its parent `w:tbl` element."""
+        tbl = cast(CT_Tbl, self.getparent())
+        return tbl.tr_lst.index(self)
+
+    @property
+    def trHeight_hRule(self) -> WD_ROW_HEIGHT_RULE | None:
+        """The value of `./w:trPr/w:trHeight/@w:hRule`, or |None| if not present."""
+        trPr = self.trPr
+        if trPr is None:
+            return None
+        return trPr.trHeight_hRule
+
+    @trHeight_hRule.setter
+    def trHeight_hRule(self, value: WD_ROW_HEIGHT_RULE | None):
+        trPr = self.get_or_add_trPr()
+        trPr.trHeight_hRule = value
+
+    @property
+    def trHeight_val(self):
+        """Return the value of `w:trPr/w:trHeight@w:val`, or |None| if not present."""
+        trPr = self.trPr
+        if trPr is None:
+            return None
+        return trPr.trHeight_val
+
+    @trHeight_val.setter
+    def trHeight_val(self, value: Length | None):
+        trPr = self.get_or_add_trPr()
+        trPr.trHeight_val = value
+
+    def _insert_tblPrEx(self, tblPrEx: CT_TblPrEx):
+        self.insert(0, tblPrEx)
+
+    def _insert_trPr(self, trPr: CT_TrPr):
+        tblPrEx = self.tblPrEx
+        if tblPrEx is not None:
+            tblPrEx.addnext(trPr)
+        else:
+            self.insert(0, trPr)
+
+    def _new_tc(self):
+        return CT_Tc.new()
+
+
+class CT_Tbl(BaseOxmlElement):
+    """``<w:tbl>`` element."""
+
+    add_tr: Callable[[], CT_Row]
+    tr_lst: list[CT_Row]
+
+    tblPr: CT_TblPr = OneAndOnlyOne("w:tblPr")  # pyright: ignore[reportAssignmentType]
+    tblGrid: CT_TblGrid = OneAndOnlyOne("w:tblGrid")  # pyright: ignore[reportAssignmentType]
+    tr = ZeroOrMore("w:tr")
+
+    @property
+    def bidiVisual_val(self) -> bool | None:
+        """Value of `./w:tblPr/w:bidiVisual/@w:val` or |None| if not present.
+
+        Controls whether table cells are displayed right-to-left or left-to-right.
+        """
+        bidiVisual = self.tblPr.bidiVisual
+        if bidiVisual is None:
+            return None
+        return bidiVisual.val
+
+    @bidiVisual_val.setter
+    def bidiVisual_val(self, value: WD_TABLE_DIRECTION | None):
+        tblPr = self.tblPr
+        if value is None:
+            tblPr._remove_bidiVisual()  # pyright: ignore[reportPrivateUsage]
+        else:
+            tblPr.get_or_add_bidiVisual().val = bool(value)
+
+    @property
+    def col_count(self):
+        """The number of grid columns in this table."""
+        return len(self.tblGrid.gridCol_lst)
+
+    def iter_tcs(self):
+        """Generate each of the `w:tc` elements in this table, left to right and top to
+        bottom.
+
+        Each cell in the first row is generated, followed by each cell in the second
+        row, etc.
+        """
+        for tr in self.tr_lst:
+            for tc in tr.tc_lst:
+                yield tc
+
+    @classmethod
+    def new_tbl(cls, rows: int, cols: int, width: Length) -> CT_Tbl:
+        """Return a new `w:tbl` element having `rows` rows and `cols` columns.
+
+        `width` is distributed evenly between the columns.
+        """
+        return cast(CT_Tbl, parse_xml(cls._tbl_xml(rows, cols, width)))
+
+    @property
+    def tblStyle_val(self) -> str | None:
+        """`w:tblPr/w:tblStyle/@w:val` (a table style id) or |None| if not present."""
+        tblStyle = self.tblPr.tblStyle
+        if tblStyle is None:
+            return None
+        return tblStyle.val
+
+    @tblStyle_val.setter
+    def tblStyle_val(self, styleId: str | None) -> None:
+        """Set the value of `w:tblPr/w:tblStyle/@w:val` (a table style id) to `styleId`.
+
+        If `styleId` is None, remove the `w:tblStyle` element.
+        """
+        tblPr = self.tblPr
+        tblPr._remove_tblStyle()  # pyright: ignore[reportPrivateUsage]
+        if styleId is None:
+            return
+        tblPr._add_tblStyle().val = styleId  # pyright: ignore[reportPrivateUsage]
+
+    @classmethod
+    def _tbl_xml(cls, rows: int, cols: int, width: Length) -> str:
+        col_width = Emu(width // cols) if cols > 0 else Emu(0)
+        return (
+            f"<w:tbl {nsdecls('w')}>\n"
+            f"  <w:tblPr>\n"
+            f'    <w:tblW w:type="auto" w:w="0"/>\n'
+            f'    <w:tblLook w:firstColumn="1" w:firstRow="1"\n'
+            f'               w:lastColumn="0" w:lastRow="0" w:noHBand="0"\n'
+            f'               w:noVBand="1" w:val="04A0"/>\n'
+            f"  </w:tblPr>\n"
+            f"{cls._tblGrid_xml(cols, col_width)}"
+            f"{cls._trs_xml(rows, cols, col_width)}"
+            f"</w:tbl>\n"
+        )
+
+    @classmethod
+    def _tblGrid_xml(cls, col_count: int, col_width: Length) -> str:
+        xml = "  <w:tblGrid>\n"
+        for _ in range(col_count):
+            xml += '    <w:gridCol w:w="%d"/>\n' % col_width.twips
+        xml += "  </w:tblGrid>\n"
+        return xml
+
+    @classmethod
+    def _trs_xml(cls, row_count: int, col_count: int, col_width: Length) -> str:
+        return f"  <w:tr>\n{cls._tcs_xml(col_count, col_width)}  </w:tr>\n" * row_count
+
+    @classmethod
+    def _tcs_xml(cls, col_count: int, col_width: Length) -> str:
+        return (
+            f"    <w:tc>\n"
+            f"      <w:tcPr>\n"
+            f'        <w:tcW w:type="dxa" w:w="{col_width.twips}"/>\n'
+            f"      </w:tcPr>\n"
+            f"      <w:p/>\n"
+            f"    </w:tc>\n"
+        ) * col_count
+
+
+class CT_TblGrid(BaseOxmlElement):
+    """`w:tblGrid` element.
+
+    Child of `w:tbl`, holds `w:gridCol> elements that define column count, width, etc.
+    """
+
+    add_gridCol: Callable[[], CT_TblGridCol]
+    gridCol_lst: list[CT_TblGridCol]
+
+    gridCol = ZeroOrMore("w:gridCol", successors=("w:tblGridChange",))
+
+
+class CT_TblGridCol(BaseOxmlElement):
+    """`w:gridCol` element, child of `w:tblGrid`, defines a table column."""
+
+    w: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:w", ST_TwipsMeasure
+    )
+
+    @property
+    def gridCol_idx(self) -> int:
+        """Index of this `w:gridCol` element within its parent `w:tblGrid` element."""
+        tblGrid = cast(CT_TblGrid, self.getparent())
+        return tblGrid.gridCol_lst.index(self)
+
+
+class CT_TblLayoutType(BaseOxmlElement):
+    """`w:tblLayout` element.
+
+    Specifies whether column widths are fixed or can be automatically adjusted based on
+    content.
+    """
+
+    type: str | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:type", ST_TblLayoutType
+    )
+
+
+class CT_TblPr(BaseOxmlElement):
+    """``<w:tblPr>`` element, child of ``<w:tbl>``, holds child elements that define
+    table properties such as style and borders."""
+
+    get_or_add_bidiVisual: Callable[[], CT_OnOff]
+    get_or_add_jc: Callable[[], CT_Jc]
+    get_or_add_tblLayout: Callable[[], CT_TblLayoutType]
+    _add_tblStyle: Callable[[], CT_String]
+    _remove_bidiVisual: Callable[[], None]
+    _remove_jc: Callable[[], None]
+    _remove_tblStyle: Callable[[], None]
+
+    _tag_seq = (
+        "w:tblStyle",
+        "w:tblpPr",
+        "w:tblOverlap",
+        "w:bidiVisual",
+        "w:tblStyleRowBandSize",
+        "w:tblStyleColBandSize",
+        "w:tblW",
+        "w:jc",
+        "w:tblCellSpacing",
+        "w:tblInd",
+        "w:tblBorders",
+        "w:shd",
+        "w:tblLayout",
+        "w:tblCellMar",
+        "w:tblLook",
+        "w:tblCaption",
+        "w:tblDescription",
+        "w:tblPrChange",
+    )
+    tblStyle: CT_String | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:tblStyle", successors=_tag_seq[1:]
+    )
+    bidiVisual: CT_OnOff | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:bidiVisual", successors=_tag_seq[4:]
+    )
+    jc: CT_Jc | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:jc", successors=_tag_seq[8:]
+    )
+    tblLayout: CT_TblLayoutType | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:tblLayout", successors=_tag_seq[13:]
+    )
+    del _tag_seq
+
+    @property
+    def alignment(self) -> WD_TABLE_ALIGNMENT | None:
+        """Horizontal alignment of table, |None| if `./w:jc` is not present."""
+        jc = self.jc
+        if jc is None:
+            return None
+        return cast("WD_TABLE_ALIGNMENT | None", jc.val)
+
+    @alignment.setter
+    def alignment(self, value: WD_TABLE_ALIGNMENT | None):
+        self._remove_jc()
+        if value is None:
+            return
+        jc = self.get_or_add_jc()
+        jc.val = cast("WD_ALIGN_PARAGRAPH", value)
+
+    @property
+    def autofit(self) -> bool:
+        """|False| when there is a `w:tblLayout` child with `@w:type="fixed"`.
+
+        Otherwise |True|.
+        """
+        tblLayout = self.tblLayout
+        return True if tblLayout is None else tblLayout.type != "fixed"
+
+    @autofit.setter
+    def autofit(self, value: bool):
+        tblLayout = self.get_or_add_tblLayout()
+        tblLayout.type = "autofit" if value else "fixed"
+
+    @property
+    def style(self):
+        """Return the value of the ``val`` attribute of the ``<w:tblStyle>`` child or
+        |None| if not present."""
+        tblStyle = self.tblStyle
+        if tblStyle is None:
+            return None
+        return tblStyle.val
+
+    @style.setter
+    def style(self, value: str | None):
+        self._remove_tblStyle()
+        if value is None:
+            return
+        self._add_tblStyle().val = value
+
+
+class CT_TblPrEx(BaseOxmlElement):
+    """`w:tblPrEx` element, exceptions to table-properties.
+
+    Applied at a lower level, like a `w:tr` to modify the appearance. Possibly used when
+    two tables are merged. For more see:
+    http://officeopenxml.com/WPtablePropertyExceptions.php
+    """
+
+
+class CT_TblWidth(BaseOxmlElement):
+    """Used for `w:tblW` and `w:tcW` and others, specifies a table-related width."""
+
+    # the type for `w` attr is actually ST_MeasurementOrPercent, but using
+    # XsdInt for now because only dxa (twips) values are being used. It's not
+    # entirely clear what the semantics are for other values like -01.4mm
+    w: int = RequiredAttribute("w:w", XsdInt)  # pyright: ignore[reportAssignmentType]
+    type = RequiredAttribute("w:type", ST_TblWidth)
+
+    @property
+    def width(self) -> Length | None:
+        """EMU length indicated by the combined `w:w` and `w:type` attrs."""
+        if self.type != "dxa":
+            return None
+        return Twips(self.w)
+
+    @width.setter
+    def width(self, value: Length):
+        self.type = "dxa"
+        self.w = Emu(value).twips
+
+
+class CT_Tc(BaseOxmlElement):
+    """`w:tc` table cell element."""
+
+    add_p: Callable[[], CT_P]
+    get_or_add_tcPr: Callable[[], CT_TcPr]
+    p_lst: list[CT_P]
+    tbl_lst: list[CT_Tbl]
+    _insert_tbl: Callable[[CT_Tbl], CT_Tbl]
+    _new_p: Callable[[], CT_P]
+
+    # -- tcPr has many successors, `._insert_tcPr()` is overridden below --
+    tcPr: CT_TcPr | None = ZeroOrOne("w:tcPr")  # pyright: ignore[reportAssignmentType]
+    p = OneOrMore("w:p")
+    tbl = OneOrMore("w:tbl")
+
+    @property
+    def bottom(self) -> int:
+        """The row index that marks the bottom extent of the vertical span of this cell.
+
+        This is one greater than the index of the bottom-most row of the span, similar
+        to how a slice of the cell's rows would be specified.
+        """
+        if self.vMerge is not None:
+            tc_below = self._tc_below
+            if tc_below is not None and tc_below.vMerge == ST_Merge.CONTINUE:
+                return tc_below.bottom
+        return self._tr_idx + 1
+
+    def clear_content(self):
+        """Remove all content elements, preserving `w:tcPr` element if present.
+
+        Note that this leaves the `w:tc` element in an invalid state because it doesn't
+        contain at least one block-level element. It's up to the caller to add a
+        `w:p`child element as the last content element.
+        """
+        # -- remove all cell inner-content except a `w:tcPr` when present. --
+        for e in self.xpath("./*[not(self::w:tcPr)]"):
+            self.remove(e)
+
+    @property
+    def grid_offset(self) -> int:
+        """Starting offset of `tc` in the layout-grid columns of its table.
+
+        A cell in the leftmost grid-column has offset 0.
+        """
+        grid_before = self._tr.grid_before
+        preceding_tc_grid_spans = sum(
+            tc.grid_span for tc in self.xpath("./preceding-sibling::w:tc")
+        )
+        return grid_before + preceding_tc_grid_spans
+
+    @property
+    def grid_span(self) -> int:
+        """The integer number of columns this cell spans.
+
+        Determined by ./w:tcPr/w:gridSpan/@val, it defaults to 1.
+        """
+        tcPr = self.tcPr
+        return 1 if tcPr is None else tcPr.grid_span
+
+    @grid_span.setter
+    def grid_span(self, value: int):
+        tcPr = self.get_or_add_tcPr()
+        tcPr.grid_span = value
+
+    @property
+    def inner_content_elements(self) -> list[CT_P | CT_Tbl]:
+        """Generate all `w:p` and `w:tbl` elements in this document-body.
+
+        Elements appear in document order. Elements shaded by nesting in a `w:ins` or
+        other "wrapper" element will not be included.
+        """
+        return self.xpath("./w:p | ./w:tbl")
+
+    def iter_block_items(self):
+        """Generate a reference to each of the block-level content elements in this
+        cell, in the order they appear."""
+        block_item_tags = (qn("w:p"), qn("w:tbl"), qn("w:sdt"))
+        for child in self:
+            if child.tag in block_item_tags:
+                yield child
+
+    @property
+    def left(self) -> int:
+        """The grid column index at which this ``<w:tc>`` element appears."""
+        return self.grid_offset
+
+    def merge(self, other_tc: CT_Tc) -> CT_Tc:
+        """Return top-left `w:tc` element of a new span.
+
+        Span is formed by merging the rectangular region defined by using this tc
+        element and `other_tc` as diagonal corners.
+        """
+        top, left, height, width = self._span_dimensions(other_tc)
+        top_tc = self._tbl.tr_lst[top].tc_at_grid_offset(left)
+        top_tc._grow_to(width, height)
+        return top_tc
+
+    @classmethod
+    def new(cls) -> CT_Tc:
+        """A new `w:tc` element, containing an empty paragraph as the required EG_BlockLevelElt."""
+        return cast(CT_Tc, parse_xml("<w:tc %s>\n" "  <w:p/>\n" "</w:tc>" % nsdecls("w")))
+
+    @property
+    def right(self) -> int:
+        """The grid column index that marks the right-side extent of the horizontal span
+        of this cell.
+
+        This is one greater than the index of the right-most column of the span, similar
+        to how a slice of the cell's columns would be specified.
+        """
+        return self.grid_offset + self.grid_span
+
+    @property
+    def top(self) -> int:
+        """The top-most row index in the vertical span of this cell."""
+        if self.vMerge is None or self.vMerge == ST_Merge.RESTART:
+            return self._tr_idx
+        return self._tc_above.top
+
+    @property
+    def vMerge(self) -> str | None:
+        """Value of ./w:tcPr/w:vMerge/@val, |None| if w:vMerge is not present."""
+        tcPr = self.tcPr
+        if tcPr is None:
+            return None
+        return tcPr.vMerge_val
+
+    @vMerge.setter
+    def vMerge(self, value: str | None):
+        tcPr = self.get_or_add_tcPr()
+        tcPr.vMerge_val = value
+
+    @property
+    def width(self) -> Length | None:
+        """EMU length represented in `./w:tcPr/w:tcW` or |None| if not present."""
+        tcPr = self.tcPr
+        if tcPr is None:
+            return None
+        return tcPr.width
+
+    @width.setter
+    def width(self, value: Length):
+        tcPr = self.get_or_add_tcPr()
+        tcPr.width = value
+
+    def _add_width_of(self, other_tc: CT_Tc):
+        """Add the width of `other_tc` to this cell.
+
+        Does nothing if either this tc or `other_tc` does not have a specified width.
+        """
+        if self.width and other_tc.width:
+            self.width = Length(self.width + other_tc.width)
+
+    def _grow_to(self, width: int, height: int, top_tc: CT_Tc | None = None):
+        """Grow this cell to `width` grid columns and `height` rows.
+
+        This is accomplished by expanding horizontal spans and creating continuation
+        cells to form vertical spans.
+        """
+
+        def vMerge_val(top_tc: CT_Tc):
+            return (
+                ST_Merge.CONTINUE
+                if top_tc is not self
+                else None if height == 1 else ST_Merge.RESTART
+            )
+
+        top_tc = self if top_tc is None else top_tc
+        self._span_to_width(width, top_tc, vMerge_val(top_tc))
+        if height > 1:
+            tc_below = self._tc_below
+            assert tc_below is not None
+            tc_below._grow_to(width, height - 1, top_tc)
+
+    def _insert_tcPr(self, tcPr: CT_TcPr) -> CT_TcPr:
+        """Override default `._insert_tcPr()`."""
+        # -- `tcPr`` has a large number of successors, but always comes first if it appears,
+        # -- so just using insert(0, ...) rather than spelling out successors.
+        self.insert(0, tcPr)
+        return tcPr
+
+    @property
+    def _is_empty(self) -> bool:
+        """True if this cell contains only a single empty `w:p` element."""
+        block_items = list(self.iter_block_items())
+        if len(block_items) > 1:
+            return False
+        # -- cell must include at least one block item but can be a `w:tbl`, `w:sdt`,
+        # -- `w:customXml` or a `w:p`
+        only_item = block_items[0]
+        if isinstance(only_item, CT_P) and len(only_item.r_lst) == 0:
+            return True
+        return False
+
+    def _move_content_to(self, other_tc: CT_Tc):
+        """Append the content of this cell to `other_tc`.
+
+        Leaves this cell with a single empty ``<w:p>`` element.
+        """
+        if other_tc is self:
+            return
+        if self._is_empty:
+            return
+        other_tc._remove_trailing_empty_p()
+        # -- appending moves each element from self to other_tc --
+        for block_element in self.iter_block_items():
+            other_tc.append(block_element)
+        # -- add back the required minimum single empty <w:p> element --
+        self.append(self._new_p())
+
+    def _new_tbl(self) -> None:
+        raise NotImplementedError(
+            "use CT_Tbl.new_tbl() to add a new table, specifying rows and columns"
+        )
+
+    @property
+    def _next_tc(self) -> CT_Tc | None:
+        """The `w:tc` element immediately following this one in this row, or |None| if
+        this is the last `w:tc` element in the row."""
+        following_tcs = self.xpath("./following-sibling::w:tc")
+        return following_tcs[0] if following_tcs else None
+
+    def _remove(self):
+        """Remove this `w:tc` element from the XML tree."""
+        parent_element = self.getparent()
+        assert parent_element is not None
+        parent_element.remove(self)
+
+    def _remove_trailing_empty_p(self):
+        """Remove last content element from this cell if it's an empty `w:p` element."""
+        block_items = list(self.iter_block_items())
+        last_content_elm = block_items[-1]
+        if not isinstance(last_content_elm, CT_P):
+            return
+        p = last_content_elm
+        if len(p.r_lst) > 0:
+            return
+        self.remove(p)
+
+    def _span_dimensions(self, other_tc: CT_Tc) -> tuple[int, int, int, int]:
+        """Return a (top, left, height, width) 4-tuple specifying the extents of the
+        merged cell formed by using this tc and `other_tc` as opposite corner
+        extents."""
+
+        def raise_on_inverted_L(a: CT_Tc, b: CT_Tc):
+            if a.top == b.top and a.bottom != b.bottom:
+                raise InvalidSpanError("requested span not rectangular")
+            if a.left == b.left and a.right != b.right:
+                raise InvalidSpanError("requested span not rectangular")
+
+        def raise_on_tee_shaped(a: CT_Tc, b: CT_Tc):
+            top_most, other = (a, b) if a.top < b.top else (b, a)
+            if top_most.top < other.top and top_most.bottom > other.bottom:
+                raise InvalidSpanError("requested span not rectangular")
+
+            left_most, other = (a, b) if a.left < b.left else (b, a)
+            if left_most.left < other.left and left_most.right > other.right:
+                raise InvalidSpanError("requested span not rectangular")
+
+        raise_on_inverted_L(self, other_tc)
+        raise_on_tee_shaped(self, other_tc)
+
+        top = min(self.top, other_tc.top)
+        left = min(self.left, other_tc.left)
+        bottom = max(self.bottom, other_tc.bottom)
+        right = max(self.right, other_tc.right)
+
+        return top, left, bottom - top, right - left
+
+    def _span_to_width(self, grid_width: int, top_tc: CT_Tc, vMerge: str | None):
+        """Incorporate `w:tc` elements to the right until this cell spans `grid_width`.
+
+        Incorporated `w:tc` elements are removed (replaced by gridSpan value).
+
+        Raises |ValueError| if `grid_width` cannot be exactly achieved, such as when a
+        merged cell would drive the span width greater than `grid_width` or if not
+        enough grid columns are available to make this cell that wide. All content from
+        incorporated cells is appended to `top_tc`. The val attribute of the vMerge
+        element on the single remaining cell is set to `vMerge`. If `vMerge` is |None|,
+        the vMerge element is removed if present.
+        """
+        self._move_content_to(top_tc)
+        while self.grid_span < grid_width:
+            self._swallow_next_tc(grid_width, top_tc)
+        self.vMerge = vMerge
+
+    def _swallow_next_tc(self, grid_width: int, top_tc: CT_Tc):
+        """Extend the horizontal span of this `w:tc` element to incorporate the
+        following `w:tc` element in the row and then delete that following `w:tc`
+        element.
+
+        Any content in the following `w:tc` element is appended to the content of
+        `top_tc`. The width of the following `w:tc` element is added to this one, if
+        present. Raises |InvalidSpanError| if the width of the resulting cell is greater
+        than `grid_width` or if there is no next `<w:tc>` element in the row.
+        """
+
+        def raise_on_invalid_swallow(next_tc: CT_Tc | None):
+            if next_tc is None:
+                raise InvalidSpanError("not enough grid columns")
+            if self.grid_span + next_tc.grid_span > grid_width:
+                raise InvalidSpanError("span is not rectangular")
+
+        next_tc = self._next_tc
+        raise_on_invalid_swallow(next_tc)
+        assert next_tc is not None
+        next_tc._move_content_to(top_tc)
+        self._add_width_of(next_tc)
+        self.grid_span += next_tc.grid_span
+        next_tc._remove()
+
+    @property
+    def _tbl(self) -> CT_Tbl:
+        """The tbl element this tc element appears in."""
+        return cast(CT_Tbl, self.xpath("./ancestor::w:tbl[position()=1]")[0])
+
+    @property
+    def _tc_above(self) -> CT_Tc:
+        """The `w:tc` element immediately above this one in its grid column."""
+        return self._tr_above.tc_at_grid_offset(self.grid_offset)
+
+    @property
+    def _tc_below(self) -> CT_Tc | None:
+        """The tc element immediately below this one in its grid column."""
+        tr_below = self._tr_below
+        if tr_below is None:
+            return None
+        return tr_below.tc_at_grid_offset(self.grid_offset)
+
+    @property
+    def _tr(self) -> CT_Row:
+        """The tr element this tc element appears in."""
+        return cast(CT_Row, self.xpath("./ancestor::w:tr[position()=1]")[0])
+
+    @property
+    def _tr_above(self) -> CT_Row:
+        """The tr element prior in sequence to the tr this cell appears in.
+
+        Raises |ValueError| if called on a cell in the top-most row.
+        """
+        tr_aboves = self.xpath("./ancestor::w:tr[position()=1]/preceding-sibling::w:tr[1]")
+        if not tr_aboves:
+            raise ValueError("no tr above topmost tr in w:tbl")
+        return tr_aboves[0]
+
+    @property
+    def _tr_below(self) -> CT_Row | None:
+        """The tr element next in sequence after the tr this cell appears in, or |None|
+        if this cell appears in the last row."""
+        tr_lst = self._tbl.tr_lst
+        tr_idx = tr_lst.index(self._tr)
+        try:
+            return tr_lst[tr_idx + 1]
+        except IndexError:
+            return None
+
+    @property
+    def _tr_idx(self) -> int:
+        """The row index of the tr element this tc element appears in."""
+        return self._tbl.tr_lst.index(self._tr)
+
+
+class CT_TcPr(BaseOxmlElement):
+    """``<w:tcPr>`` element, defining table cell properties."""
+
+    get_or_add_gridSpan: Callable[[], CT_DecimalNumber]
+    get_or_add_tcW: Callable[[], CT_TblWidth]
+    get_or_add_vAlign: Callable[[], CT_VerticalJc]
+    _add_vMerge: Callable[[], CT_VMerge]
+    _remove_gridSpan: Callable[[], None]
+    _remove_vAlign: Callable[[], None]
+    _remove_vMerge: Callable[[], None]
+
+    _tag_seq = (
+        "w:cnfStyle",
+        "w:tcW",
+        "w:gridSpan",
+        "w:hMerge",
+        "w:vMerge",
+        "w:tcBorders",
+        "w:shd",
+        "w:noWrap",
+        "w:tcMar",
+        "w:textDirection",
+        "w:tcFitText",
+        "w:vAlign",
+        "w:hideMark",
+        "w:headers",
+        "w:cellIns",
+        "w:cellDel",
+        "w:cellMerge",
+        "w:tcPrChange",
+    )
+    tcW: CT_TblWidth | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:tcW", successors=_tag_seq[2:]
+    )
+    gridSpan: CT_DecimalNumber | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:gridSpan", successors=_tag_seq[3:]
+    )
+    vMerge: CT_VMerge | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:vMerge", successors=_tag_seq[5:]
+    )
+    vAlign: CT_VerticalJc | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:vAlign", successors=_tag_seq[12:]
+    )
+    del _tag_seq
+
+    @property
+    def grid_span(self) -> int:
+        """The integer number of columns this cell spans.
+
+        Determined by ./w:gridSpan/@val, it defaults to 1.
+        """
+        gridSpan = self.gridSpan
+        return 1 if gridSpan is None else gridSpan.val
+
+    @grid_span.setter
+    def grid_span(self, value: int):
+        self._remove_gridSpan()
+        if value > 1:
+            self.get_or_add_gridSpan().val = value
+
+    @property
+    def vAlign_val(self):
+        """Value of `w:val` attribute on  `w:vAlign` child.
+
+        Value is |None| if `w:vAlign` child is not present. The `w:val` attribute on
+        `w:vAlign` is required.
+        """
+        vAlign = self.vAlign
+        if vAlign is None:
+            return None
+        return vAlign.val
+
+    @vAlign_val.setter
+    def vAlign_val(self, value: WD_CELL_VERTICAL_ALIGNMENT | None):
+        if value is None:
+            self._remove_vAlign()
+            return
+        self.get_or_add_vAlign().val = value
+
+    @property
+    def vMerge_val(self):
+        """The value of the ./w:vMerge/@val attribute, or |None| if the w:vMerge element
+        is not present."""
+        vMerge = self.vMerge
+        if vMerge is None:
+            return None
+        return vMerge.val
+
+    @vMerge_val.setter
+    def vMerge_val(self, value: str | None):
+        self._remove_vMerge()
+        if value is not None:
+            self._add_vMerge().val = value
+
+    @property
+    def width(self) -> Length | None:
+        """EMU length in `./w:tcW` or |None| if not present or its type is not 'dxa'."""
+        tcW = self.tcW
+        if tcW is None:
+            return None
+        return tcW.width
+
+    @width.setter
+    def width(self, value: Length):
+        tcW = self.get_or_add_tcW()
+        tcW.width = value
+
+
+class CT_TrPr(BaseOxmlElement):
+    """``<w:trPr>`` element, defining table row properties."""
+
+    get_or_add_trHeight: Callable[[], CT_Height]
+
+    _tag_seq = (
+        "w:cnfStyle",
+        "w:divId",
+        "w:gridBefore",
+        "w:gridAfter",
+        "w:wBefore",
+        "w:wAfter",
+        "w:cantSplit",
+        "w:trHeight",
+        "w:tblHeader",
+        "w:tblCellSpacing",
+        "w:jc",
+        "w:hidden",
+        "w:ins",
+        "w:del",
+        "w:trPrChange",
+    )
+    gridAfter: CT_DecimalNumber | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:gridAfter", successors=_tag_seq[4:]
+    )
+    gridBefore: CT_DecimalNumber | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:gridBefore", successors=_tag_seq[3:]
+    )
+    trHeight: CT_Height | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:trHeight", successors=_tag_seq[8:]
+    )
+    del _tag_seq
+
+    @property
+    def grid_after(self) -> int:
+        """The number of unpopulated layout-grid cells at the end of this row."""
+        gridAfter = self.gridAfter
+        return 0 if gridAfter is None else gridAfter.val
+
+    @property
+    def grid_before(self) -> int:
+        """The number of unpopulated layout-grid cells at the start of this row."""
+        gridBefore = self.gridBefore
+        return 0 if gridBefore is None else gridBefore.val
+
+    @property
+    def trHeight_hRule(self) -> WD_ROW_HEIGHT_RULE | None:
+        """Return the value of `w:trHeight@w:hRule`, or |None| if not present."""
+        trHeight = self.trHeight
+        return None if trHeight is None else trHeight.hRule
+
+    @trHeight_hRule.setter
+    def trHeight_hRule(self, value: WD_ROW_HEIGHT_RULE | None):
+        if value is None and self.trHeight is None:
+            return
+        trHeight = self.get_or_add_trHeight()
+        trHeight.hRule = value
+
+    @property
+    def trHeight_val(self):
+        """Return the value of `w:trHeight@w:val`, or |None| if not present."""
+        trHeight = self.trHeight
+        return None if trHeight is None else trHeight.val
+
+    @trHeight_val.setter
+    def trHeight_val(self, value: Length | None):
+        if value is None and self.trHeight is None:
+            return
+        trHeight = self.get_or_add_trHeight()
+        trHeight.val = value
+
+
+class CT_VerticalJc(BaseOxmlElement):
+    """`w:vAlign` element, specifying vertical alignment of cell."""
+
+    val: WD_CELL_VERTICAL_ALIGNMENT = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:val", WD_CELL_VERTICAL_ALIGNMENT
+    )
+
+
+class CT_VMerge(BaseOxmlElement):
+    """``<w:vMerge>`` element, specifying vertical merging behavior of a cell."""
+
+    val: str | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:val", ST_Merge, default=ST_Merge.CONTINUE
+    )
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/text/__init__.py b/.venv/lib/python3.12/site-packages/docx/oxml/text/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/text/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/text/font.py b/.venv/lib/python3.12/site-packages/docx/oxml/text/font.py
new file mode 100644
index 00000000..140086aa
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/text/font.py
@@ -0,0 +1,356 @@
+"""Custom element classes related to run properties (font)."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable
+
+from docx.enum.dml import MSO_THEME_COLOR
+from docx.enum.text import WD_COLOR_INDEX, WD_UNDERLINE
+from docx.oxml.ns import nsdecls
+from docx.oxml.parser import parse_xml
+from docx.oxml.simpletypes import (
+    ST_HexColor,
+    ST_HpsMeasure,
+    ST_String,
+    ST_VerticalAlignRun,
+)
+from docx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrOne,
+)
+
+if TYPE_CHECKING:
+    from docx.oxml.shared import CT_OnOff, CT_String
+    from docx.shared import Length
+
+
+class CT_Color(BaseOxmlElement):
+    """`w:color` element, specifying the color of a font and perhaps other objects."""
+
+    val = RequiredAttribute("w:val", ST_HexColor)
+    themeColor = OptionalAttribute("w:themeColor", MSO_THEME_COLOR)
+
+
+class CT_Fonts(BaseOxmlElement):
+    """`<w:rFonts>` element.
+
+    Specifies typeface name for the various language types.
+    """
+
+    ascii: str | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:ascii", ST_String
+    )
+    hAnsi: str | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:hAnsi", ST_String
+    )
+
+
+class CT_Highlight(BaseOxmlElement):
+    """`w:highlight` element, specifying font highlighting/background color."""
+
+    val: WD_COLOR_INDEX = RequiredAttribute(  # pyright: ignore[reportGeneralTypeIssues]
+        "w:val", WD_COLOR_INDEX
+    )
+
+
+class CT_HpsMeasure(BaseOxmlElement):
+    """Used for `<w:sz>` element and others, specifying font size in half-points."""
+
+    val: Length = RequiredAttribute(  # pyright: ignore[reportGeneralTypeIssues]
+        "w:val", ST_HpsMeasure
+    )
+
+
+class CT_RPr(BaseOxmlElement):
+    """`<w:rPr>` element, containing the properties for a run."""
+
+    get_or_add_highlight: Callable[[], CT_Highlight]
+    get_or_add_rFonts: Callable[[], CT_Fonts]
+    get_or_add_sz: Callable[[], CT_HpsMeasure]
+    get_or_add_vertAlign: Callable[[], CT_VerticalAlignRun]
+    _add_rStyle: Callable[..., CT_String]
+    _add_u: Callable[[], CT_Underline]
+    _remove_highlight: Callable[[], None]
+    _remove_rFonts: Callable[[], None]
+    _remove_rStyle: Callable[[], None]
+    _remove_sz: Callable[[], None]
+    _remove_u: Callable[[], None]
+    _remove_vertAlign: Callable[[], None]
+
+    _tag_seq = (
+        "w:rStyle",
+        "w:rFonts",
+        "w:b",
+        "w:bCs",
+        "w:i",
+        "w:iCs",
+        "w:caps",
+        "w:smallCaps",
+        "w:strike",
+        "w:dstrike",
+        "w:outline",
+        "w:shadow",
+        "w:emboss",
+        "w:imprint",
+        "w:noProof",
+        "w:snapToGrid",
+        "w:vanish",
+        "w:webHidden",
+        "w:color",
+        "w:spacing",
+        "w:w",
+        "w:kern",
+        "w:position",
+        "w:sz",
+        "w:szCs",
+        "w:highlight",
+        "w:u",
+        "w:effect",
+        "w:bdr",
+        "w:shd",
+        "w:fitText",
+        "w:vertAlign",
+        "w:rtl",
+        "w:cs",
+        "w:em",
+        "w:lang",
+        "w:eastAsianLayout",
+        "w:specVanish",
+        "w:oMath",
+    )
+    rStyle: CT_String | None = ZeroOrOne(  # pyright: ignore[reportGeneralTypeIssues]
+        "w:rStyle", successors=_tag_seq[1:]
+    )
+    rFonts: CT_Fonts | None = ZeroOrOne(  # pyright: ignore[reportGeneralTypeIssues]
+        "w:rFonts", successors=_tag_seq[2:]
+    )
+    b: CT_OnOff | None = ZeroOrOne(  # pyright: ignore[reportGeneralTypeIssues]
+        "w:b", successors=_tag_seq[3:]
+    )
+    bCs = ZeroOrOne("w:bCs", successors=_tag_seq[4:])
+    i = ZeroOrOne("w:i", successors=_tag_seq[5:])
+    iCs = ZeroOrOne("w:iCs", successors=_tag_seq[6:])
+    caps = ZeroOrOne("w:caps", successors=_tag_seq[7:])
+    smallCaps = ZeroOrOne("w:smallCaps", successors=_tag_seq[8:])
+    strike = ZeroOrOne("w:strike", successors=_tag_seq[9:])
+    dstrike = ZeroOrOne("w:dstrike", successors=_tag_seq[10:])
+    outline = ZeroOrOne("w:outline", successors=_tag_seq[11:])
+    shadow = ZeroOrOne("w:shadow", successors=_tag_seq[12:])
+    emboss = ZeroOrOne("w:emboss", successors=_tag_seq[13:])
+    imprint = ZeroOrOne("w:imprint", successors=_tag_seq[14:])
+    noProof = ZeroOrOne("w:noProof", successors=_tag_seq[15:])
+    snapToGrid = ZeroOrOne("w:snapToGrid", successors=_tag_seq[16:])
+    vanish = ZeroOrOne("w:vanish", successors=_tag_seq[17:])
+    webHidden = ZeroOrOne("w:webHidden", successors=_tag_seq[18:])
+    color = ZeroOrOne("w:color", successors=_tag_seq[19:])
+    sz: CT_HpsMeasure | None = ZeroOrOne(  # pyright: ignore[reportGeneralTypeIssues]
+        "w:sz", successors=_tag_seq[24:]
+    )
+    highlight: CT_Highlight | None = ZeroOrOne(  # pyright: ignore[reportGeneralTypeIssues]
+        "w:highlight", successors=_tag_seq[26:]
+    )
+    u: CT_Underline | None = ZeroOrOne(  # pyright: ignore[reportGeneralTypeIssues]
+        "w:u", successors=_tag_seq[27:]
+    )
+    vertAlign: CT_VerticalAlignRun | None = ZeroOrOne(  # pyright: ignore[reportGeneralTypeIssues]
+        "w:vertAlign", successors=_tag_seq[32:]
+    )
+    rtl = ZeroOrOne("w:rtl", successors=_tag_seq[33:])
+    cs = ZeroOrOne("w:cs", successors=_tag_seq[34:])
+    specVanish = ZeroOrOne("w:specVanish", successors=_tag_seq[38:])
+    oMath = ZeroOrOne("w:oMath", successors=_tag_seq[39:])
+    del _tag_seq
+
+    def _new_color(self):
+        """Override metaclass method to set `w:color/@val` to RGB black on create."""
+        return parse_xml('<w:color %s w:val="000000"/>' % nsdecls("w"))
+
+    @property
+    def highlight_val(self) -> WD_COLOR_INDEX | None:
+        """Value of `./w:highlight/@val`.
+
+        Specifies font's highlight color, or `None` if the text is not highlighted.
+        """
+        highlight = self.highlight
+        if highlight is None:
+            return None
+        return highlight.val
+
+    @highlight_val.setter
+    def highlight_val(self, value: WD_COLOR_INDEX | None) -> None:
+        if value is None:
+            self._remove_highlight()
+            return
+        highlight = self.get_or_add_highlight()
+        highlight.val = value
+
+    @property
+    def rFonts_ascii(self) -> str | None:
+        """The value of `w:rFonts/@w:ascii` or |None| if not present.
+
+        Represents the assigned typeface name. The rFonts element also specifies other
+        special-case typeface names; this method handles the case where just the common
+        name is required.
+        """
+        rFonts = self.rFonts
+        if rFonts is None:
+            return None
+        return rFonts.ascii
+
+    @rFonts_ascii.setter
+    def rFonts_ascii(self, value: str | None) -> None:
+        if value is None:
+            self._remove_rFonts()
+            return
+        rFonts = self.get_or_add_rFonts()
+        rFonts.ascii = value
+
+    @property
+    def rFonts_hAnsi(self) -> str | None:
+        """The value of `w:rFonts/@w:hAnsi` or |None| if not present."""
+        rFonts = self.rFonts
+        if rFonts is None:
+            return None
+        return rFonts.hAnsi
+
+    @rFonts_hAnsi.setter
+    def rFonts_hAnsi(self, value: str | None):
+        if value is None and self.rFonts is None:
+            return
+        rFonts = self.get_or_add_rFonts()
+        rFonts.hAnsi = value
+
+    @property
+    def style(self) -> str | None:
+        """String in `./w:rStyle/@val`, or None if `w:rStyle` is not present."""
+        rStyle = self.rStyle
+        if rStyle is None:
+            return None
+        return rStyle.val
+
+    @style.setter
+    def style(self, style: str | None) -> None:
+        """Set `./w:rStyle/@val` to `style`, adding the `w:rStyle` element if necessary.
+
+        If `style` is |None|, remove `w:rStyle` element if present.
+        """
+        if style is None:
+            self._remove_rStyle()
+        elif self.rStyle is None:
+            self._add_rStyle(val=style)
+        else:
+            self.rStyle.val = style
+
+    @property
+    def subscript(self) -> bool | None:
+        """|True| if `./w:vertAlign/@w:val` is "subscript".
+
+        |False| if `w:vertAlign/@w:val` contains any other value. |None| if
+        `w:vertAlign` is not present.
+        """
+        vertAlign = self.vertAlign
+        if vertAlign is None:
+            return None
+        if vertAlign.val == ST_VerticalAlignRun.SUBSCRIPT:
+            return True
+        return False
+
+    @subscript.setter
+    def subscript(self, value: bool | None) -> None:
+        if value is None:
+            self._remove_vertAlign()
+        elif bool(value) is True:
+            self.get_or_add_vertAlign().val = ST_VerticalAlignRun.SUBSCRIPT
+        # -- assert bool(value) is False --
+        elif self.vertAlign is not None and self.vertAlign.val == ST_VerticalAlignRun.SUBSCRIPT:
+            self._remove_vertAlign()
+
+    @property
+    def superscript(self) -> bool | None:
+        """|True| if `w:vertAlign/@w:val` is 'superscript'.
+
+        |False| if `w:vertAlign/@w:val` contains any other value. |None| if
+        `w:vertAlign` is not present.
+        """
+        vertAlign = self.vertAlign
+        if vertAlign is None:
+            return None
+        if vertAlign.val == ST_VerticalAlignRun.SUPERSCRIPT:
+            return True
+        return False
+
+    @superscript.setter
+    def superscript(self, value: bool | None):
+        if value is None:
+            self._remove_vertAlign()
+        elif bool(value) is True:
+            self.get_or_add_vertAlign().val = ST_VerticalAlignRun.SUPERSCRIPT
+        # -- assert bool(value) is False --
+        elif self.vertAlign is not None and self.vertAlign.val == ST_VerticalAlignRun.SUPERSCRIPT:
+            self._remove_vertAlign()
+
+    @property
+    def sz_val(self) -> Length | None:
+        """The value of `w:sz/@w:val` or |None| if not present."""
+        sz = self.sz
+        if sz is None:
+            return None
+        return sz.val
+
+    @sz_val.setter
+    def sz_val(self, value: Length | None):
+        if value is None:
+            self._remove_sz()
+            return
+        sz = self.get_or_add_sz()
+        sz.val = value
+
+    @property
+    def u_val(self) -> WD_UNDERLINE | None:
+        """Value of `w:u/@val`, or None if not present.
+
+        Values `WD_UNDERLINE.SINGLE` and `WD_UNDERLINE.NONE` are mapped to `True` and
+        `False` respectively.
+        """
+        u = self.u
+        if u is None:
+            return None
+        return u.val
+
+    @u_val.setter
+    def u_val(self, value: WD_UNDERLINE | None):
+        self._remove_u()
+        if value is not None:
+            self._add_u().val = value
+
+    def _get_bool_val(self, name: str) -> bool | None:
+        """Value of boolean child with `name`, e.g. "w:b", "w:i", and "w:smallCaps"."""
+        element = getattr(self, name)
+        if element is None:
+            return None
+        return element.val
+
+    def _set_bool_val(self, name: str, value: bool | None):
+        if value is None:
+            getattr(self, "_remove_%s" % name)()
+            return
+        element = getattr(self, "get_or_add_%s" % name)()
+        element.val = value
+
+
+class CT_Underline(BaseOxmlElement):
+    """`<w:u>` element, specifying the underlining style for a run."""
+
+    val: WD_UNDERLINE | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:val", WD_UNDERLINE
+    )
+
+
+class CT_VerticalAlignRun(BaseOxmlElement):
+    """`<w:vertAlign>` element, specifying subscript or superscript."""
+
+    val: str = RequiredAttribute(  # pyright: ignore[reportGeneralTypeIssues]
+        "w:val", ST_VerticalAlignRun
+    )
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/text/hyperlink.py b/.venv/lib/python3.12/site-packages/docx/oxml/text/hyperlink.py
new file mode 100644
index 00000000..38a33ff1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/text/hyperlink.py
@@ -0,0 +1,45 @@
+"""Custom element classes related to hyperlinks (CT_Hyperlink)."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, List
+
+from docx.oxml.simpletypes import ST_OnOff, ST_String, XsdString
+from docx.oxml.text.run import CT_R
+from docx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OptionalAttribute,
+    ZeroOrMore,
+)
+
+if TYPE_CHECKING:
+    from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak
+
+
+class CT_Hyperlink(BaseOxmlElement):
+    """`<w:hyperlink>` element, containing the text and address for a hyperlink."""
+
+    r_lst: List[CT_R]
+
+    rId: str | None = OptionalAttribute("r:id", XsdString)  # pyright: ignore[reportAssignmentType]
+    anchor: str | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:anchor", ST_String
+    )
+    history: bool = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:history", ST_OnOff, default=True
+    )
+
+    r = ZeroOrMore("w:r")
+
+    @property
+    def lastRenderedPageBreaks(self) -> List[CT_LastRenderedPageBreak]:
+        """All `w:lastRenderedPageBreak` descendants of this hyperlink."""
+        return self.xpath("./w:r/w:lastRenderedPageBreak")
+
+    @property
+    def text(self) -> str:  # pyright: ignore[reportIncompatibleMethodOverride]
+        """The textual content of this hyperlink.
+
+        `CT_Hyperlink` stores the hyperlink-text as one or more `w:r` children.
+        """
+        return "".join(r.text for r in self.xpath("w:r"))
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/text/pagebreak.py b/.venv/lib/python3.12/site-packages/docx/oxml/text/pagebreak.py
new file mode 100644
index 00000000..943f9b6c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/text/pagebreak.py
@@ -0,0 +1,284 @@
+"""Custom element class for rendered page-break (CT_LastRenderedPageBreak)."""
+
+from __future__ import annotations
+
+import copy
+from typing import TYPE_CHECKING
+
+from docx.oxml.xmlchemy import BaseOxmlElement
+from docx.shared import lazyproperty
+
+if TYPE_CHECKING:
+    from docx.oxml.text.hyperlink import CT_Hyperlink
+    from docx.oxml.text.paragraph import CT_P
+
+
+class CT_LastRenderedPageBreak(BaseOxmlElement):
+    """`<w:lastRenderedPageBreak>` element, indicating page break inserted by renderer.
+
+    A rendered page-break is one inserted by the renderer when it runs out of room on a
+    page. It is an empty element (no attrs or children) and is a child of CT_R, peer to
+    CT_Text.
+
+    NOTE: this complex-type name does not exist in the schema, where
+    `w:lastRenderedPageBreak` maps to `CT_Empty`. This name was added to give it
+    distinguished behavior. CT_Empty is used for many elements.
+    """
+
+    @property
+    def following_fragment_p(self) -> CT_P:
+        """A "loose" `CT_P` containing only the paragraph content before this break.
+
+        Raises `ValueError` if this `w:lastRenderedPageBreak` is not the first rendered
+        page-break in its paragraph.
+
+        The returned `CT_P` is a "clone" (deepcopy) of the `w:p` ancestor of this
+        page-break with this `w:lastRenderedPageBreak` element and all content preceding
+        it removed.
+
+        NOTE: this `w:p` can itself contain one or more `w:renderedPageBreak` elements
+        (when the paragraph contained more than one). While this is rare, the caller
+        should treat this paragraph the same as other paragraphs and split it if
+        necessary in a folloing step or recursion.
+        """
+        if not self == self._first_lrpb_in_p(self._enclosing_p):
+            raise ValueError("only defined on first rendered page-break in paragraph")
+
+        # -- splitting approach is different when break is inside a hyperlink --
+        return (
+            self._following_frag_in_hlink
+            if self._is_in_hyperlink
+            else self._following_frag_in_run
+        )
+
+    @property
+    def follows_all_content(self) -> bool:
+        """True when this page-break element is the last "content" in the paragraph.
+
+        This is very uncommon case and may only occur in contrived or cases where the
+        XML is edited by hand, but it is not precluded by the spec.
+        """
+        # -- a page-break inside a hyperlink never meets these criteria (for our
+        # -- purposes at least) because it is considered "atomic" and always associated
+        # -- with the page it starts on.
+        if self._is_in_hyperlink:
+            return False
+
+        return bool(
+            # -- XPath will match zero-or-one w:lastRenderedPageBreak element --
+            self._enclosing_p.xpath(
+                # -- in first run of paragraph --
+                f"(./w:r)[last()]"
+                # -- all page-breaks --
+                f"/w:lastRenderedPageBreak"
+                # -- that are not preceded by any content-bearing elements --
+                f"[not(following-sibling::*[{self._run_inner_content_xpath}])]"
+            )
+        )
+
+    @property
+    def precedes_all_content(self) -> bool:
+        """True when a `w:lastRenderedPageBreak` precedes all paragraph content.
+
+        This is a common case; it occurs whenever the page breaks on an even paragraph
+        boundary.
+        """
+        # -- a page-break inside a hyperlink never meets these criteria because there
+        # -- is always part of the hyperlink text before the page-break.
+        if self._is_in_hyperlink:
+            return False
+
+        return bool(
+            # -- XPath will match zero-or-one w:lastRenderedPageBreak element --
+            self._enclosing_p.xpath(
+                # -- in first run of paragraph --
+                f"./w:r[1]"
+                # -- all page-breaks --
+                f"/w:lastRenderedPageBreak"
+                # -- that are not preceded by any content-bearing elements --
+                f"[not(preceding-sibling::*[{self._run_inner_content_xpath}])]"
+            )
+        )
+
+    @property
+    def preceding_fragment_p(self) -> CT_P:
+        """A "loose" `CT_P` containing only the paragraph content before this break.
+
+        Raises `ValueError` if this `w:lastRenderedPageBreak` is not the first rendered
+        paragraph in its paragraph.
+
+        The returned `CT_P` is a "clone" (deepcopy) of the `w:p` ancestor of this
+        page-break with this `w:lastRenderedPageBreak` element and all its following
+        siblings removed.
+        """
+        if not self == self._first_lrpb_in_p(self._enclosing_p):
+            raise ValueError("only defined on first rendered page-break in paragraph")
+
+        # -- splitting approach is different when break is inside a hyperlink --
+        return (
+            self._preceding_frag_in_hlink
+            if self._is_in_hyperlink
+            else self._preceding_frag_in_run
+        )
+
+    def _enclosing_hyperlink(self, lrpb: CT_LastRenderedPageBreak) -> CT_Hyperlink:
+        """The `w:hyperlink` grandparent of this `w:lastRenderedPageBreak`.
+
+        Raises `IndexError` when this page-break has a `w:p` grandparent, so only call
+        when `._is_in_hyperlink` is True.
+        """
+        return lrpb.xpath("./parent::w:r/parent::w:hyperlink")[0]
+
+    @property
+    def _enclosing_p(self) -> CT_P:
+        """The `w:p` element parent or grandparent of this `w:lastRenderedPageBreak`."""
+        return self.xpath("./ancestor::w:p[1]")[0]
+
+    def _first_lrpb_in_p(self, p: CT_P) -> CT_LastRenderedPageBreak:
+        """The first `w:lastRenderedPageBreak` element in `p`.
+
+        Raises `ValueError` if there are no rendered page-breaks in `p`.
+        """
+        lrpbs = p.xpath(
+            "./w:r/w:lastRenderedPageBreak | ./w:hyperlink/w:r/w:lastRenderedPageBreak"
+        )
+        if not lrpbs:
+            raise ValueError("no rendered page-breaks in paragraph element")
+        return lrpbs[0]
+
+    @lazyproperty
+    def _following_frag_in_hlink(self) -> CT_P:
+        """Following CT_P fragment when break occurs within a hyperlink.
+
+        Note this is a *partial-function* and raises when `lrpb` is not inside a
+        hyperlink.
+        """
+        if not self._is_in_hyperlink:
+            raise ValueError("only defined on a rendered page-break in a hyperlink")
+
+        # -- work on a clone `w:p` so our mutations don't persist --
+        p = copy.deepcopy(self._enclosing_p)
+
+        # -- get this `w:lastRenderedPageBreak` in the cloned `w:p` (not self) --
+        lrpb = self._first_lrpb_in_p(p)
+
+        # -- locate `w:hyperlink` in which this `w:lastRenderedPageBreak` is found --
+        hyperlink = lrpb._enclosing_hyperlink(lrpb)
+
+        # -- delete all w:p inner-content preceding the hyperlink --
+        for e in hyperlink.xpath("./preceding-sibling::*[not(self::w:pPr)]"):
+            p.remove(e)
+
+        # -- remove the whole hyperlink, it belongs to the preceding-fragment-p --
+        hyperlink.getparent().remove(hyperlink)
+
+        # -- that's it, return the remaining fragment of `w:p` clone --
+        return p
+
+    @lazyproperty
+    def _following_frag_in_run(self) -> CT_P:
+        """following CT_P fragment when break does not occur in a hyperlink.
+
+        Note this is a *partial-function* and raises when `lrpb` is inside a hyperlink.
+        """
+        if self._is_in_hyperlink:
+            raise ValueError("only defined on a rendered page-break not in a hyperlink")
+
+        # -- work on a clone `w:p` so our mutations don't persist --
+        p = copy.deepcopy(self._enclosing_p)
+
+        # -- get this `w:lastRenderedPageBreak` in the cloned `w:p` (not self) --
+        lrpb = self._first_lrpb_in_p(p)
+
+        # -- locate `w:r` in which this `w:lastRenderedPageBreak` is found --
+        enclosing_r = lrpb.xpath("./parent::w:r")[0]
+
+        # -- delete all w:p inner-content preceding that run (but not w:pPr) --
+        for e in enclosing_r.xpath("./preceding-sibling::*[not(self::w:pPr)]"):
+            p.remove(e)
+
+        # -- then remove all run inner-content preceding this lrpb in its run (but not
+        # -- the `w:rPr`) and also remove the page-break itself
+        for e in lrpb.xpath("./preceding-sibling::*[not(self::w:rPr)]"):
+            enclosing_r.remove(e)
+        enclosing_r.remove(lrpb)
+
+        return p
+
+    @lazyproperty
+    def _is_in_hyperlink(self) -> bool:
+        """True when this page-break is embedded in a hyperlink run."""
+        return bool(self.xpath("./parent::w:r/parent::w:hyperlink"))
+
+    @lazyproperty
+    def _preceding_frag_in_hlink(self) -> CT_P:
+        """Preceding CT_P fragment when break occurs within a hyperlink.
+
+        Note this is a *partial-function* and raises when `lrpb` is not inside a
+        hyperlink.
+        """
+        if not self._is_in_hyperlink:
+            raise ValueError("only defined on a rendered page-break in a hyperlink")
+
+        # -- work on a clone `w:p` so our mutations don't persist --
+        p = copy.deepcopy(self._enclosing_p)
+
+        # -- get this `w:lastRenderedPageBreak` in the cloned `w:p` (not self) --
+        lrpb = self._first_lrpb_in_p(p)
+
+        # -- locate `w:hyperlink` in which this `w:lastRenderedPageBreak` is found --
+        hyperlink = lrpb._enclosing_hyperlink(lrpb)
+
+        # -- delete all w:p inner-content following the hyperlink --
+        for e in hyperlink.xpath("./following-sibling::*"):
+            p.remove(e)
+
+        # -- remove this page-break from inside the hyperlink --
+        lrpb.getparent().remove(lrpb)
+
+        # -- that's it, the entire hyperlink goes into the preceding fragment so
+        # -- the hyperlink is not "split".
+        return p
+
+    @lazyproperty
+    def _preceding_frag_in_run(self) -> CT_P:
+        """Preceding CT_P fragment when break does not occur in a hyperlink.
+
+        Note this is a *partial-function* and raises when `lrpb` is inside a hyperlink.
+        """
+        if self._is_in_hyperlink:
+            raise ValueError("only defined on a rendered page-break not in a hyperlink")
+
+        # -- work on a clone `w:p` so our mutations don't persist --
+        p = copy.deepcopy(self._enclosing_p)
+
+        # -- get this `w:lastRenderedPageBreak` in the cloned `w:p` (not self) --
+        lrpb = self._first_lrpb_in_p(p)
+
+        # -- locate `w:r` in which this `w:lastRenderedPageBreak` is found --
+        enclosing_r = lrpb.xpath("./parent::w:r")[0]
+
+        # -- delete all `w:p` inner-content following that run --
+        for e in enclosing_r.xpath("./following-sibling::*"):
+            p.remove(e)
+
+        # -- then delete all `w:r` inner-content following this lrpb in its run and
+        # -- also remove the page-break itself
+        for e in lrpb.xpath("./following-sibling::*"):
+            enclosing_r.remove(e)
+        enclosing_r.remove(lrpb)
+
+        return p
+
+    @lazyproperty
+    def _run_inner_content_xpath(self) -> str:
+        """XPath fragment matching any run inner-content elements."""
+        return (
+            "self::w:br"
+            " | self::w:cr"
+            " | self::w:drawing"
+            " | self::w:noBreakHyphen"
+            " | self::w:ptab"
+            " | self::w:t"
+            " | self::w:tab"
+        )
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/text/paragraph.py b/.venv/lib/python3.12/site-packages/docx/oxml/text/paragraph.py
new file mode 100644
index 00000000..63e96f31
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/text/paragraph.py
@@ -0,0 +1,106 @@
+# pyright: reportPrivateUsage=false
+
+"""Custom element classes related to paragraphs (CT_P)."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, List, cast
+
+from docx.oxml.parser import OxmlElement
+from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrMore, ZeroOrOne
+
+if TYPE_CHECKING:
+    from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
+    from docx.oxml.section import CT_SectPr
+    from docx.oxml.text.hyperlink import CT_Hyperlink
+    from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak
+    from docx.oxml.text.parfmt import CT_PPr
+    from docx.oxml.text.run import CT_R
+
+
+class CT_P(BaseOxmlElement):
+    """`<w:p>` element, containing the properties and text for a paragraph."""
+
+    add_r: Callable[[], CT_R]
+    get_or_add_pPr: Callable[[], CT_PPr]
+    hyperlink_lst: List[CT_Hyperlink]
+    r_lst: List[CT_R]
+
+    pPr: CT_PPr | None = ZeroOrOne("w:pPr")  # pyright: ignore[reportAssignmentType]
+    hyperlink = ZeroOrMore("w:hyperlink")
+    r = ZeroOrMore("w:r")
+
+    def add_p_before(self) -> CT_P:
+        """Return a new `<w:p>` element inserted directly prior to this one."""
+        new_p = cast(CT_P, OxmlElement("w:p"))
+        self.addprevious(new_p)
+        return new_p
+
+    @property
+    def alignment(self) -> WD_PARAGRAPH_ALIGNMENT | None:
+        """The value of the `<w:jc>` grandchild element or |None| if not present."""
+        pPr = self.pPr
+        if pPr is None:
+            return None
+        return pPr.jc_val
+
+    @alignment.setter
+    def alignment(self, value: WD_PARAGRAPH_ALIGNMENT):
+        pPr = self.get_or_add_pPr()
+        pPr.jc_val = value
+
+    def clear_content(self):
+        """Remove all child elements, except the `<w:pPr>` element if present."""
+        for child in self.xpath("./*[not(self::w:pPr)]"):
+            self.remove(child)
+
+    @property
+    def inner_content_elements(self) -> List[CT_R | CT_Hyperlink]:
+        """Run and hyperlink children of the `w:p` element, in document order."""
+        return self.xpath("./w:r | ./w:hyperlink")
+
+    @property
+    def lastRenderedPageBreaks(self) -> List[CT_LastRenderedPageBreak]:
+        """All `w:lastRenderedPageBreak` descendants of this paragraph.
+
+        Rendered page-breaks commonly occur in a run but can also occur in a run inside
+        a hyperlink. This returns both.
+        """
+        return self.xpath(
+            "./w:r/w:lastRenderedPageBreak | ./w:hyperlink/w:r/w:lastRenderedPageBreak"
+        )
+
+    def set_sectPr(self, sectPr: CT_SectPr):
+        """Unconditionally replace or add `sectPr` as grandchild in correct sequence."""
+        pPr = self.get_or_add_pPr()
+        pPr._remove_sectPr()
+        pPr._insert_sectPr(sectPr)
+
+    @property
+    def style(self) -> str | None:
+        """String contained in `w:val` attribute of `./w:pPr/w:pStyle` grandchild.
+
+        |None| if not present.
+        """
+        pPr = self.pPr
+        if pPr is None:
+            return None
+        return pPr.style
+
+    @style.setter
+    def style(self, style: str | None):
+        pPr = self.get_or_add_pPr()
+        pPr.style = style
+
+    @property
+    def text(self):  # pyright: ignore[reportIncompatibleMethodOverride]
+        """The textual content of this paragraph.
+
+        Inner-content child elements like `w:r` and `w:hyperlink` are translated to
+        their text equivalent.
+        """
+        return "".join(e.text for e in self.xpath("w:r | w:hyperlink"))
+
+    def _insert_pPr(self, pPr: CT_PPr) -> CT_PPr:
+        self.insert(0, pPr)
+        return pPr
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/text/parfmt.py b/.venv/lib/python3.12/site-packages/docx/oxml/text/parfmt.py
new file mode 100644
index 00000000..de560963
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/text/parfmt.py
@@ -0,0 +1,387 @@
+"""Custom element classes related to paragraph properties (CT_PPr)."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable
+
+from docx.enum.text import (
+    WD_ALIGN_PARAGRAPH,
+    WD_LINE_SPACING,
+    WD_TAB_ALIGNMENT,
+    WD_TAB_LEADER,
+)
+from docx.oxml.simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure
+from docx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OneOrMore,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrOne,
+)
+from docx.shared import Length
+
+if TYPE_CHECKING:
+    from docx.oxml.section import CT_SectPr
+    from docx.oxml.shared import CT_String
+
+
+class CT_Ind(BaseOxmlElement):
+    """``<w:ind>`` element, specifying paragraph indentation."""
+
+    left: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:left", ST_SignedTwipsMeasure
+    )
+    right: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:right", ST_SignedTwipsMeasure
+    )
+    firstLine: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:firstLine", ST_TwipsMeasure
+    )
+    hanging: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:hanging", ST_TwipsMeasure
+    )
+
+
+class CT_Jc(BaseOxmlElement):
+    """``<w:jc>`` element, specifying paragraph justification."""
+
+    val: WD_ALIGN_PARAGRAPH = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:val", WD_ALIGN_PARAGRAPH
+    )
+
+
+class CT_PPr(BaseOxmlElement):
+    """``<w:pPr>`` element, containing the properties for a paragraph."""
+
+    get_or_add_ind: Callable[[], CT_Ind]
+    get_or_add_pStyle: Callable[[], CT_String]
+    _insert_sectPr: Callable[[CT_SectPr], None]
+    _remove_pStyle: Callable[[], None]
+    _remove_sectPr: Callable[[], None]
+
+    _tag_seq = (
+        "w:pStyle",
+        "w:keepNext",
+        "w:keepLines",
+        "w:pageBreakBefore",
+        "w:framePr",
+        "w:widowControl",
+        "w:numPr",
+        "w:suppressLineNumbers",
+        "w:pBdr",
+        "w:shd",
+        "w:tabs",
+        "w:suppressAutoHyphens",
+        "w:kinsoku",
+        "w:wordWrap",
+        "w:overflowPunct",
+        "w:topLinePunct",
+        "w:autoSpaceDE",
+        "w:autoSpaceDN",
+        "w:bidi",
+        "w:adjustRightInd",
+        "w:snapToGrid",
+        "w:spacing",
+        "w:ind",
+        "w:contextualSpacing",
+        "w:mirrorIndents",
+        "w:suppressOverlap",
+        "w:jc",
+        "w:textDirection",
+        "w:textAlignment",
+        "w:textboxTightWrap",
+        "w:outlineLvl",
+        "w:divId",
+        "w:cnfStyle",
+        "w:rPr",
+        "w:sectPr",
+        "w:pPrChange",
+    )
+    pStyle: CT_String | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:pStyle", successors=_tag_seq[1:]
+    )
+    keepNext = ZeroOrOne("w:keepNext", successors=_tag_seq[2:])
+    keepLines = ZeroOrOne("w:keepLines", successors=_tag_seq[3:])
+    pageBreakBefore = ZeroOrOne("w:pageBreakBefore", successors=_tag_seq[4:])
+    widowControl = ZeroOrOne("w:widowControl", successors=_tag_seq[6:])
+    numPr = ZeroOrOne("w:numPr", successors=_tag_seq[7:])
+    tabs = ZeroOrOne("w:tabs", successors=_tag_seq[11:])
+    spacing = ZeroOrOne("w:spacing", successors=_tag_seq[22:])
+    ind: CT_Ind | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "w:ind", successors=_tag_seq[23:]
+    )
+    jc = ZeroOrOne("w:jc", successors=_tag_seq[27:])
+    sectPr = ZeroOrOne("w:sectPr", successors=_tag_seq[35:])
+    del _tag_seq
+
+    @property
+    def first_line_indent(self) -> Length | None:
+        """A |Length| value calculated from the values of `w:ind/@w:firstLine` and
+        `w:ind/@w:hanging`.
+
+        Returns |None| if the `w:ind` child is not present.
+        """
+        ind = self.ind
+        if ind is None:
+            return None
+        hanging = ind.hanging
+        if hanging is not None:
+            return Length(-hanging)
+        firstLine = ind.firstLine
+        if firstLine is None:
+            return None
+        return firstLine
+
+    @first_line_indent.setter
+    def first_line_indent(self, value: Length | None):
+        if self.ind is None and value is None:
+            return
+        ind = self.get_or_add_ind()
+        ind.firstLine = ind.hanging = None
+        if value is None:
+            return
+        elif value < 0:
+            ind.hanging = -value
+        else:
+            ind.firstLine = value
+
+    @property
+    def ind_left(self) -> Length | None:
+        """The value of `w:ind/@w:left` or |None| if not present."""
+        ind = self.ind
+        if ind is None:
+            return None
+        return ind.left
+
+    @ind_left.setter
+    def ind_left(self, value: Length | None):
+        if value is None and self.ind is None:
+            return
+        ind = self.get_or_add_ind()
+        ind.left = value
+
+    @property
+    def ind_right(self) -> Length | None:
+        """The value of `w:ind/@w:right` or |None| if not present."""
+        ind = self.ind
+        if ind is None:
+            return None
+        return ind.right
+
+    @ind_right.setter
+    def ind_right(self, value: Length | None):
+        if value is None and self.ind is None:
+            return
+        ind = self.get_or_add_ind()
+        ind.right = value
+
+    @property
+    def jc_val(self) -> WD_ALIGN_PARAGRAPH | None:
+        """Value of the `<w:jc>` child element or |None| if not present."""
+        return self.jc.val if self.jc is not None else None
+
+    @jc_val.setter
+    def jc_val(self, value):
+        if value is None:
+            self._remove_jc()
+            return
+        self.get_or_add_jc().val = value
+
+    @property
+    def keepLines_val(self):
+        """The value of `keepLines/@val` or |None| if not present."""
+        keepLines = self.keepLines
+        if keepLines is None:
+            return None
+        return keepLines.val
+
+    @keepLines_val.setter
+    def keepLines_val(self, value):
+        if value is None:
+            self._remove_keepLines()
+        else:
+            self.get_or_add_keepLines().val = value
+
+    @property
+    def keepNext_val(self):
+        """The value of `keepNext/@val` or |None| if not present."""
+        keepNext = self.keepNext
+        if keepNext is None:
+            return None
+        return keepNext.val
+
+    @keepNext_val.setter
+    def keepNext_val(self, value):
+        if value is None:
+            self._remove_keepNext()
+        else:
+            self.get_or_add_keepNext().val = value
+
+    @property
+    def pageBreakBefore_val(self):
+        """The value of `pageBreakBefore/@val` or |None| if not present."""
+        pageBreakBefore = self.pageBreakBefore
+        if pageBreakBefore is None:
+            return None
+        return pageBreakBefore.val
+
+    @pageBreakBefore_val.setter
+    def pageBreakBefore_val(self, value):
+        if value is None:
+            self._remove_pageBreakBefore()
+        else:
+            self.get_or_add_pageBreakBefore().val = value
+
+    @property
+    def spacing_after(self):
+        """The value of `w:spacing/@w:after` or |None| if not present."""
+        spacing = self.spacing
+        if spacing is None:
+            return None
+        return spacing.after
+
+    @spacing_after.setter
+    def spacing_after(self, value):
+        if value is None and self.spacing is None:
+            return
+        self.get_or_add_spacing().after = value
+
+    @property
+    def spacing_before(self):
+        """The value of `w:spacing/@w:before` or |None| if not present."""
+        spacing = self.spacing
+        if spacing is None:
+            return None
+        return spacing.before
+
+    @spacing_before.setter
+    def spacing_before(self, value):
+        if value is None and self.spacing is None:
+            return
+        self.get_or_add_spacing().before = value
+
+    @property
+    def spacing_line(self):
+        """The value of `w:spacing/@w:line` or |None| if not present."""
+        spacing = self.spacing
+        if spacing is None:
+            return None
+        return spacing.line
+
+    @spacing_line.setter
+    def spacing_line(self, value):
+        if value is None and self.spacing is None:
+            return
+        self.get_or_add_spacing().line = value
+
+    @property
+    def spacing_lineRule(self):
+        """The value of `w:spacing/@w:lineRule` as a member of the :ref:`WdLineSpacing`
+        enumeration.
+
+        Only the `MULTIPLE`, `EXACTLY`, and `AT_LEAST` members are used. It is the
+        responsibility of the client to calculate the use of `SINGLE`, `DOUBLE`, and
+        `MULTIPLE` based on the value of `w:spacing/@w:line` if that behavior is
+        desired.
+        """
+        spacing = self.spacing
+        if spacing is None:
+            return None
+        lineRule = spacing.lineRule
+        if lineRule is None and spacing.line is not None:
+            return WD_LINE_SPACING.MULTIPLE
+        return lineRule
+
+    @spacing_lineRule.setter
+    def spacing_lineRule(self, value):
+        if value is None and self.spacing is None:
+            return
+        self.get_or_add_spacing().lineRule = value
+
+    @property
+    def style(self) -> str | None:
+        """String contained in `./w:pStyle/@val`, or None if child is not present."""
+        pStyle = self.pStyle
+        if pStyle is None:
+            return None
+        return pStyle.val
+
+    @style.setter
+    def style(self, style: str | None):
+        """Set `./w:pStyle/@val` `style`, adding a new element if necessary.
+
+        If `style` is |None|, remove `./w:pStyle` when present.
+        """
+        if style is None:
+            self._remove_pStyle()
+            return
+        pStyle = self.get_or_add_pStyle()
+        pStyle.val = style
+
+    @property
+    def widowControl_val(self):
+        """The value of `widowControl/@val` or |None| if not present."""
+        widowControl = self.widowControl
+        if widowControl is None:
+            return None
+        return widowControl.val
+
+    @widowControl_val.setter
+    def widowControl_val(self, value):
+        if value is None:
+            self._remove_widowControl()
+        else:
+            self.get_or_add_widowControl().val = value
+
+
+class CT_Spacing(BaseOxmlElement):
+    """``<w:spacing>`` element, specifying paragraph spacing attributes such as space
+    before and line spacing."""
+
+    after = OptionalAttribute("w:after", ST_TwipsMeasure)
+    before = OptionalAttribute("w:before", ST_TwipsMeasure)
+    line = OptionalAttribute("w:line", ST_SignedTwipsMeasure)
+    lineRule = OptionalAttribute("w:lineRule", WD_LINE_SPACING)
+
+
+class CT_TabStop(BaseOxmlElement):
+    """`<w:tab>` element, representing an individual tab stop.
+
+    Overloaded to use for a tab-character in a run, which also uses the w:tab tag but
+    only needs a __str__ method.
+    """
+
+    val: WD_TAB_ALIGNMENT = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:val", WD_TAB_ALIGNMENT
+    )
+    leader: WD_TAB_LEADER | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:leader", WD_TAB_LEADER, default=WD_TAB_LEADER.SPACES
+    )
+    pos: Length = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:pos", ST_SignedTwipsMeasure
+    )
+
+    def __str__(self) -> str:
+        """Text equivalent of a `w:tab` element appearing in a run.
+
+        Allows text of run inner-content to be accessed consistently across all text
+        inner-content.
+        """
+        return "\t"
+
+
+class CT_TabStops(BaseOxmlElement):
+    """``<w:tabs>`` element, container for a sorted sequence of tab stops."""
+
+    tab = OneOrMore("w:tab", successors=())
+
+    def insert_tab_in_order(self, pos, align, leader):
+        """Insert a newly created `w:tab` child element in `pos` order."""
+        new_tab = self._new_tab()
+        new_tab.pos, new_tab.val, new_tab.leader = pos, align, leader
+        for tab in self.tab_lst:
+            if new_tab.pos < tab.pos:
+                tab.addprevious(new_tab)
+                return new_tab
+        self.append(new_tab)
+        return new_tab
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/text/run.py b/.venv/lib/python3.12/site-packages/docx/oxml/text/run.py
new file mode 100644
index 00000000..88efae83
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/text/run.py
@@ -0,0 +1,276 @@
+"""Custom element classes related to text runs (CT_R)."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, Iterator, List
+
+from docx.oxml.drawing import CT_Drawing
+from docx.oxml.ns import qn
+from docx.oxml.simpletypes import ST_BrClear, ST_BrType
+from docx.oxml.text.font import CT_RPr
+from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, ZeroOrMore, ZeroOrOne
+from docx.shared import TextAccumulator
+
+if TYPE_CHECKING:
+    from docx.oxml.shape import CT_Anchor, CT_Inline
+    from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak
+    from docx.oxml.text.parfmt import CT_TabStop
+
+# ------------------------------------------------------------------------------------
+# Run-level elements
+
+
+class CT_R(BaseOxmlElement):
+    """`<w:r>` element, containing the properties and text for a run."""
+
+    add_br: Callable[[], CT_Br]
+    add_tab: Callable[[], CT_TabStop]
+    get_or_add_rPr: Callable[[], CT_RPr]
+    _add_drawing: Callable[[], CT_Drawing]
+    _add_t: Callable[..., CT_Text]
+
+    rPr: CT_RPr | None = ZeroOrOne("w:rPr")  # pyright: ignore[reportAssignmentType]
+    br = ZeroOrMore("w:br")
+    cr = ZeroOrMore("w:cr")
+    drawing = ZeroOrMore("w:drawing")
+    t = ZeroOrMore("w:t")
+    tab = ZeroOrMore("w:tab")
+
+    def add_t(self, text: str) -> CT_Text:
+        """Return a newly added `<w:t>` element containing `text`."""
+        t = self._add_t(text=text)
+        if len(text.strip()) < len(text):
+            t.set(qn("xml:space"), "preserve")
+        return t
+
+    def add_drawing(self, inline_or_anchor: CT_Inline | CT_Anchor) -> CT_Drawing:
+        """Return newly appended `CT_Drawing` (`w:drawing`) child element.
+
+        The `w:drawing` element has `inline_or_anchor` as its child.
+        """
+        drawing = self._add_drawing()
+        drawing.append(inline_or_anchor)
+        return drawing
+
+    def clear_content(self) -> None:
+        """Remove all child elements except a `w:rPr` element if present."""
+        # -- remove all run inner-content except a `w:rPr` when present. --
+        for e in self.xpath("./*[not(self::w:rPr)]"):
+            self.remove(e)
+
+    @property
+    def inner_content_items(self) -> List[str | CT_Drawing | CT_LastRenderedPageBreak]:
+        """Text of run, possibly punctuated by `w:lastRenderedPageBreak` elements."""
+        from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak
+
+        accum = TextAccumulator()
+
+        def iter_items() -> Iterator[str | CT_Drawing | CT_LastRenderedPageBreak]:
+            for e in self.xpath(
+                "w:br"
+                " | w:cr"
+                " | w:drawing"
+                " | w:lastRenderedPageBreak"
+                " | w:noBreakHyphen"
+                " | w:ptab"
+                " | w:t"
+                " | w:tab"
+            ):
+                if isinstance(e, (CT_Drawing, CT_LastRenderedPageBreak)):
+                    yield from accum.pop()
+                    yield e
+                else:
+                    accum.push(str(e))
+
+            # -- don't forget the "tail" string --
+            yield from accum.pop()
+
+        return list(iter_items())
+
+    @property
+    def lastRenderedPageBreaks(self) -> List[CT_LastRenderedPageBreak]:
+        """All `w:lastRenderedPageBreaks` descendants of this run."""
+        return self.xpath("./w:lastRenderedPageBreak")
+
+    @property
+    def style(self) -> str | None:
+        """String contained in `w:val` attribute of `w:rStyle` grandchild.
+
+        |None| if that element is not present.
+        """
+        rPr = self.rPr
+        if rPr is None:
+            return None
+        return rPr.style
+
+    @style.setter
+    def style(self, style: str | None):
+        """Set character style of this `w:r` element to `style`.
+
+        If `style` is None, remove the style element.
+        """
+        rPr = self.get_or_add_rPr()
+        rPr.style = style
+
+    @property
+    def text(self) -> str:
+        """The textual content of this run.
+
+        Inner-content child elements like `w:tab` are translated to their text
+        equivalent.
+        """
+        return "".join(
+            str(e) for e in self.xpath("w:br | w:cr | w:noBreakHyphen | w:ptab | w:t | w:tab")
+        )
+
+    @text.setter
+    def text(self, text: str):  # pyright: ignore[reportIncompatibleMethodOverride]
+        self.clear_content()
+        _RunContentAppender.append_to_run_from_text(self, text)
+
+    def _insert_rPr(self, rPr: CT_RPr) -> CT_RPr:
+        self.insert(0, rPr)
+        return rPr
+
+
+# ------------------------------------------------------------------------------------
+# Run inner-content elements
+
+
+class CT_Br(BaseOxmlElement):
+    """`<w:br>` element, indicating a line, page, or column break in a run."""
+
+    type: str | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w:type", ST_BrType, default="textWrapping"
+    )
+    clear: str | None = OptionalAttribute("w:clear", ST_BrClear)  # pyright: ignore
+
+    def __str__(self) -> str:
+        """Text equivalent of this element. Actual value depends on break type.
+
+        A line break is translated as "\n". Column and page breaks produce the empty
+        string ("").
+
+        This allows the text of run inner-content to be accessed in a consistent way
+        for all run inner-context text elements.
+        """
+        return "\n" if self.type == "textWrapping" else ""
+
+
+class CT_Cr(BaseOxmlElement):
+    """`<w:cr>` element, representing a carriage-return (0x0D) character within a run.
+
+    In Word, this represents a "soft carriage-return" in the sense that it does not end
+    the paragraph the way pressing Enter (aka. Return) on the keyboard does. Here the
+    text equivalent is considered to be newline ("\n") since in plain-text that's the
+    closest Python equivalent.
+
+    NOTE: this complex-type name does not exist in the schema, where `w:tab` maps to
+    `CT_Empty`. This name was added to give it distinguished behavior. CT_Empty is used
+    for many elements.
+    """
+
+    def __str__(self) -> str:
+        """Text equivalent of this element, a single newline ("\n")."""
+        return "\n"
+
+
+class CT_NoBreakHyphen(BaseOxmlElement):
+    """`<w:noBreakHyphen>` element, a hyphen ineligible for a line-wrap position.
+
+    This maps to a plain-text dash ("-").
+
+    NOTE: this complex-type name does not exist in the schema, where `w:noBreakHyphen`
+    maps to `CT_Empty`. This name was added to give it behavior distinguished from the
+    many other elements represented in the schema by CT_Empty.
+    """
+
+    def __str__(self) -> str:
+        """Text equivalent of this element, a single dash character ("-")."""
+        return "-"
+
+
+class CT_PTab(BaseOxmlElement):
+    """`<w:ptab>` element, representing an absolute-position tab character within a run.
+
+    This character advances the rendering position to the specified position regardless
+    of any tab-stops, perhaps for layout of a table-of-contents (TOC) or similar.
+    """
+
+    def __str__(self) -> str:
+        """Text equivalent of this element, a single tab ("\t") character.
+
+        This allows the text of run inner-content to be accessed in a consistent way
+        for all run inner-context text elements.
+        """
+        return "\t"
+
+
+# -- CT_Tab functionality is provided by CT_TabStop which also uses `w:tab` tag. That
+# -- element class provides the __str__() method for this empty element, unconditionally
+# -- returning "\t".
+
+
+class CT_Text(BaseOxmlElement):
+    """`<w:t>` element, containing a sequence of characters within a run."""
+
+    def __str__(self) -> str:
+        """Text contained in this element, the empty string if it has no content.
+
+        This property allows this run inner-content element to be queried for its text
+        the same way as other run-content elements are. In particular, this never
+        returns None, as etree._Element does when there is no content.
+        """
+        return self.text or ""
+
+
+# ------------------------------------------------------------------------------------
+# Utility
+
+
+class _RunContentAppender:
+    """Translates a Python string into run content elements appended in a `w:r` element.
+
+    Contiguous sequences of regular characters are appended in a single `<w:t>` element.
+    Each tab character ('\t') causes a `<w:tab/>` element to be appended. Likewise a
+    newline or carriage return character ('\n', '\r') causes a `<w:cr>` element to be
+    appended.
+    """
+
+    def __init__(self, r: CT_R):
+        self._r = r
+        self._bfr: List[str] = []
+
+    @classmethod
+    def append_to_run_from_text(cls, r: CT_R, text: str):
+        """Append inner-content elements for `text` to `r` element."""
+        appender = cls(r)
+        appender.add_text(text)
+
+    def add_text(self, text: str):
+        """Append inner-content elements for `text` to the `w:r` element."""
+        for char in text:
+            self.add_char(char)
+        self.flush()
+
+    def add_char(self, char: str):
+        """Process next character of input through finite state maching (FSM).
+
+        There are two possible states, buffer pending and not pending, but those are
+        hidden behind the `.flush()` method which must be called at the end of text to
+        ensure any pending `<w:t>` element is written.
+        """
+        if char == "\t":
+            self.flush()
+            self._r.add_tab()
+        elif char in "\r\n":
+            self.flush()
+            self._r.add_br()
+        else:
+            self._bfr.append(char)
+
+    def flush(self):
+        text = "".join(self._bfr)
+        if text:
+            self._r.add_t(text)
+        self._bfr.clear()
diff --git a/.venv/lib/python3.12/site-packages/docx/oxml/xmlchemy.py b/.venv/lib/python3.12/site-packages/docx/oxml/xmlchemy.py
new file mode 100644
index 00000000..077bcd58
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/oxml/xmlchemy.py
@@ -0,0 +1,709 @@
+# pyright: reportImportCycles=false
+
+"""Enabling declarative definition of lxml custom element classes."""
+
+from __future__ import annotations
+
+import re
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Callable,
+    Dict,
+    List,
+    Sequence,
+    Tuple,
+    Type,
+    TypeVar,
+)
+
+from lxml import etree
+from lxml.etree import ElementBase, _Element  # pyright: ignore[reportPrivateUsage]
+
+from docx.oxml.exceptions import InvalidXmlError
+from docx.oxml.ns import NamespacePrefixedTag, nsmap, qn
+from docx.shared import lazyproperty
+
+if TYPE_CHECKING:
+    from docx.enum.base import BaseXmlEnum
+    from docx.oxml.simpletypes import BaseSimpleType
+
+
+def serialize_for_reading(element: ElementBase):
+    """Serialize `element` to human-readable XML suitable for tests.
+
+    No XML declaration.
+    """
+    xml = etree.tostring(element, encoding="unicode", pretty_print=True)
+    return XmlString(xml)
+
+
+class XmlString(str):
+    """Provides string comparison override suitable for serialized XML that is useful
+    for tests."""
+
+    # '    <w:xyz xmlns:a="http://ns/decl/a" attr_name="val">text</w:xyz>'
+    # |          |                                          ||           |
+    # +----------+------------------------------------------++-----------+
+    #  front      attrs                                     | text
+    #                                                     close
+
+    _xml_elm_line_patt = re.compile(r"( *</?[\w:]+)(.*?)(/?>)([^<]*</[\w:]+>)?$")
+
+    def __eq__(self, other: object) -> bool:
+        if not isinstance(other, str):
+            return False
+        lines = self.splitlines()
+        lines_other = other.splitlines()
+        if len(lines) != len(lines_other):
+            return False
+        for line, line_other in zip(lines, lines_other):
+            if not self._eq_elm_strs(line, line_other):
+                return False
+        return True
+
+    def __ne__(self, other: object) -> bool:
+        return not self.__eq__(other)
+
+    def _attr_seq(self, attrs: str) -> List[str]:
+        """Return a sequence of attribute strings parsed from `attrs`.
+
+        Each attribute string is stripped of whitespace on both ends.
+        """
+        attrs = attrs.strip()
+        attr_lst = attrs.split()
+        return sorted(attr_lst)
+
+    def _eq_elm_strs(self, line: str, line_2: str):
+        """Return True if the element in `line_2` is XML equivalent to the element in
+        `line`."""
+        front, attrs, close, text = self._parse_line(line)
+        front_2, attrs_2, close_2, text_2 = self._parse_line(line_2)
+        if front != front_2:
+            return False
+        if self._attr_seq(attrs) != self._attr_seq(attrs_2):
+            return False
+        if close != close_2:
+            return False
+        if text != text_2:
+            return False
+        return True
+
+    @classmethod
+    def _parse_line(cls, line: str) -> Tuple[str, str, str, str]:
+        """(front, attrs, close, text) 4-tuple result of parsing XML element `line`."""
+        match = cls._xml_elm_line_patt.match(line)
+        if match is None:
+            return "", "", "", ""
+        front, attrs, close, text = [match.group(n) for n in range(1, 5)]
+        return front, attrs, close, text
+
+
+_T = TypeVar("_T")
+
+
+class MetaOxmlElement(type):
+    """Metaclass for BaseOxmlElement."""
+
+    def __init__(cls, clsname: str, bases: Tuple[type, ...], namespace: Dict[str, Any]):
+        dispatchable = (
+            OneAndOnlyOne,
+            OneOrMore,
+            OptionalAttribute,
+            RequiredAttribute,
+            ZeroOrMore,
+            ZeroOrOne,
+            ZeroOrOneChoice,
+        )
+        for key, value in namespace.items():
+            if isinstance(value, dispatchable):
+                value.populate_class_members(cls, key)
+
+
+class BaseAttribute:
+    """Base class for OptionalAttribute and RequiredAttribute.
+
+    Provides common methods.
+    """
+
+    def __init__(self, attr_name: str, simple_type: Type[BaseXmlEnum] | Type[BaseSimpleType]):
+        super(BaseAttribute, self).__init__()
+        self._attr_name = attr_name
+        self._simple_type = simple_type
+
+    def populate_class_members(self, element_cls: MetaOxmlElement, prop_name: str) -> None:
+        """Add the appropriate methods to `element_cls`."""
+        self._element_cls = element_cls
+        self._prop_name = prop_name
+
+        self._add_attr_property()
+
+    def _add_attr_property(self):
+        """Add a read/write `.{prop_name}` property to the element class.
+
+        The property returns the interpreted value of this attribute on access and
+        changes the attribute value to its ST_* counterpart on assignment.
+        """
+        property_ = property(self._getter, self._setter, None)
+        # -- assign unconditionally to overwrite element name definition --
+        setattr(self._element_cls, self._prop_name, property_)
+
+    @property
+    def _clark_name(self):
+        if ":" in self._attr_name:
+            return qn(self._attr_name)
+        return self._attr_name
+
+    @property
+    def _getter(self) -> Callable[[BaseOxmlElement], Any | None]: ...
+
+    @property
+    def _setter(
+        self,
+    ) -> Callable[[BaseOxmlElement, Any | None], None]: ...
+
+
+class OptionalAttribute(BaseAttribute):
+    """Defines an optional attribute on a custom element class.
+
+    An optional attribute returns a default value when not present for reading. When
+    assigned |None|, the attribute is removed, but still returns the default value when
+    one is specified.
+    """
+
+    def __init__(
+        self,
+        attr_name: str,
+        simple_type: Type[BaseXmlEnum] | Type[BaseSimpleType],
+        default: BaseXmlEnum | BaseSimpleType | str | bool | None = None,
+    ):
+        super(OptionalAttribute, self).__init__(attr_name, simple_type)
+        self._default = default
+
+    @property
+    def _docstring(self):
+        """String to use as `__doc__` attribute of attribute property."""
+        return (
+            f"{self._simple_type.__name__} type-converted value of"
+            f" ``{self._attr_name}`` attribute, or |None| (or specified default"
+            f" value) if not present. Assigning the default value causes the"
+            f" attribute to be removed from the element."
+        )
+
+    @property
+    def _getter(
+        self,
+    ) -> Callable[[BaseOxmlElement], Any | None]:
+        """Function suitable for `__get__()` method on attribute property descriptor."""
+
+        def get_attr_value(
+            obj: BaseOxmlElement,
+        ) -> Any | None:
+            attr_str_value = obj.get(self._clark_name)
+            if attr_str_value is None:
+                return self._default
+            return self._simple_type.from_xml(attr_str_value)
+
+        get_attr_value.__doc__ = self._docstring
+        return get_attr_value
+
+    @property
+    def _setter(self) -> Callable[[BaseOxmlElement, Any], None]:
+        """Function suitable for `__set__()` method on attribute property descriptor."""
+
+        def set_attr_value(obj: BaseOxmlElement, value: Any | None):
+            if value is None or value == self._default:
+                if self._clark_name in obj.attrib:
+                    del obj.attrib[self._clark_name]
+                return
+            str_value = self._simple_type.to_xml(value)
+            if str_value is None:
+                if self._clark_name in obj.attrib:
+                    del obj.attrib[self._clark_name]
+                return
+            obj.set(self._clark_name, str_value)
+
+        return set_attr_value
+
+
+class RequiredAttribute(BaseAttribute):
+    """Defines a required attribute on a custom element class.
+
+    A required attribute is assumed to be present for reading, so does not have a
+    default value; its actual value is always used. If missing on read, an
+    |InvalidXmlError| is raised. It also does not remove the attribute if |None| is
+    assigned. Assigning |None| raises |TypeError| or |ValueError|, depending on the
+    simple type of the attribute.
+    """
+
+    @property
+    def _docstring(self):
+        """Return the string to use as the ``__doc__`` attribute of the property for
+        this attribute."""
+        return "%s type-converted value of ``%s`` attribute." % (
+            self._simple_type.__name__,
+            self._attr_name,
+        )
+
+    @property
+    def _getter(self) -> Callable[[BaseOxmlElement], Any]:
+        """function object suitable for "get" side of attr property descriptor."""
+
+        def get_attr_value(obj: BaseOxmlElement) -> Any | None:
+            attr_str_value = obj.get(self._clark_name)
+            if attr_str_value is None:
+                raise InvalidXmlError(
+                    "required '%s' attribute not present on element %s" % (self._attr_name, obj.tag)
+                )
+            return self._simple_type.from_xml(attr_str_value)
+
+        get_attr_value.__doc__ = self._docstring
+        return get_attr_value
+
+    @property
+    def _setter(self) -> Callable[[BaseOxmlElement, Any], None]:
+        """function object suitable for "set" side of attribute property descriptor."""
+
+        def set_attr_value(obj: BaseOxmlElement, value: Any):
+            str_value = self._simple_type.to_xml(value)
+            if str_value is None:
+                raise ValueError(f"cannot assign {value} to this required attribute")
+            obj.set(self._clark_name, str_value)
+
+        return set_attr_value
+
+
+class _BaseChildElement:
+    """Base class for the child-element classes.
+
+    The child-element sub-classes correspond to varying cardinalities, such as ZeroOrOne
+    and ZeroOrMore.
+    """
+
+    def __init__(self, nsptagname: str, successors: Tuple[str, ...] = ()):
+        super(_BaseChildElement, self).__init__()
+        self._nsptagname = nsptagname
+        self._successors = successors
+
+    def populate_class_members(self, element_cls: MetaOxmlElement, prop_name: str) -> None:
+        """Baseline behavior for adding the appropriate methods to `element_cls`."""
+        self._element_cls = element_cls
+        self._prop_name = prop_name
+
+    def _add_adder(self):
+        """Add an ``_add_x()`` method to the element class for this child element."""
+
+        def _add_child(obj: BaseOxmlElement, **attrs: Any):
+            new_method = getattr(obj, self._new_method_name)
+            child = new_method()
+            for key, value in attrs.items():
+                setattr(child, key, value)
+            insert_method = getattr(obj, self._insert_method_name)
+            insert_method(child)
+            return child
+
+        _add_child.__doc__ = (
+            "Add a new ``<%s>`` child element unconditionally, inserted in t"
+            "he correct sequence." % self._nsptagname
+        )
+        self._add_to_class(self._add_method_name, _add_child)
+
+    def _add_creator(self):
+        """Add a ``_new_{prop_name}()`` method to the element class that creates a new,
+        empty element of the correct type, having no attributes."""
+        creator = self._creator
+        creator.__doc__ = (
+            'Return a "loose", newly created ``<%s>`` element having no attri'
+            "butes, text, or children." % self._nsptagname
+        )
+        self._add_to_class(self._new_method_name, creator)
+
+    def _add_getter(self):
+        """Add a read-only ``{prop_name}`` property to the element class for this child
+        element."""
+        property_ = property(self._getter, None, None)
+        # -- assign unconditionally to overwrite element name definition --
+        setattr(self._element_cls, self._prop_name, property_)
+
+    def _add_inserter(self):
+        """Add an ``_insert_x()`` method to the element class for this child element."""
+
+        def _insert_child(obj: BaseOxmlElement, child: BaseOxmlElement):
+            obj.insert_element_before(child, *self._successors)
+            return child
+
+        _insert_child.__doc__ = (
+            "Return the passed ``<%s>`` element after inserting it as a chil"
+            "d in the correct sequence." % self._nsptagname
+        )
+        self._add_to_class(self._insert_method_name, _insert_child)
+
+    def _add_list_getter(self):
+        """Add a read-only ``{prop_name}_lst`` property to the element class to retrieve
+        a list of child elements matching this type."""
+        prop_name = "%s_lst" % self._prop_name
+        property_ = property(self._list_getter, None, None)
+        setattr(self._element_cls, prop_name, property_)
+
+    @lazyproperty
+    def _add_method_name(self):
+        return "_add_%s" % self._prop_name
+
+    def _add_public_adder(self):
+        """Add a public ``add_x()`` method to the parent element class."""
+
+        def add_child(obj: BaseOxmlElement):
+            private_add_method = getattr(obj, self._add_method_name)
+            child = private_add_method()
+            return child
+
+        add_child.__doc__ = (
+            "Add a new ``<%s>`` child element unconditionally, inserted in t"
+            "he correct sequence." % self._nsptagname
+        )
+        self._add_to_class(self._public_add_method_name, add_child)
+
+    def _add_to_class(self, name: str, method: Callable[..., Any]):
+        """Add `method` to the target class as `name`, unless `name` is already defined
+        on the class."""
+        if hasattr(self._element_cls, name):
+            return
+        setattr(self._element_cls, name, method)
+
+    @property
+    def _creator(self) -> Callable[[BaseOxmlElement], BaseOxmlElement]:
+        """Callable that creates an empty element of the right type, with no attrs."""
+        from docx.oxml.parser import OxmlElement
+
+        def new_child_element(obj: BaseOxmlElement):
+            return OxmlElement(self._nsptagname)
+
+        return new_child_element
+
+    @property
+    def _getter(self):
+        """Return a function object suitable for the "get" side of the property
+        descriptor.
+
+        This default getter returns the child element with matching tag name or |None|
+        if not present.
+        """
+
+        def get_child_element(obj: BaseOxmlElement):
+            return obj.find(qn(self._nsptagname))
+
+        get_child_element.__doc__ = (
+            "``<%s>`` child element or |None| if not present." % self._nsptagname
+        )
+        return get_child_element
+
+    @lazyproperty
+    def _insert_method_name(self):
+        return "_insert_%s" % self._prop_name
+
+    @property
+    def _list_getter(self):
+        """Return a function object suitable for the "get" side of a list property
+        descriptor."""
+
+        def get_child_element_list(obj: BaseOxmlElement):
+            return obj.findall(qn(self._nsptagname))
+
+        get_child_element_list.__doc__ = (
+            "A list containing each of the ``<%s>`` child elements, in the o"
+            "rder they appear." % self._nsptagname
+        )
+        return get_child_element_list
+
+    @lazyproperty
+    def _public_add_method_name(self):
+        """add_childElement() is public API for a repeating element, allowing new
+        elements to be added to the sequence.
+
+        May be overridden to provide a friendlier API to clients having domain
+        appropriate parameter names for required attributes.
+        """
+        return "add_%s" % self._prop_name
+
+    @lazyproperty
+    def _remove_method_name(self):
+        return "_remove_%s" % self._prop_name
+
+    @lazyproperty
+    def _new_method_name(self):
+        return "_new_%s" % self._prop_name
+
+
+class Choice(_BaseChildElement):
+    """Defines a child element belonging to a group, only one of which may appear as a
+    child."""
+
+    @property
+    def nsptagname(self):
+        return self._nsptagname
+
+    def populate_class_members(  # pyright: ignore[reportIncompatibleMethodOverride]
+        self,
+        element_cls: MetaOxmlElement,
+        group_prop_name: str,
+        successors: Tuple[str, ...],
+    ) -> None:
+        """Add the appropriate methods to `element_cls`."""
+        self._element_cls = element_cls
+        self._group_prop_name = group_prop_name
+        self._successors = successors
+
+        self._add_getter()
+        self._add_creator()
+        self._add_inserter()
+        self._add_adder()
+        self._add_get_or_change_to_method()
+
+    def _add_get_or_change_to_method(self):
+        """Add a ``get_or_change_to_x()`` method to the element class for this child
+        element."""
+
+        def get_or_change_to_child(obj: BaseOxmlElement):
+            child = getattr(obj, self._prop_name)
+            if child is not None:
+                return child
+            remove_group_method = getattr(obj, self._remove_group_method_name)
+            remove_group_method()
+            add_method = getattr(obj, self._add_method_name)
+            child = add_method()
+            return child
+
+        get_or_change_to_child.__doc__ = (
+            "Return the ``<%s>`` child, replacing any other group element if" " found."
+        ) % self._nsptagname
+        self._add_to_class(self._get_or_change_to_method_name, get_or_change_to_child)
+
+    @property
+    def _prop_name(self):
+        """Property name computed from tag name, e.g. a:schemeClr -> schemeClr."""
+        start = self._nsptagname.index(":") + 1 if ":" in self._nsptagname else 0
+        return self._nsptagname[start:]
+
+    @lazyproperty
+    def _get_or_change_to_method_name(self):
+        return "get_or_change_to_%s" % self._prop_name
+
+    @lazyproperty
+    def _remove_group_method_name(self):
+        return "_remove_%s" % self._group_prop_name
+
+
+class OneAndOnlyOne(_BaseChildElement):
+    """Defines a required child element for MetaOxmlElement."""
+
+    def __init__(self, nsptagname: str):
+        super(OneAndOnlyOne, self).__init__(nsptagname, ())
+
+    def populate_class_members(self, element_cls: MetaOxmlElement, prop_name: str) -> None:
+        """Add the appropriate methods to `element_cls`."""
+        super(OneAndOnlyOne, self).populate_class_members(element_cls, prop_name)
+        self._add_getter()
+
+    @property
+    def _getter(self):
+        """Return a function object suitable for the "get" side of the property
+        descriptor."""
+
+        def get_child_element(obj: BaseOxmlElement):
+            child = obj.find(qn(self._nsptagname))
+            if child is None:
+                raise InvalidXmlError(
+                    "required ``<%s>`` child element not present" % self._nsptagname
+                )
+            return child
+
+        get_child_element.__doc__ = "Required ``<%s>`` child element." % self._nsptagname
+        return get_child_element
+
+
+class OneOrMore(_BaseChildElement):
+    """Defines a repeating child element for MetaOxmlElement that must appear at least
+    once."""
+
+    def populate_class_members(self, element_cls: MetaOxmlElement, prop_name: str) -> None:
+        """Add the appropriate methods to `element_cls`."""
+        super(OneOrMore, self).populate_class_members(element_cls, prop_name)
+        self._add_list_getter()
+        self._add_creator()
+        self._add_inserter()
+        self._add_adder()
+        self._add_public_adder()
+        delattr(element_cls, prop_name)
+
+
+class ZeroOrMore(_BaseChildElement):
+    """Defines an optional repeating child element for MetaOxmlElement."""
+
+    def populate_class_members(self, element_cls: MetaOxmlElement, prop_name: str) -> None:
+        """Add the appropriate methods to `element_cls`."""
+        super(ZeroOrMore, self).populate_class_members(element_cls, prop_name)
+        self._add_list_getter()
+        self._add_creator()
+        self._add_inserter()
+        self._add_adder()
+        self._add_public_adder()
+        delattr(element_cls, prop_name)
+
+
+class ZeroOrOne(_BaseChildElement):
+    """Defines an optional child element for MetaOxmlElement."""
+
+    def populate_class_members(self, element_cls: MetaOxmlElement, prop_name: str) -> None:
+        """Add the appropriate methods to `element_cls`."""
+        super(ZeroOrOne, self).populate_class_members(element_cls, prop_name)
+        self._add_getter()
+        self._add_creator()
+        self._add_inserter()
+        self._add_adder()
+        self._add_get_or_adder()
+        self._add_remover()
+
+    def _add_get_or_adder(self):
+        """Add a ``get_or_add_x()`` method to the element class for this child
+        element."""
+
+        def get_or_add_child(obj: BaseOxmlElement):
+            child = getattr(obj, self._prop_name)
+            if child is None:
+                add_method = getattr(obj, self._add_method_name)
+                child = add_method()
+            return child
+
+        get_or_add_child.__doc__ = (
+            "Return the ``<%s>`` child element, newly added if not present."
+        ) % self._nsptagname
+        self._add_to_class(self._get_or_add_method_name, get_or_add_child)
+
+    def _add_remover(self):
+        """Add a ``_remove_x()`` method to the element class for this child element."""
+
+        def _remove_child(obj: BaseOxmlElement):
+            obj.remove_all(self._nsptagname)
+
+        _remove_child.__doc__ = ("Remove all ``<%s>`` child elements.") % self._nsptagname
+        self._add_to_class(self._remove_method_name, _remove_child)
+
+    @lazyproperty
+    def _get_or_add_method_name(self):
+        return "get_or_add_%s" % self._prop_name
+
+
+class ZeroOrOneChoice(_BaseChildElement):
+    """Correspondes to an ``EG_*`` element group where at most one of its members may
+    appear as a child."""
+
+    def __init__(self, choices: Sequence[Choice], successors: Tuple[str, ...] = ()):
+        self._choices = choices
+        self._successors = successors
+
+    def populate_class_members(self, element_cls: MetaOxmlElement, prop_name: str) -> None:
+        """Add the appropriate methods to `element_cls`."""
+        super(ZeroOrOneChoice, self).populate_class_members(element_cls, prop_name)
+        self._add_choice_getter()
+        for choice in self._choices:
+            choice.populate_class_members(element_cls, self._prop_name, self._successors)
+        self._add_group_remover()
+
+    def _add_choice_getter(self):
+        """Add a read-only ``{prop_name}`` property to the element class that returns
+        the present member of this group, or |None| if none are present."""
+        property_ = property(self._choice_getter, None, None)
+        # assign unconditionally to overwrite element name definition
+        setattr(self._element_cls, self._prop_name, property_)
+
+    def _add_group_remover(self):
+        """Add a ``_remove_eg_x()`` method to the element class for this choice
+        group."""
+
+        def _remove_choice_group(obj: BaseOxmlElement):
+            for tagname in self._member_nsptagnames:
+                obj.remove_all(tagname)
+
+        _remove_choice_group.__doc__ = "Remove the current choice group child element if present."
+        self._add_to_class(self._remove_choice_group_method_name, _remove_choice_group)
+
+    @property
+    def _choice_getter(self):
+        """Return a function object suitable for the "get" side of the property
+        descriptor."""
+
+        def get_group_member_element(obj: BaseOxmlElement):
+            return obj.first_child_found_in(*self._member_nsptagnames)
+
+        get_group_member_element.__doc__ = (
+            "Return the child element belonging to this element group, or "
+            "|None| if no member child is present."
+        )
+        return get_group_member_element
+
+    @lazyproperty
+    def _member_nsptagnames(self):
+        """Sequence of namespace-prefixed tagnames, one for each of the member elements
+        of this choice group."""
+        return [choice.nsptagname for choice in self._choices]
+
+    @lazyproperty
+    def _remove_choice_group_method_name(self):
+        return "_remove_%s" % self._prop_name
+
+
+# -- lxml typing isn't quite right here, just ignore this error on _Element --
+class BaseOxmlElement(etree.ElementBase, metaclass=MetaOxmlElement):
+    """Effective base class for all custom element classes.
+
+    Adds standardized behavior to all classes in one place.
+    """
+
+    def __repr__(self):
+        return "<%s '<%s>' at 0x%0x>" % (
+            self.__class__.__name__,
+            self._nsptag,
+            id(self),
+        )
+
+    def first_child_found_in(self, *tagnames: str) -> _Element | None:
+        """First child with tag in `tagnames`, or None if not found."""
+        for tagname in tagnames:
+            child = self.find(qn(tagname))
+            if child is not None:
+                return child
+        return None
+
+    def insert_element_before(self, elm: ElementBase, *tagnames: str):
+        successor = self.first_child_found_in(*tagnames)
+        if successor is not None:
+            successor.addprevious(elm)
+        else:
+            self.append(elm)
+        return elm
+
+    def remove_all(self, *tagnames: str) -> None:
+        """Remove child elements with tagname (e.g. "a:p") in `tagnames`."""
+        for tagname in tagnames:
+            matching = self.findall(qn(tagname))
+            for child in matching:
+                self.remove(child)
+
+    @property
+    def xml(self) -> str:
+        """XML string for this element, suitable for testing purposes.
+
+        Pretty printed for readability and without an XML declaration at the top.
+        """
+        return serialize_for_reading(self)
+
+    def xpath(self, xpath_str: str) -> Any:  # pyright: ignore[reportIncompatibleMethodOverride]
+        """Override of `lxml` _Element.xpath() method.
+
+        Provides standard Open XML namespace mapping (`nsmap`) in centralized location.
+        """
+        return super().xpath(xpath_str, namespaces=nsmap)
+
+    @property
+    def _nsptag(self) -> str:
+        return NamespacePrefixedTag.from_clark_name(self.tag)