about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/pptx
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/pptx
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx')
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/__init__.py82
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/action.py270
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/api.py49
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/axis.py523
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/category.py200
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/chart.py280
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/data.py864
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/datalabel.py288
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/legend.py79
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/marker.py70
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/plot.py412
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/point.py101
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/series.py258
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py272
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/xmlwriter.py1840
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/dml/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/dml/chtfmt.py40
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/dml/color.py301
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/dml/effect.py41
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/dml/fill.py398
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/dml/line.py100
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/action.py71
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/base.py175
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/chart.py492
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/dml.py405
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/lang.py685
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/shapes.py1029
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/text.py230
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/exc.py23
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/media.py197
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/constants.py331
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/oxml.py188
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/package.py762
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/packuri.py109
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/serialized.py296
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/shared.py20
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/spec.py44
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/__init__.py486
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/action.py53
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/axis.py297
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/chart.py282
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/datalabel.py252
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/legend.py72
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/marker.py61
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/plot.py345
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/series.py254
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/shared.py219
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/coreprops.py288
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/dml/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/dml/color.py111
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/dml/fill.py197
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/dml/line.py12
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/ns.py129
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/presentation.py130
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/__init__.py19
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/autoshape.py455
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/connector.py107
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/graphfrm.py342
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/groupshape.py280
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/picture.py270
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/shared.py523
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/simpletypes.py740
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/slide.py347
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/table.py588
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/text.py618
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/theme.py29
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/xmlchemy.py717
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/package.py222
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/chart.py95
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/coreprops.py167
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/embeddedpackage.py93
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/image.py275
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/media.py37
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/presentation.py126
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/slide.py297
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/presentation.py113
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/py.typed0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/__init__.py26
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/autoshape.py355
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/base.py244
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/connector.py297
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/freeform.py337
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/graphfrm.py166
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/group.py69
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/picture.py203
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/placeholder.py407
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/shapetree.py1190
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shared.py82
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/slide.py498
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/spec.py632
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/table.py496
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/default.pptxbin0 -> 34030 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/docx-icon.emfbin0 -> 127228 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/generic-icon.emfbin0 -> 5396 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/notes.xml23
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/notesMaster.xml352
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/pptx-icon.emfbin0 -> 5492 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/theme.xml321
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/xlsx-icon.emfbin0 -> 130380 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/text/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/text/fonts.py399
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/text/layout.py325
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/text/text.py681
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/types.py36
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/util.py214
110 files changed, 28526 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pptx/__init__.py b/.venv/lib/python3.12/site-packages/pptx/__init__.py
new file mode 100644
index 00000000..fb5c2d7e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/__init__.py
@@ -0,0 +1,82 @@
+"""Initialization module for python-pptx package."""
+
+from __future__ import annotations
+
+import sys
+from typing import TYPE_CHECKING
+
+import pptx.exc as exceptions
+from pptx.api import Presentation
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.package import PartFactory
+from pptx.parts.chart import ChartPart
+from pptx.parts.coreprops import CorePropertiesPart
+from pptx.parts.image import ImagePart
+from pptx.parts.media import MediaPart
+from pptx.parts.presentation import PresentationPart
+from pptx.parts.slide import (
+    NotesMasterPart,
+    NotesSlidePart,
+    SlideLayoutPart,
+    SlideMasterPart,
+    SlidePart,
+)
+
+if TYPE_CHECKING:
+    from pptx.opc.package import Part
+
+__version__ = "1.0.2"
+
+sys.modules["pptx.exceptions"] = exceptions
+del sys
+
+__all__ = ["Presentation"]
+
+content_type_to_part_class_map: dict[str, type[Part]] = {
+    CT.PML_PRESENTATION_MAIN: PresentationPart,
+    CT.PML_PRES_MACRO_MAIN: PresentationPart,
+    CT.PML_TEMPLATE_MAIN: PresentationPart,
+    CT.PML_SLIDESHOW_MAIN: PresentationPart,
+    CT.OPC_CORE_PROPERTIES: CorePropertiesPart,
+    CT.PML_NOTES_MASTER: NotesMasterPart,
+    CT.PML_NOTES_SLIDE: NotesSlidePart,
+    CT.PML_SLIDE: SlidePart,
+    CT.PML_SLIDE_LAYOUT: SlideLayoutPart,
+    CT.PML_SLIDE_MASTER: SlideMasterPart,
+    CT.DML_CHART: ChartPart,
+    CT.BMP: ImagePart,
+    CT.GIF: ImagePart,
+    CT.JPEG: ImagePart,
+    CT.MS_PHOTO: ImagePart,
+    CT.PNG: ImagePart,
+    CT.TIFF: ImagePart,
+    CT.X_EMF: ImagePart,
+    CT.X_WMF: ImagePart,
+    CT.ASF: MediaPart,
+    CT.AVI: MediaPart,
+    CT.MOV: MediaPart,
+    CT.MP4: MediaPart,
+    CT.MPG: MediaPart,
+    CT.MS_VIDEO: MediaPart,
+    CT.SWF: MediaPart,
+    CT.VIDEO: MediaPart,
+    CT.WMV: MediaPart,
+    CT.X_MS_VIDEO: MediaPart,
+    # -- accommodate "image/jpg" as an alias for "image/jpeg" --
+    "image/jpg": ImagePart,
+}
+
+PartFactory.part_type_for.update(content_type_to_part_class_map)
+
+del (
+    ChartPart,
+    CorePropertiesPart,
+    ImagePart,
+    MediaPart,
+    SlidePart,
+    SlideLayoutPart,
+    SlideMasterPart,
+    PresentationPart,
+    CT,
+    PartFactory,
+)
diff --git a/.venv/lib/python3.12/site-packages/pptx/action.py b/.venv/lib/python3.12/site-packages/pptx/action.py
new file mode 100644
index 00000000..83c6ebf1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/action.py
@@ -0,0 +1,270 @@
+"""Objects related to mouse click and hover actions on a shape or text."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+from pptx.enum.action import PP_ACTION
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.shapes import Subshape
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.oxml.action import CT_Hyperlink
+    from pptx.oxml.shapes.shared import CT_NonVisualDrawingProps
+    from pptx.oxml.text import CT_TextCharacterProperties
+    from pptx.parts.slide import SlidePart
+    from pptx.shapes.base import BaseShape
+    from pptx.slide import Slide, Slides
+
+
+class ActionSetting(Subshape):
+    """Properties specifying how a shape or run reacts to mouse actions."""
+
+    # -- The Subshape base class provides access to the Slide Part, which is needed to access
+    # -- relationships, which is where hyperlinks live.
+
+    def __init__(
+        self,
+        xPr: CT_NonVisualDrawingProps | CT_TextCharacterProperties,
+        parent: BaseShape,
+        hover: bool = False,
+    ):
+        super(ActionSetting, self).__init__(parent)
+        # xPr is either a cNvPr or rPr element
+        self._element = xPr
+        # _hover determines use of `a:hlinkClick` or `a:hlinkHover`
+        self._hover = hover
+
+    @property
+    def action(self):
+        """Member of :ref:`PpActionType` enumeration, such as `PP_ACTION.HYPERLINK`.
+
+        The returned member indicates the type of action that will result when the
+        specified shape or text is clicked or the mouse pointer is positioned over the
+        shape during a slide show.
+
+        If there is no click-action or the click-action value is not recognized (is not
+        one of the official `MsoPpAction` values) then `PP_ACTION.NONE` is returned.
+        """
+        hlink = self._hlink
+
+        if hlink is None:
+            return PP_ACTION.NONE
+
+        action_verb = hlink.action_verb
+
+        if action_verb == "hlinkshowjump":
+            relative_target = hlink.action_fields["jump"]
+            return {
+                "firstslide": PP_ACTION.FIRST_SLIDE,
+                "lastslide": PP_ACTION.LAST_SLIDE,
+                "lastslideviewed": PP_ACTION.LAST_SLIDE_VIEWED,
+                "nextslide": PP_ACTION.NEXT_SLIDE,
+                "previousslide": PP_ACTION.PREVIOUS_SLIDE,
+                "endshow": PP_ACTION.END_SHOW,
+            }[relative_target]
+
+        return {
+            None: PP_ACTION.HYPERLINK,
+            "hlinksldjump": PP_ACTION.NAMED_SLIDE,
+            "hlinkpres": PP_ACTION.PLAY,
+            "hlinkfile": PP_ACTION.OPEN_FILE,
+            "customshow": PP_ACTION.NAMED_SLIDE_SHOW,
+            "ole": PP_ACTION.OLE_VERB,
+            "macro": PP_ACTION.RUN_MACRO,
+            "program": PP_ACTION.RUN_PROGRAM,
+        }.get(action_verb, PP_ACTION.NONE)
+
+    @lazyproperty
+    def hyperlink(self) -> Hyperlink:
+        """
+        A |Hyperlink| object representing the hyperlink action defined on
+        this click or hover mouse event. A |Hyperlink| object is always
+        returned, even if no hyperlink or other click action is defined.
+        """
+        return Hyperlink(self._element, self._parent, self._hover)
+
+    @property
+    def target_slide(self) -> Slide | None:
+        """
+        A reference to the slide in this presentation that is the target of
+        the slide jump action in this shape. Slide jump actions include
+        `PP_ACTION.FIRST_SLIDE`, `LAST_SLIDE`, `NEXT_SLIDE`,
+        `PREVIOUS_SLIDE`, and `NAMED_SLIDE`. Returns |None| for all other
+        actions. In particular, the `LAST_SLIDE_VIEWED` action and the `PLAY`
+        (start other presentation) actions are not supported.
+
+        A slide object may be assigned to this property, which makes the
+        shape an "internal hyperlink" to the assigened slide::
+
+            slide, target_slide = prs.slides[0], prs.slides[1]
+            shape = slide.shapes[0]
+            shape.target_slide = target_slide
+
+        Assigning |None| removes any slide jump action. Note that this is
+        accomplished by removing any action present (such as a hyperlink),
+        without first checking that it is a slide jump action.
+        """
+        slide_jump_actions = (
+            PP_ACTION.FIRST_SLIDE,
+            PP_ACTION.LAST_SLIDE,
+            PP_ACTION.NEXT_SLIDE,
+            PP_ACTION.PREVIOUS_SLIDE,
+            PP_ACTION.NAMED_SLIDE,
+        )
+
+        if self.action not in slide_jump_actions:
+            return None
+
+        if self.action == PP_ACTION.FIRST_SLIDE:
+            return self._slides[0]
+        elif self.action == PP_ACTION.LAST_SLIDE:
+            return self._slides[-1]
+        elif self.action == PP_ACTION.NEXT_SLIDE:
+            next_slide_idx = self._slide_index + 1
+            if next_slide_idx >= len(self._slides):
+                raise ValueError("no next slide")
+            return self._slides[next_slide_idx]
+        elif self.action == PP_ACTION.PREVIOUS_SLIDE:
+            prev_slide_idx = self._slide_index - 1
+            if prev_slide_idx < 0:
+                raise ValueError("no previous slide")
+            return self._slides[prev_slide_idx]
+        elif self.action == PP_ACTION.NAMED_SLIDE:
+            assert self._hlink is not None
+            rId = self._hlink.rId
+            slide_part = cast("SlidePart", self.part.related_part(rId))
+            return slide_part.slide
+
+    @target_slide.setter
+    def target_slide(self, slide: Slide | None):
+        self._clear_click_action()
+        if slide is None:
+            return
+        hlink = self._element.get_or_add_hlinkClick()
+        hlink.action = "ppaction://hlinksldjump"
+        hlink.rId = self.part.relate_to(slide.part, RT.SLIDE)
+
+    def _clear_click_action(self):
+        """Remove any existing click action."""
+        hlink = self._hlink
+        if hlink is None:
+            return
+        rId = hlink.rId
+        if rId:
+            self.part.drop_rel(rId)
+        self._element.remove(hlink)
+
+    @property
+    def _hlink(self) -> CT_Hyperlink | None:
+        """
+        Reference to the `a:hlinkClick` or `a:hlinkHover` element for this
+        click action. Returns |None| if the element is not present.
+        """
+        if self._hover:
+            assert isinstance(self._element, CT_NonVisualDrawingProps)
+            return self._element.hlinkHover
+        return self._element.hlinkClick
+
+    @lazyproperty
+    def _slide(self):
+        """
+        Reference to the slide containing the shape having this click action.
+        """
+        return self.part.slide
+
+    @lazyproperty
+    def _slide_index(self):
+        """
+        Position in the slide collection of the slide containing the shape
+        having this click action.
+        """
+        return self._slides.index(self._slide)
+
+    @lazyproperty
+    def _slides(self) -> Slides:
+        """
+        Reference to the slide collection for this presentation.
+        """
+        return self.part.package.presentation_part.presentation.slides
+
+
+class Hyperlink(Subshape):
+    """Represents a hyperlink action on a shape or text run."""
+
+    def __init__(
+        self,
+        xPr: CT_NonVisualDrawingProps | CT_TextCharacterProperties,
+        parent: BaseShape,
+        hover: bool = False,
+    ):
+        super(Hyperlink, self).__init__(parent)
+        # xPr is either a cNvPr or rPr element
+        self._element = xPr
+        # _hover determines use of `a:hlinkClick` or `a:hlinkHover`
+        self._hover = hover
+
+    @property
+    def address(self) -> str | None:
+        """Read/write. The URL of the hyperlink.
+
+        URL can be on http, https, mailto, or file scheme; others may work. Returns |None| if no
+        hyperlink is defined, including when another action such as `RUN_MACRO` is defined on the
+        object. Assigning |None| removes any action defined on the object, whether it is a hyperlink
+        action or not.
+        """
+        hlink = self._hlink
+
+        # there's no URL if there's no click action
+        if hlink is None:
+            return None
+
+        # a click action without a relationship has no URL
+        rId = hlink.rId
+        if not rId:
+            return None
+
+        return self.part.target_ref(rId)
+
+    @address.setter
+    def address(self, url: str | None):
+        # implements all three of add, change, and remove hyperlink
+        self._remove_hlink()
+
+        if url:
+            rId = self.part.relate_to(url, RT.HYPERLINK, is_external=True)
+            hlink = self._get_or_add_hlink()
+            hlink.rId = rId
+
+    def _get_or_add_hlink(self) -> CT_Hyperlink:
+        """Get the `a:hlinkClick` or `a:hlinkHover` element for the Hyperlink object.
+
+        The actual element depends on the value of `self._hover`. Create the element if not present.
+        """
+        if self._hover:
+            return cast("CT_NonVisualDrawingProps", self._element).get_or_add_hlinkHover()
+        return self._element.get_or_add_hlinkClick()
+
+    @property
+    def _hlink(self) -> CT_Hyperlink | None:
+        """Reference to the `a:hlinkClick` or `h:hlinkHover` element for this click action.
+
+        Returns |None| if the element is not present.
+        """
+        if self._hover:
+            return cast("CT_NonVisualDrawingProps", self._element).hlinkHover
+        return self._element.hlinkClick
+
+    def _remove_hlink(self):
+        """Remove the a:hlinkClick or a:hlinkHover element.
+
+        Also drops any relationship it might have.
+        """
+        hlink = self._hlink
+        if hlink is None:
+            return
+        rId = hlink.rId
+        if rId:
+            self.part.drop_rel(rId)
+        self._element.remove(hlink)
diff --git a/.venv/lib/python3.12/site-packages/pptx/api.py b/.venv/lib/python3.12/site-packages/pptx/api.py
new file mode 100644
index 00000000..892f425a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/api.py
@@ -0,0 +1,49 @@
+"""Directly exposed API classes, Presentation for now.
+
+Provides some syntactic sugar for interacting with the pptx.presentation.Package graph and also
+provides some insulation so not so many classes in the other modules need to be named as internal
+(leading underscore).
+"""
+
+from __future__ import annotations
+
+import os
+from typing import IO, TYPE_CHECKING
+
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.package import Package
+
+if TYPE_CHECKING:
+    from pptx import presentation
+    from pptx.parts.presentation import PresentationPart
+
+
+def Presentation(pptx: str | IO[bytes] | None = None) -> presentation.Presentation:
+    """
+    Return a |Presentation| object loaded from *pptx*, where *pptx* can be
+    either a path to a ``.pptx`` file (a string) or a file-like object. If
+    *pptx* is missing or ``None``, the built-in default presentation
+    "template" is loaded.
+    """
+    if pptx is None:
+        pptx = _default_pptx_path()
+
+    presentation_part = Package.open(pptx).main_document_part
+
+    if not _is_pptx_package(presentation_part):
+        tmpl = "file '%s' is not a PowerPoint file, content type is '%s'"
+        raise ValueError(tmpl % (pptx, presentation_part.content_type))
+
+    return presentation_part.presentation
+
+
+def _default_pptx_path() -> str:
+    """Return the path to the built-in default .pptx package."""
+    _thisdir = os.path.split(__file__)[0]
+    return os.path.join(_thisdir, "templates", "default.pptx")
+
+
+def _is_pptx_package(prs_part: PresentationPart):
+    """Return |True| if *prs_part* is a valid main document part, |False| otherwise."""
+    valid_content_types = (CT.PML_PRESENTATION_MAIN, CT.PML_PRES_MACRO_MAIN)
+    return prs_part.content_type in valid_content_types
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/__init__.py b/.venv/lib/python3.12/site-packages/pptx/chart/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/axis.py b/.venv/lib/python3.12/site-packages/pptx/chart/axis.py
new file mode 100644
index 00000000..a9b87703
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/axis.py
@@ -0,0 +1,523 @@
+"""Axis-related chart objects."""
+
+from __future__ import annotations
+
+from pptx.dml.chtfmt import ChartFormat
+from pptx.enum.chart import (
+    XL_AXIS_CROSSES,
+    XL_CATEGORY_TYPE,
+    XL_TICK_LABEL_POSITION,
+    XL_TICK_MARK,
+)
+from pptx.oxml.ns import qn
+from pptx.oxml.simpletypes import ST_Orientation
+from pptx.shared import ElementProxy
+from pptx.text.text import Font, TextFrame
+from pptx.util import lazyproperty
+
+
+class _BaseAxis(object):
+    """Base class for chart axis objects. All axis objects share these properties."""
+
+    def __init__(self, xAx):
+        super(_BaseAxis, self).__init__()
+        self._element = xAx  # axis element, c:catAx or c:valAx
+        self._xAx = xAx
+
+    @property
+    def axis_title(self):
+        """An |AxisTitle| object providing access to title properties.
+
+        Calling this property is destructive in the sense that it adds an
+        axis title element (`c:title`) to the axis XML if one is not already
+        present. Use :attr:`has_title` to test for presence of axis title
+        non-destructively.
+        """
+        return AxisTitle(self._element.get_or_add_title())
+
+    @lazyproperty
+    def format(self):
+        """
+        The |ChartFormat| object providing access to the shape formatting
+        properties of this axis, such as its line color and fill.
+        """
+        return ChartFormat(self._element)
+
+    @property
+    def has_major_gridlines(self):
+        """
+        Read/write boolean value specifying whether this axis has gridlines
+        at its major tick mark locations. Assigning |True| to this property
+        causes major gridlines to be displayed. Assigning |False| causes them
+        to be removed.
+        """
+        if self._element.majorGridlines is None:
+            return False
+        return True
+
+    @has_major_gridlines.setter
+    def has_major_gridlines(self, value):
+        if bool(value) is True:
+            self._element.get_or_add_majorGridlines()
+        else:
+            self._element._remove_majorGridlines()
+
+    @property
+    def has_minor_gridlines(self):
+        """
+        Read/write boolean value specifying whether this axis has gridlines
+        at its minor tick mark locations. Assigning |True| to this property
+        causes minor gridlines to be displayed. Assigning |False| causes them
+        to be removed.
+        """
+        if self._element.minorGridlines is None:
+            return False
+        return True
+
+    @has_minor_gridlines.setter
+    def has_minor_gridlines(self, value):
+        if bool(value) is True:
+            self._element.get_or_add_minorGridlines()
+        else:
+            self._element._remove_minorGridlines()
+
+    @property
+    def has_title(self):
+        """Read/write boolean specifying whether this axis has a title.
+
+        |True| if this axis has a title, |False| otherwise. Assigning |True|
+        causes an axis title to be added if not already present. Assigning
+        |False| causes any existing title to be deleted.
+        """
+        if self._element.title is None:
+            return False
+        return True
+
+    @has_title.setter
+    def has_title(self, value):
+        if bool(value) is True:
+            self._element.get_or_add_title()
+        else:
+            self._element._remove_title()
+
+    @lazyproperty
+    def major_gridlines(self):
+        """
+        The |MajorGridlines| object representing the major gridlines for
+        this axis.
+        """
+        return MajorGridlines(self._element)
+
+    @property
+    def major_tick_mark(self):
+        """
+        Read/write :ref:`XlTickMark` value specifying the type of major tick
+        mark to display on this axis.
+        """
+        majorTickMark = self._element.majorTickMark
+        if majorTickMark is None:
+            return XL_TICK_MARK.CROSS
+        return majorTickMark.val
+
+    @major_tick_mark.setter
+    def major_tick_mark(self, value):
+        self._element._remove_majorTickMark()
+        if value is XL_TICK_MARK.CROSS:
+            return
+        self._element._add_majorTickMark(val=value)
+
+    @property
+    def maximum_scale(self):
+        """
+        Read/write float value specifying the upper limit of the value range
+        for this axis, the number at the top or right of the vertical or
+        horizontal value scale, respectively. The value |None| indicates the
+        upper limit should be determined automatically based on the range of
+        data point values associated with the axis.
+        """
+        return self._element.scaling.maximum
+
+    @maximum_scale.setter
+    def maximum_scale(self, value):
+        scaling = self._element.scaling
+        scaling.maximum = value
+
+    @property
+    def minimum_scale(self):
+        """
+        Read/write float value specifying lower limit of value range, the
+        number at the bottom or left of the value scale. |None| if no minimum
+        scale has been set. The value |None| indicates the lower limit should
+        be determined automatically based on the range of data point values
+        associated with the axis.
+        """
+        return self._element.scaling.minimum
+
+    @minimum_scale.setter
+    def minimum_scale(self, value):
+        scaling = self._element.scaling
+        scaling.minimum = value
+
+    @property
+    def minor_tick_mark(self):
+        """
+        Read/write :ref:`XlTickMark` value specifying the type of minor tick
+        mark for this axis.
+        """
+        minorTickMark = self._element.minorTickMark
+        if minorTickMark is None:
+            return XL_TICK_MARK.CROSS
+        return minorTickMark.val
+
+    @minor_tick_mark.setter
+    def minor_tick_mark(self, value):
+        self._element._remove_minorTickMark()
+        if value is XL_TICK_MARK.CROSS:
+            return
+        self._element._add_minorTickMark(val=value)
+
+    @property
+    def reverse_order(self):
+        """Read/write bool value specifying whether to reverse plotting order for axis.
+
+        For a category axis, this reverses the order in which the categories are
+        displayed. This may be desired, for example, on a (horizontal) bar-chart where
+        by default the first category appears at the bottom. Since we read from
+        top-to-bottom, many viewers may find it most natural for the first category to
+        appear on top.
+
+        For a value axis, it reverses the direction of increasing value from
+        bottom-to-top to top-to-bottom.
+        """
+        return self._element.orientation == ST_Orientation.MAX_MIN
+
+    @reverse_order.setter
+    def reverse_order(self, value):
+        self._element.orientation = (
+            ST_Orientation.MAX_MIN if bool(value) is True else ST_Orientation.MIN_MAX
+        )
+
+    @lazyproperty
+    def tick_labels(self):
+        """
+        The |TickLabels| instance providing access to axis tick label
+        formatting properties. Tick labels are the numbers appearing on
+        a value axis or the category names appearing on a category axis.
+        """
+        return TickLabels(self._element)
+
+    @property
+    def tick_label_position(self):
+        """
+        Read/write :ref:`XlTickLabelPosition` value specifying where the tick
+        labels for this axis should appear.
+        """
+        tickLblPos = self._element.tickLblPos
+        if tickLblPos is None:
+            return XL_TICK_LABEL_POSITION.NEXT_TO_AXIS
+        if tickLblPos.val is None:
+            return XL_TICK_LABEL_POSITION.NEXT_TO_AXIS
+        return tickLblPos.val
+
+    @tick_label_position.setter
+    def tick_label_position(self, value):
+        tickLblPos = self._element.get_or_add_tickLblPos()
+        tickLblPos.val = value
+
+    @property
+    def visible(self):
+        """
+        Read/write. |True| if axis is visible, |False| otherwise.
+        """
+        delete = self._element.delete_
+        if delete is None:
+            return False
+        return False if delete.val else True
+
+    @visible.setter
+    def visible(self, value):
+        if value not in (True, False):
+            raise ValueError("assigned value must be True or False, got: %s" % value)
+        delete = self._element.get_or_add_delete_()
+        delete.val = not value
+
+
+class AxisTitle(ElementProxy):
+    """Provides properties for manipulating axis title."""
+
+    def __init__(self, title):
+        super(AxisTitle, self).__init__(title)
+        self._title = title
+
+    @lazyproperty
+    def format(self):
+        """|ChartFormat| object providing access to shape formatting.
+
+        Return the |ChartFormat| object providing shape formatting properties
+        for this axis title, such as its line color and fill.
+        """
+        return ChartFormat(self._element)
+
+    @property
+    def has_text_frame(self):
+        """Read/write Boolean specifying presence of a text frame.
+
+        Return |True| if this axis title has a text frame, and |False|
+        otherwise. Assigning |True| causes a text frame to be added if not
+        already present. Assigning |False| causes any existing text frame to
+        be removed along with any text contained in the text frame.
+        """
+        if self._title.tx_rich is None:
+            return False
+        return True
+
+    @has_text_frame.setter
+    def has_text_frame(self, value):
+        if bool(value) is True:
+            self._title.get_or_add_tx_rich()
+        else:
+            self._title._remove_tx()
+
+    @property
+    def text_frame(self):
+        """|TextFrame| instance for this axis title.
+
+        Return a |TextFrame| instance allowing read/write access to the text
+        of this axis title and its text formatting properties. Accessing this
+        property is destructive as it adds a new text frame if not already
+        present.
+        """
+        rich = self._title.get_or_add_tx_rich()
+        return TextFrame(rich, self)
+
+
+class CategoryAxis(_BaseAxis):
+    """A category axis of a chart."""
+
+    @property
+    def category_type(self):
+        """
+        A member of :ref:`XlCategoryType` specifying the scale type of this
+        axis. Unconditionally ``CATEGORY_SCALE`` for a |CategoryAxis| object.
+        """
+        return XL_CATEGORY_TYPE.CATEGORY_SCALE
+
+
+class DateAxis(_BaseAxis):
+    """A category axis with dates as its category labels.
+
+    This axis-type has some special display behaviors such as making length of equal
+    periods equal and normalizing month start dates despite unequal month lengths.
+    """
+
+    @property
+    def category_type(self):
+        """
+        A member of :ref:`XlCategoryType` specifying the scale type of this
+        axis. Unconditionally ``TIME_SCALE`` for a |DateAxis| object.
+        """
+        return XL_CATEGORY_TYPE.TIME_SCALE
+
+
+class MajorGridlines(ElementProxy):
+    """Provides access to the properties of the major gridlines appearing on an axis."""
+
+    def __init__(self, xAx):
+        super(MajorGridlines, self).__init__(xAx)
+        self._xAx = xAx  # axis element, catAx or valAx
+
+    @lazyproperty
+    def format(self):
+        """
+        The |ChartFormat| object providing access to the shape formatting
+        properties of this data point, such as line and fill.
+        """
+        majorGridlines = self._xAx.get_or_add_majorGridlines()
+        return ChartFormat(majorGridlines)
+
+
+class TickLabels(object):
+    """A service class providing access to formatting of axis tick mark labels."""
+
+    def __init__(self, xAx_elm):
+        super(TickLabels, self).__init__()
+        self._element = xAx_elm
+
+    @lazyproperty
+    def font(self):
+        """
+        The |Font| object that provides access to the text properties for
+        these tick labels, such as bold, italic, etc.
+        """
+        defRPr = self._element.defRPr
+        font = Font(defRPr)
+        return font
+
+    @property
+    def number_format(self):
+        """
+        Read/write string (e.g. "$#,##0.00") specifying the format for the
+        numbers on this axis. The syntax for these strings is the same as it
+        appears in the PowerPoint or Excel UI. Returns 'General' if no number
+        format has been set. Note that this format string has no effect on
+        rendered tick labels when :meth:`number_format_is_linked` is |True|.
+        Assigning a format string to this property automatically sets
+        :meth:`number_format_is_linked` to |False|.
+        """
+        numFmt = self._element.numFmt
+        if numFmt is None:
+            return "General"
+        return numFmt.formatCode
+
+    @number_format.setter
+    def number_format(self, value):
+        numFmt = self._element.get_or_add_numFmt()
+        numFmt.formatCode = value
+        self.number_format_is_linked = False
+
+    @property
+    def number_format_is_linked(self):
+        """
+        Read/write boolean specifying whether number formatting should be
+        taken from the source spreadsheet rather than the value of
+        :meth:`number_format`.
+        """
+        numFmt = self._element.numFmt
+        if numFmt is None:
+            return False
+        souceLinked = numFmt.sourceLinked
+        if souceLinked is None:
+            return True
+        return numFmt.sourceLinked
+
+    @number_format_is_linked.setter
+    def number_format_is_linked(self, value):
+        numFmt = self._element.get_or_add_numFmt()
+        numFmt.sourceLinked = value
+
+    @property
+    def offset(self):
+        """
+        Read/write int value in range 0-1000 specifying the spacing between
+        the tick mark labels and the axis as a percentange of the default
+        value. 100 if no label offset setting is present.
+        """
+        lblOffset = self._element.lblOffset
+        if lblOffset is None:
+            return 100
+        return lblOffset.val
+
+    @offset.setter
+    def offset(self, value):
+        if self._element.tag != qn("c:catAx"):
+            raise ValueError("only a category axis has an offset")
+        self._element._remove_lblOffset()
+        if value == 100:
+            return
+        lblOffset = self._element._add_lblOffset()
+        lblOffset.val = value
+
+
+class ValueAxis(_BaseAxis):
+    """An axis having continuous (as opposed to discrete) values.
+
+    The vertical axis is generally a value axis, however both axes of an XY-type chart
+    are value axes.
+    """
+
+    @property
+    def crosses(self):
+        """
+        Member of :ref:`XlAxisCrosses` enumeration specifying the point on
+        this axis where the other axis crosses, such as auto/zero, minimum,
+        or maximum. Returns `XL_AXIS_CROSSES.CUSTOM` when a specific numeric
+        crossing point (e.g. 1.5) is defined.
+        """
+        crosses = self._cross_xAx.crosses
+        if crosses is None:
+            return XL_AXIS_CROSSES.CUSTOM
+        return crosses.val
+
+    @crosses.setter
+    def crosses(self, value):
+        cross_xAx = self._cross_xAx
+        if value == XL_AXIS_CROSSES.CUSTOM:
+            if cross_xAx.crossesAt is not None:
+                return
+        cross_xAx._remove_crosses()
+        cross_xAx._remove_crossesAt()
+        if value == XL_AXIS_CROSSES.CUSTOM:
+            cross_xAx._add_crossesAt(val=0.0)
+        else:
+            cross_xAx._add_crosses(val=value)
+
+    @property
+    def crosses_at(self):
+        """
+        Numeric value on this axis at which the perpendicular axis crosses.
+        Returns |None| if no crossing value is set.
+        """
+        crossesAt = self._cross_xAx.crossesAt
+        if crossesAt is None:
+            return None
+        return crossesAt.val
+
+    @crosses_at.setter
+    def crosses_at(self, value):
+        cross_xAx = self._cross_xAx
+        cross_xAx._remove_crosses()
+        cross_xAx._remove_crossesAt()
+        if value is None:
+            return
+        cross_xAx._add_crossesAt(val=value)
+
+    @property
+    def major_unit(self):
+        """
+        The float number of units between major tick marks on this value
+        axis. |None| corresponds to the 'Auto' setting in the UI, and
+        specifies the value should be calculated by PowerPoint based on the
+        underlying chart data.
+        """
+        majorUnit = self._element.majorUnit
+        if majorUnit is None:
+            return None
+        return majorUnit.val
+
+    @major_unit.setter
+    def major_unit(self, value):
+        self._element._remove_majorUnit()
+        if value is None:
+            return
+        self._element._add_majorUnit(val=value)
+
+    @property
+    def minor_unit(self):
+        """
+        The float number of units between minor tick marks on this value
+        axis. |None| corresponds to the 'Auto' setting in the UI, and
+        specifies the value should be calculated by PowerPoint based on the
+        underlying chart data.
+        """
+        minorUnit = self._element.minorUnit
+        if minorUnit is None:
+            return None
+        return minorUnit.val
+
+    @minor_unit.setter
+    def minor_unit(self, value):
+        self._element._remove_minorUnit()
+        if value is None:
+            return
+        self._element._add_minorUnit(val=value)
+
+    @property
+    def _cross_xAx(self):
+        """
+        The axis element in the same group (primary/secondary) that crosses
+        this axis.
+        """
+        crossAx_id = self._element.crossAx.val
+        expr = '(../c:catAx | ../c:valAx | ../c:dateAx)/c:axId[@val="%d"]' % crossAx_id
+        cross_axId = self._element.xpath(expr)[0]
+        return cross_axId.getparent()
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/category.py b/.venv/lib/python3.12/site-packages/pptx/chart/category.py
new file mode 100644
index 00000000..2c28aff5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/category.py
@@ -0,0 +1,200 @@
+"""Category-related objects.
+
+The |category.Categories| object is returned by ``Plot.categories`` and contains zero or
+more |category.Category| objects, each representing one of the category labels
+associated with the plot. Categories can be hierarchical, so there are members allowing
+discovery of the depth of that hierarchy and providing means to navigate it.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+
+class Categories(Sequence):
+    """
+    A sequence of |category.Category| objects, each representing a category
+    label on the chart. Provides properties for dealing with hierarchical
+    categories.
+    """
+
+    def __init__(self, xChart):
+        super(Categories, self).__init__()
+        self._xChart = xChart
+
+    def __getitem__(self, idx):
+        pt = self._xChart.cat_pts[idx]
+        return Category(pt, idx)
+
+    def __iter__(self):
+        cat_pts = self._xChart.cat_pts
+        for idx, pt in enumerate(cat_pts):
+            yield Category(pt, idx)
+
+    def __len__(self):
+        # a category can be "null", meaning the Excel cell for it is empty.
+        # In this case, there is no c:pt element for it. The "empty" category
+        # will, however, be accounted for in c:cat//c:ptCount/@val, which
+        # reflects the true length of the categories collection.
+        return self._xChart.cat_pt_count
+
+    @property
+    def depth(self):
+        """
+        Return an integer representing the number of hierarchical levels in
+        this category collection. Returns 1 for non-hierarchical categories
+        and 0 if no categories are present (generally meaning no series are
+        present).
+        """
+        cat = self._xChart.cat
+        if cat is None:
+            return 0
+        if cat.multiLvlStrRef is None:
+            return 1
+        return len(cat.lvls)
+
+    @property
+    def flattened_labels(self):
+        """
+        Return a sequence of tuples, each containing the flattened hierarchy
+        of category labels for a leaf category. Each tuple is in parent ->
+        child order, e.g. ``('US', 'CA', 'San Francisco')``, with the leaf
+        category appearing last. If this categories collection is
+        non-hierarchical, each tuple will contain only a leaf category label.
+        If the plot has no series (and therefore no categories), an empty
+        tuple is returned.
+        """
+        cat = self._xChart.cat
+        if cat is None:
+            return ()
+
+        if cat.multiLvlStrRef is None:
+            return tuple([(category.label,) for category in self])
+
+        return tuple(
+            [
+                tuple([category.label for category in reversed(flat_cat)])
+                for flat_cat in self._iter_flattened_categories()
+            ]
+        )
+
+    @property
+    def levels(self):
+        """
+        Return a sequence of |CategoryLevel| objects representing the
+        hierarchy of this category collection. The sequence is empty when the
+        category collection is not hierarchical, that is, contains only
+        leaf-level categories. The levels are ordered from the leaf level to
+        the root level; so the first level will contain the same categories
+        as this category collection.
+        """
+        cat = self._xChart.cat
+        if cat is None:
+            return []
+        return [CategoryLevel(lvl) for lvl in cat.lvls]
+
+    def _iter_flattened_categories(self):
+        """
+        Generate a ``tuple`` object for each leaf category in this
+        collection, containing the leaf category followed by its "parent"
+        categories, e.g. ``('San Francisco', 'CA', 'USA'). Each tuple will be
+        the same length as the number of levels (excepting certain edge
+        cases which I believe always indicate a chart construction error).
+        """
+        levels = self.levels
+        if not levels:
+            return
+        leaf_level, remaining_levels = levels[0], levels[1:]
+        for category in leaf_level:
+            yield self._parentage((category,), remaining_levels)
+
+    def _parentage(self, categories, levels):
+        """
+        Return a tuple formed by recursively concatenating *categories* with
+        its next ancestor from *levels*. The idx value of the first category
+        in *categories* determines parentage in all levels. The returned
+        sequence is in child -> parent order. A parent category is the
+        Category object in a next level having the maximum idx value not
+        exceeding that of the leaf category.
+        """
+        # exhausting levels is the expected recursion termination condition
+        if not levels:
+            return tuple(categories)
+
+        # guard against edge case where next level is present but empty. That
+        # situation is not prohibited for some reason.
+        if not levels[0]:
+            return tuple(categories)
+
+        parent_level, remaining_levels = levels[0], levels[1:]
+        leaf_node = categories[0]
+
+        # Make the first parent the default. A possible edge case is where no
+        # parent is defined for one or more leading values, e.g. idx > 0 for
+        # the first parent.
+        parent = parent_level[0]
+        for category in parent_level:
+            if category.idx > leaf_node.idx:
+                break
+            parent = category
+
+        extended_categories = tuple(categories) + (parent,)
+        return self._parentage(extended_categories, remaining_levels)
+
+
+class Category(str):
+    """
+    An extension of `str` that provides the category label as its string
+    value, and additional attributes representing other aspects of the
+    category.
+    """
+
+    def __new__(cls, pt, *args):
+        category_label = "" if pt is None else pt.v.text
+        return str.__new__(cls, category_label)
+
+    def __init__(self, pt, idx=None):
+        """
+        *idx* is a required attribute of a c:pt element, but must be
+        specified when pt is None, as when a "placeholder" category is
+        created to represent a missing c:pt element.
+        """
+        self._element = self._pt = pt
+        self._idx = idx
+
+    @property
+    def idx(self):
+        """
+        Return an integer representing the index reference of this category.
+        For a leaf node, the index identifies the category. For a parent (or
+        other ancestor) category, the index specifies the first leaf category
+        that ancestor encloses.
+        """
+        if self._pt is None:
+            return self._idx
+        return self._pt.idx
+
+    @property
+    def label(self):
+        """
+        Return the label of this category as a string.
+        """
+        return str(self)
+
+
+class CategoryLevel(Sequence):
+    """
+    A sequence of |category.Category| objects representing a single level in
+    a hierarchical category collection. This object is only used when the
+    categories are hierarchical, meaning they have more than one level and
+    higher level categories group those at lower levels.
+    """
+
+    def __init__(self, lvl):
+        self._element = self._lvl = lvl
+
+    def __getitem__(self, offset):
+        return Category(self._lvl.pt_lst[offset])
+
+    def __len__(self):
+        return len(self._lvl.pt_lst)
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/chart.py b/.venv/lib/python3.12/site-packages/pptx/chart/chart.py
new file mode 100644
index 00000000..d73aa933
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/chart.py
@@ -0,0 +1,280 @@
+"""Chart-related objects such as Chart and ChartTitle."""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+from pptx.chart.axis import CategoryAxis, DateAxis, ValueAxis
+from pptx.chart.legend import Legend
+from pptx.chart.plot import PlotFactory, PlotTypeInspector
+from pptx.chart.series import SeriesCollection
+from pptx.chart.xmlwriter import SeriesXmlRewriterFactory
+from pptx.dml.chtfmt import ChartFormat
+from pptx.shared import ElementProxy, PartElementProxy
+from pptx.text.text import Font, TextFrame
+from pptx.util import lazyproperty
+
+
+class Chart(PartElementProxy):
+    """A chart object."""
+
+    def __init__(self, chartSpace, chart_part):
+        super(Chart, self).__init__(chartSpace, chart_part)
+        self._chartSpace = chartSpace
+
+    @property
+    def category_axis(self):
+        """
+        The category axis of this chart. In the case of an XY or Bubble
+        chart, this is the X axis. Raises |ValueError| if no category
+        axis is defined (as is the case for a pie chart, for example).
+        """
+        catAx_lst = self._chartSpace.catAx_lst
+        if catAx_lst:
+            return CategoryAxis(catAx_lst[0])
+
+        dateAx_lst = self._chartSpace.dateAx_lst
+        if dateAx_lst:
+            return DateAxis(dateAx_lst[0])
+
+        valAx_lst = self._chartSpace.valAx_lst
+        if valAx_lst:
+            return ValueAxis(valAx_lst[0])
+
+        raise ValueError("chart has no category axis")
+
+    @property
+    def chart_style(self):
+        """
+        Read/write integer index of chart style used to format this chart.
+        Range is from 1 to 48. Value is |None| if no explicit style has been
+        assigned, in which case the default chart style is used. Assigning
+        |None| causes any explicit setting to be removed. The integer index
+        corresponds to the style's position in the chart style gallery in the
+        PowerPoint UI.
+        """
+        style = self._chartSpace.style
+        if style is None:
+            return None
+        return style.val
+
+    @chart_style.setter
+    def chart_style(self, value):
+        self._chartSpace._remove_style()
+        if value is None:
+            return
+        self._chartSpace._add_style(val=value)
+
+    @property
+    def chart_title(self):
+        """A |ChartTitle| object providing access to title properties.
+
+        Calling this property is destructive in the sense it adds a chart
+        title element (`c:title`) to the chart XML if one is not already
+        present. Use :attr:`has_title` to test for presence of a chart title
+        non-destructively.
+        """
+        return ChartTitle(self._element.get_or_add_title())
+
+    @property
+    def chart_type(self):
+        """Member of :ref:`XlChartType` enumeration specifying type of this chart.
+
+        If the chart has two plots, for example, a line plot overlayed on a bar plot,
+        the type reported is for the first (back-most) plot. Read-only.
+        """
+        first_plot = self.plots[0]
+        return PlotTypeInspector.chart_type(first_plot)
+
+    @lazyproperty
+    def font(self):
+        """Font object controlling text format defaults for this chart."""
+        defRPr = self._chartSpace.get_or_add_txPr().p_lst[0].get_or_add_pPr().get_or_add_defRPr()
+        return Font(defRPr)
+
+    @property
+    def has_legend(self):
+        """
+        Read/write boolean, |True| if the chart has a legend. Assigning
+        |True| causes a legend to be added to the chart if it doesn't already
+        have one. Assigning False removes any existing legend definition
+        along with any existing legend settings.
+        """
+        return self._chartSpace.chart.has_legend
+
+    @has_legend.setter
+    def has_legend(self, value):
+        self._chartSpace.chart.has_legend = bool(value)
+
+    @property
+    def has_title(self):
+        """Read/write boolean, specifying whether this chart has a title.
+
+        Assigning |True| causes a title to be added if not already present.
+        Assigning |False| removes any existing title along with its text and
+        settings.
+        """
+        title = self._chartSpace.chart.title
+        if title is None:
+            return False
+        return True
+
+    @has_title.setter
+    def has_title(self, value):
+        chart = self._chartSpace.chart
+        if bool(value) is False:
+            chart._remove_title()
+            autoTitleDeleted = chart.get_or_add_autoTitleDeleted()
+            autoTitleDeleted.val = True
+            return
+        chart.get_or_add_title()
+
+    @property
+    def legend(self):
+        """
+        A |Legend| object providing access to the properties of the legend
+        for this chart.
+        """
+        legend_elm = self._chartSpace.chart.legend
+        if legend_elm is None:
+            return None
+        return Legend(legend_elm)
+
+    @lazyproperty
+    def plots(self):
+        """
+        The sequence of plots in this chart. A plot, called a *chart group*
+        in the Microsoft API, is a distinct sequence of one or more series
+        depicted in a particular charting type. For example, a chart having
+        a series plotted as a line overlaid on three series plotted as
+        columns would have two plots; the first corresponding to the three
+        column series and the second to the line series. Plots are sequenced
+        in the order drawn, i.e. back-most to front-most. Supports *len()*,
+        membership (e.g. ``p in plots``), iteration, slicing, and indexed
+        access (e.g. ``plot = plots[i]``).
+        """
+        plotArea = self._chartSpace.chart.plotArea
+        return _Plots(plotArea, self)
+
+    def replace_data(self, chart_data):
+        """
+        Use the categories and series values in the |ChartData| object
+        *chart_data* to replace those in the XML and Excel worksheet for this
+        chart.
+        """
+        rewriter = SeriesXmlRewriterFactory(self.chart_type, chart_data)
+        rewriter.replace_series_data(self._chartSpace)
+        self._workbook.update_from_xlsx_blob(chart_data.xlsx_blob)
+
+    @lazyproperty
+    def series(self):
+        """
+        A |SeriesCollection| object containing all the series in this
+        chart. When the chart has multiple plots, all the series for the
+        first plot appear before all those for the second, and so on. Series
+        within a plot have an explicit ordering and appear in that sequence.
+        """
+        return SeriesCollection(self._chartSpace.plotArea)
+
+    @property
+    def value_axis(self):
+        """
+        The |ValueAxis| object providing access to properties of the value
+        axis of this chart. Raises |ValueError| if the chart has no value
+        axis.
+        """
+        valAx_lst = self._chartSpace.valAx_lst
+        if not valAx_lst:
+            raise ValueError("chart has no value axis")
+
+        idx = 1 if len(valAx_lst) > 1 else 0
+        return ValueAxis(valAx_lst[idx])
+
+    @property
+    def _workbook(self):
+        """
+        The |ChartWorkbook| object providing access to the Excel source data
+        for this chart.
+        """
+        return self.part.chart_workbook
+
+
+class ChartTitle(ElementProxy):
+    """Provides properties for manipulating a chart title."""
+
+    # This shares functionality with AxisTitle, which could be factored out
+    # into a base class, perhaps pptx.chart.shared.BaseTitle. I suspect they
+    # actually differ in certain fuller behaviors, but at present they're
+    # essentially identical.
+
+    def __init__(self, title):
+        super(ChartTitle, self).__init__(title)
+        self._title = title
+
+    @lazyproperty
+    def format(self):
+        """|ChartFormat| object providing access to line and fill formatting.
+
+        Return the |ChartFormat| object providing shape formatting properties
+        for this chart title, such as its line color and fill.
+        """
+        return ChartFormat(self._title)
+
+    @property
+    def has_text_frame(self):
+        """Read/write Boolean specifying whether this title has a text frame.
+
+        Return |True| if this chart title has a text frame, and |False|
+        otherwise. Assigning |True| causes a text frame to be added if not
+        already present. Assigning |False| causes any existing text frame to
+        be removed along with its text and formatting.
+        """
+        if self._title.tx_rich is None:
+            return False
+        return True
+
+    @has_text_frame.setter
+    def has_text_frame(self, value):
+        if bool(value) is False:
+            self._title._remove_tx()
+            return
+        self._title.get_or_add_tx_rich()
+
+    @property
+    def text_frame(self):
+        """|TextFrame| instance for this chart title.
+
+        Return a |TextFrame| instance allowing read/write access to the text
+        of this chart title and its text formatting properties. Accessing this
+        property is destructive in the sense it adds a text frame if one is
+        not present. Use :attr:`has_text_frame` to test for the presence of
+        a text frame non-destructively.
+        """
+        rich = self._title.get_or_add_tx_rich()
+        return TextFrame(rich, self)
+
+
+class _Plots(Sequence):
+    """
+    The sequence of plots in a chart, such as a bar plot or a line plot. Most
+    charts have only a single plot. The concept is necessary when two chart
+    types are displayed in a single set of axes, like a bar plot with
+    a superimposed line plot.
+    """
+
+    def __init__(self, plotArea, chart):
+        super(_Plots, self).__init__()
+        self._plotArea = plotArea
+        self._chart = chart
+
+    def __getitem__(self, index):
+        xCharts = self._plotArea.xCharts
+        if isinstance(index, slice):
+            plots = [PlotFactory(xChart, self._chart) for xChart in xCharts]
+            return plots[index]
+        else:
+            xChart = xCharts[index]
+            return PlotFactory(xChart, self._chart)
+
+    def __len__(self):
+        return len(self._plotArea.xCharts)
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/data.py b/.venv/lib/python3.12/site-packages/pptx/chart/data.py
new file mode 100644
index 00000000..ec6a61f3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/data.py
@@ -0,0 +1,864 @@
+"""ChartData and related objects."""
+
+from __future__ import annotations
+
+import datetime
+from collections.abc import Sequence
+from numbers import Number
+
+from pptx.chart.xlsx import (
+    BubbleWorkbookWriter,
+    CategoryWorkbookWriter,
+    XyWorkbookWriter,
+)
+from pptx.chart.xmlwriter import ChartXmlWriter
+from pptx.util import lazyproperty
+
+
+class _BaseChartData(Sequence):
+    """Base class providing common members for chart data objects.
+
+    A chart data object serves as a proxy for the chart data table that will be written to an
+    Excel worksheet; operating as a sequence of series as well as providing access to chart-level
+    attributes. A chart data object is used as a parameter in :meth:`shapes.add_chart` and
+    :meth:`Chart.replace_data`. The data structure varies between major chart categories such as
+    category charts and XY charts.
+    """
+
+    def __init__(self, number_format="General"):
+        super(_BaseChartData, self).__init__()
+        self._number_format = number_format
+        self._series = []
+
+    def __getitem__(self, index):
+        return self._series.__getitem__(index)
+
+    def __len__(self):
+        return self._series.__len__()
+
+    def append(self, series):
+        return self._series.append(series)
+
+    def data_point_offset(self, series):
+        """
+        The total integer number of data points appearing in the series of
+        this chart that are prior to *series* in this sequence.
+        """
+        count = 0
+        for this_series in self:
+            if series is this_series:
+                return count
+            count += len(this_series)
+        raise ValueError("series not in chart data object")
+
+    @property
+    def number_format(self):
+        """
+        The formatting template string, e.g. '#,##0.0', that determines how
+        X and Y values are formatted in this chart and in the Excel
+        spreadsheet. A number format specified on a series will override this
+        value for that series. Likewise, a distinct number format can be
+        specified for a particular data point within a series.
+        """
+        return self._number_format
+
+    def series_index(self, series):
+        """
+        Return the integer index of *series* in this sequence.
+        """
+        for idx, s in enumerate(self):
+            if series is s:
+                return idx
+        raise ValueError("series not in chart data object")
+
+    def series_name_ref(self, series):
+        """
+        Return the Excel worksheet reference to the cell containing the name
+        for *series*.
+        """
+        return self._workbook_writer.series_name_ref(series)
+
+    def x_values_ref(self, series):
+        """
+        The Excel worksheet reference to the X values for *series* (not
+        including the column label).
+        """
+        return self._workbook_writer.x_values_ref(series)
+
+    @property
+    def xlsx_blob(self):
+        """
+        Return a blob containing an Excel workbook file populated with the
+        contents of this chart data object.
+        """
+        return self._workbook_writer.xlsx_blob
+
+    def xml_bytes(self, chart_type):
+        """
+        Return a blob containing the XML for a chart of *chart_type*
+        containing the series in this chart data object, as bytes suitable
+        for writing directly to a file.
+        """
+        return self._xml(chart_type).encode("utf-8")
+
+    def y_values_ref(self, series):
+        """
+        The Excel worksheet reference to the Y values for *series* (not
+        including the column label).
+        """
+        return self._workbook_writer.y_values_ref(series)
+
+    @property
+    def _workbook_writer(self):
+        """
+        The worksheet writer object to which layout and writing of the Excel
+        worksheet for this chart will be delegated.
+        """
+        raise NotImplementedError("must be implemented by all subclasses")
+
+    def _xml(self, chart_type):
+        """
+        Return (as unicode text) the XML for a chart of *chart_type*
+        populated with the values in this chart data object. The XML is
+        a complete XML document, including an XML declaration specifying
+        UTF-8 encoding.
+        """
+        return ChartXmlWriter(chart_type, self).xml
+
+
+class _BaseSeriesData(Sequence):
+    """
+    Base class providing common members for series data objects. A series
+    data object serves as proxy for a series data column in the Excel
+    worksheet. It operates as a sequence of data points, as well as providing
+    access to series-level attributes like the series label.
+    """
+
+    def __init__(self, chart_data, name, number_format):
+        self._chart_data = chart_data
+        self._name = name
+        self._number_format = number_format
+        self._data_points = []
+
+    def __getitem__(self, index):
+        return self._data_points.__getitem__(index)
+
+    def __len__(self):
+        return self._data_points.__len__()
+
+    def append(self, data_point):
+        return self._data_points.append(data_point)
+
+    @property
+    def data_point_offset(self):
+        """
+        The integer count of data points that appear in all chart series
+        prior to this one.
+        """
+        return self._chart_data.data_point_offset(self)
+
+    @property
+    def index(self):
+        """
+        Zero-based integer indicating the sequence position of this series in
+        its chart. For example, the second of three series would return `1`.
+        """
+        return self._chart_data.series_index(self)
+
+    @property
+    def name(self):
+        """
+        The name of this series, e.g. 'Series 1'. This name is used as the
+        column heading for the y-values of this series and may also appear in
+        the chart legend and perhaps other chart locations.
+        """
+        return self._name if self._name is not None else ""
+
+    @property
+    def name_ref(self):
+        """
+        The Excel worksheet reference to the cell containing the name for
+        this series.
+        """
+        return self._chart_data.series_name_ref(self)
+
+    @property
+    def number_format(self):
+        """
+        The formatting template string that determines how a number in this
+        series is formatted, both in the chart and in the Excel spreadsheet;
+        for example '#,##0.0'. If not specified for this series, it is
+        inherited from the parent chart data object.
+        """
+        number_format = self._number_format
+        if number_format is None:
+            return self._chart_data.number_format
+        return number_format
+
+    @property
+    def x_values(self):
+        """
+        A sequence containing the X value of each datapoint in this series,
+        in data point order.
+        """
+        return [dp.x for dp in self._data_points]
+
+    @property
+    def x_values_ref(self):
+        """
+        The Excel worksheet reference to the X values for this chart (not
+        including the column heading).
+        """
+        return self._chart_data.x_values_ref(self)
+
+    @property
+    def y_values(self):
+        """
+        A sequence containing the Y value of each datapoint in this series,
+        in data point order.
+        """
+        return [dp.y for dp in self._data_points]
+
+    @property
+    def y_values_ref(self):
+        """
+        The Excel worksheet reference to the Y values for this chart (not
+        including the column heading).
+        """
+        return self._chart_data.y_values_ref(self)
+
+
+class _BaseDataPoint(object):
+    """
+    Base class providing common members for data point objects.
+    """
+
+    def __init__(self, series_data, number_format):
+        super(_BaseDataPoint, self).__init__()
+        self._series_data = series_data
+        self._number_format = number_format
+
+    @property
+    def number_format(self):
+        """
+        The formatting template string that determines how the value of this
+        data point is formatted, both in the chart and in the Excel
+        spreadsheet; for example '#,##0.0'. If not specified for this data
+        point, it is inherited from the parent series data object.
+        """
+        number_format = self._number_format
+        if number_format is None:
+            return self._series_data.number_format
+        return number_format
+
+
+class CategoryChartData(_BaseChartData):
+    """
+    Accumulates data specifying the categories and series values for a chart
+    and acts as a proxy for the chart data table that will be written to an
+    Excel worksheet. Used as a parameter in :meth:`shapes.add_chart` and
+    :meth:`Chart.replace_data`.
+
+    This object is suitable for use with category charts, i.e. all those
+    having a discrete set of label values (categories) as the range of their
+    independent variable (X-axis) values. Unlike the ChartData types for
+    charts supporting a continuous range of independent variable values (such
+    as XyChartData), CategoryChartData has a single collection of category
+    (X) values and each data point in its series specifies only the Y value.
+    The corresponding X value is inferred by its position in the sequence.
+    """
+
+    def add_category(self, label):
+        """
+        Return a newly created |data.Category| object having *label* and
+        appended to the end of the category collection for this chart.
+        *label* can be a string, a number, a datetime.date, or
+        datetime.datetime object. All category labels in a chart must be the
+        same type. All category labels in a chart having multi-level
+        categories must be strings.
+        """
+        return self.categories.add_category(label)
+
+    def add_series(self, name, values=(), number_format=None):
+        """
+        Add a series to this data set entitled *name* and having the data
+        points specified by *values*, an iterable of numeric values.
+        *number_format* specifies how the series values will be displayed,
+        and may be a string, e.g. '#,##0' corresponding to an Excel number
+        format.
+        """
+        series_data = CategorySeriesData(self, name, number_format)
+        self.append(series_data)
+        for value in values:
+            series_data.add_data_point(value)
+        return series_data
+
+    @property
+    def categories(self):
+        """|data.Categories| object providing access to category-object hierarchy.
+
+        Assigning an iterable of category labels (strings, numbers, or dates) replaces
+        the |data.Categories| object with a new one containing a category for each label
+        in the sequence.
+
+        Creating a chart from chart data having date categories will cause the chart to
+        have a |DateAxis| for its category axis.
+        """
+        if not getattr(self, "_categories", False):
+            self._categories = Categories()
+        return self._categories
+
+    @categories.setter
+    def categories(self, category_labels):
+        categories = Categories()
+        for label in category_labels:
+            categories.add_category(label)
+        self._categories = categories
+
+    @property
+    def categories_ref(self):
+        """
+        The Excel worksheet reference to the categories for this chart (not
+        including the column heading).
+        """
+        return self._workbook_writer.categories_ref
+
+    def values_ref(self, series):
+        """
+        The Excel worksheet reference to the values for *series* (not
+        including the column heading).
+        """
+        return self._workbook_writer.values_ref(series)
+
+    @lazyproperty
+    def _workbook_writer(self):
+        """
+        The worksheet writer object to which layout and writing of the Excel
+        worksheet for this chart will be delegated.
+        """
+        return CategoryWorkbookWriter(self)
+
+
+class Categories(Sequence):
+    """
+    A sequence of |data.Category| objects, also having certain hierarchical
+    graph behaviors for support of multi-level (nested) categories.
+    """
+
+    def __init__(self):
+        super(Categories, self).__init__()
+        self._categories = []
+        self._number_format = None
+
+    def __getitem__(self, idx):
+        return self._categories.__getitem__(idx)
+
+    def __len__(self):
+        """
+        Return the count of the highest level of category in this sequence.
+        If it contains hierarchical (multi-level) categories, this number
+        will differ from :attr:`category_count`, which is the number of leaf
+        nodes.
+        """
+        return self._categories.__len__()
+
+    def add_category(self, label):
+        """
+        Return a newly created |data.Category| object having *label* and
+        appended to the end of this category sequence. *label* can be
+        a string, a number, a datetime.date, or datetime.datetime object. All
+        category labels in a chart must be the same type. All category labels
+        in a chart having multi-level categories must be strings.
+
+        Creating a chart from chart data having date categories will cause
+        the chart to have a |DateAxis| for its category axis.
+        """
+        category = Category(label, self)
+        self._categories.append(category)
+        return category
+
+    @property
+    def are_dates(self):
+        """
+        Return |True| if the first category in this collection has a date
+        label (as opposed to str or numeric). A date label is one of type
+        datetime.date or datetime.datetime. Returns |False| otherwise,
+        including when this category collection is empty. It also returns
+        False when this category collection is hierarchical, because
+        hierarchical categories can only be written as string labels.
+        """
+        if self.depth != 1:
+            return False
+        first_cat_label = self[0].label
+        date_types = (datetime.date, datetime.datetime)
+        if isinstance(first_cat_label, date_types):
+            return True
+        return False
+
+    @property
+    def are_numeric(self):
+        """
+        Return |True| if the first category in this collection has a numeric
+        label (as opposed to a string label), including if that value is
+        a datetime.date or datetime.datetime object (as those are converted
+        to integers for storage in Excel). Returns |False| otherwise,
+        including when this category collection is empty. It also returns
+        False when this category collection is hierarchical, because
+        hierarchical categories can only be written as string labels.
+        """
+        if self.depth != 1:
+            return False
+        # This method only tests the first category. The categories must
+        # be of uniform type, and if they're not, there will be problems
+        # later in the process, but it's not this method's job to validate
+        # the caller's input.
+        first_cat_label = self[0].label
+        numeric_types = (Number, datetime.date, datetime.datetime)
+        if isinstance(first_cat_label, numeric_types):
+            return True
+        return False
+
+    @property
+    def depth(self):
+        """
+        The number of hierarchy levels in this category graph. Returns 0 if
+        it contains no categories.
+        """
+        categories = self._categories
+        if not categories:
+            return 0
+        first_depth = categories[0].depth
+        for category in categories[1:]:
+            if category.depth != first_depth:
+                raise ValueError("category depth not uniform")
+        return first_depth
+
+    def index(self, category):
+        """
+        The offset of *category* in the overall sequence of leaf categories.
+        A non-leaf category gets the index of its first sub-category.
+        """
+        index = 0
+        for this_category in self._categories:
+            if category is this_category:
+                return index
+            index += this_category.leaf_count
+        raise ValueError("category not in top-level categories")
+
+    @property
+    def leaf_count(self):
+        """
+        The number of leaf-level categories in this hierarchy. The return
+        value is the same as that of `len()` only when the hierarchy is
+        single level.
+        """
+        return sum(c.leaf_count for c in self._categories)
+
+    @property
+    def levels(self):
+        """
+        A generator of (idx, label) sequences representing the category
+        hierarchy from the bottom up. The first level contains all leaf
+        categories, and each subsequent is the next level up.
+        """
+
+        def levels(categories):
+            # yield all lower levels
+            sub_categories = [sc for c in categories for sc in c.sub_categories]
+            if sub_categories:
+                for level in levels(sub_categories):
+                    yield level
+            # yield this level
+            yield [(cat.idx, cat.label) for cat in categories]
+
+        for level in levels(self):
+            yield level
+
+    @property
+    def number_format(self):
+        """
+        Read/write. Return a string representing the number format used in
+        Excel to format these category values, e.g. '0.0' or 'mm/dd/yyyy'.
+        This string is only relevant when the categories are numeric or date
+        type, although it returns 'General' without error when the categories
+        are string labels. Assigning |None| causes the default number format
+        to be used, based on the type of the category labels.
+        """
+        GENERAL = "General"
+
+        # defined value takes precedence
+        if self._number_format is not None:
+            return self._number_format
+
+        # multi-level (should) always be string labels
+        # zero depth means empty in which case we can't tell anyway
+        if self.depth != 1:
+            return GENERAL
+
+        # everything except dates gets 'General'
+        first_cat_label = self[0].label
+        if isinstance(first_cat_label, (datetime.date, datetime.datetime)):
+            return r"yyyy\-mm\-dd"
+        return GENERAL
+
+    @number_format.setter
+    def number_format(self, value):
+        self._number_format = value
+
+
+class Category(object):
+    """
+    A chart category, primarily having a label to be displayed on the
+    category axis, but also able to be configured in a hierarchy for support
+    of multi-level category charts.
+    """
+
+    def __init__(self, label, parent):
+        super(Category, self).__init__()
+        self._label = label
+        self._parent = parent
+        self._sub_categories = []
+
+    def add_sub_category(self, label):
+        """
+        Return a newly created |data.Category| object having *label* and
+        appended to the end of the sub-category sequence for this category.
+        """
+        category = Category(label, self)
+        self._sub_categories.append(category)
+        return category
+
+    @property
+    def depth(self):
+        """
+        The number of hierarchy levels rooted at this category node. Returns
+        1 if this category has no sub-categories.
+        """
+        sub_categories = self._sub_categories
+        if not sub_categories:
+            return 1
+        first_depth = sub_categories[0].depth
+        for category in sub_categories[1:]:
+            if category.depth != first_depth:
+                raise ValueError("category depth not uniform")
+        return first_depth + 1
+
+    @property
+    def idx(self):
+        """
+        The offset of this category in the overall sequence of leaf
+        categories. A non-leaf category gets the index of its first
+        sub-category.
+        """
+        return self._parent.index(self)
+
+    def index(self, sub_category):
+        """
+        The offset of *sub_category* in the overall sequence of leaf
+        categories.
+        """
+        index = self._parent.index(self)
+        for this_sub_category in self._sub_categories:
+            if sub_category is this_sub_category:
+                return index
+            index += this_sub_category.leaf_count
+        raise ValueError("sub_category not in this category")
+
+    @property
+    def leaf_count(self):
+        """
+        The number of leaf category nodes under this category. Returns
+        1 if this category has no sub-categories.
+        """
+        if not self._sub_categories:
+            return 1
+        return sum(category.leaf_count for category in self._sub_categories)
+
+    @property
+    def label(self):
+        """
+        The value that appears on the axis for this category. The label can
+        be a string, a number, or a datetime.date or datetime.datetime
+        object.
+        """
+        return self._label if self._label is not None else ""
+
+    def numeric_str_val(self, date_1904=False):
+        """
+        The string representation of the numeric (or date) label of this
+        category, suitable for use in the XML `c:pt` element for this
+        category. The optional *date_1904* parameter specifies the epoch used
+        for calculating Excel date numbers.
+        """
+        label = self._label
+        if isinstance(label, (datetime.date, datetime.datetime)):
+            return "%.1f" % self._excel_date_number(date_1904)
+        return str(self._label)
+
+    @property
+    def sub_categories(self):
+        """
+        The sequence of child categories for this category.
+        """
+        return self._sub_categories
+
+    def _excel_date_number(self, date_1904):
+        """
+        Return an integer representing the date label of this category as the
+        number of days since January 1, 1900 (or 1904 if date_1904 is
+        |True|).
+        """
+        date, label = datetime.date, self._label
+        # -- get date from label in type-independent-ish way
+        date_ = date(label.year, label.month, label.day)
+        epoch = date(1904, 1, 1) if date_1904 else date(1899, 12, 31)
+        delta = date_ - epoch
+        excel_day_number = delta.days
+
+        # -- adjust for Excel mistaking 1900 for a leap year --
+        if not date_1904 and excel_day_number > 59:
+            excel_day_number += 1
+
+        return excel_day_number
+
+
+class ChartData(CategoryChartData):
+    """
+    |ChartData| is simply an alias for |CategoryChartData| and may be removed
+    in a future release. All new development should use |CategoryChartData|
+    for creating or replacing the data in chart types other than XY and
+    Bubble.
+    """
+
+
+class CategorySeriesData(_BaseSeriesData):
+    """
+    The data specific to a particular category chart series. It provides
+    access to the series label, the series data points, and an optional
+    number format to be applied to each data point not having a specified
+    number format.
+    """
+
+    def add_data_point(self, value, number_format=None):
+        """
+        Return a CategoryDataPoint object newly created with value *value*,
+        an optional *number_format*, and appended to this sequence.
+        """
+        data_point = CategoryDataPoint(self, value, number_format)
+        self.append(data_point)
+        return data_point
+
+    @property
+    def categories(self):
+        """
+        The |data.Categories| object that provides access to the category
+        objects for this series.
+        """
+        return self._chart_data.categories
+
+    @property
+    def categories_ref(self):
+        """
+        The Excel worksheet reference to the categories for this chart (not
+        including the column heading).
+        """
+        return self._chart_data.categories_ref
+
+    @property
+    def values(self):
+        """
+        A sequence containing the (Y) value of each datapoint in this series,
+        in data point order.
+        """
+        return [dp.value for dp in self._data_points]
+
+    @property
+    def values_ref(self):
+        """
+        The Excel worksheet reference to the (Y) values for this series (not
+        including the column heading).
+        """
+        return self._chart_data.values_ref(self)
+
+
+class XyChartData(_BaseChartData):
+    """
+    A specialized ChartData object suitable for use with an XY (aka. scatter)
+    chart. Unlike ChartData, it has no category sequence. Rather, each data
+    point of each series specifies both an X and a Y value.
+    """
+
+    def add_series(self, name, number_format=None):
+        """
+        Return an |XySeriesData| object newly created and added at the end of
+        this sequence, identified by *name* and values formatted with
+        *number_format*.
+        """
+        series_data = XySeriesData(self, name, number_format)
+        self.append(series_data)
+        return series_data
+
+    @lazyproperty
+    def _workbook_writer(self):
+        """
+        The worksheet writer object to which layout and writing of the Excel
+        worksheet for this chart will be delegated.
+        """
+        return XyWorkbookWriter(self)
+
+
+class BubbleChartData(XyChartData):
+    """
+    A specialized ChartData object suitable for use with a bubble chart.
+    A bubble chart is essentially an XY chart where the markers are scaled to
+    provide a third quantitative dimension to the exhibit.
+    """
+
+    def add_series(self, name, number_format=None):
+        """
+        Return a |BubbleSeriesData| object newly created and added at the end
+        of this sequence, and having series named *name* and values formatted
+        with *number_format*.
+        """
+        series_data = BubbleSeriesData(self, name, number_format)
+        self.append(series_data)
+        return series_data
+
+    def bubble_sizes_ref(self, series):
+        """
+        The Excel worksheet reference for the range containing the bubble
+        sizes for *series*.
+        """
+        return self._workbook_writer.bubble_sizes_ref(series)
+
+    @lazyproperty
+    def _workbook_writer(self):
+        """
+        The worksheet writer object to which layout and writing of the Excel
+        worksheet for this chart will be delegated.
+        """
+        return BubbleWorkbookWriter(self)
+
+
+class XySeriesData(_BaseSeriesData):
+    """
+    The data specific to a particular XY chart series. It provides access to
+    the series label, the series data points, and an optional number format
+    to be applied to each data point not having a specified number format.
+
+    The sequence of data points in an XY series is significant; lines are
+    plotted following the sequence of points, even if that causes a line
+    segment to "travel backward" (implying a multi-valued function). The data
+    points are not automatically sorted into increasing order by X value.
+    """
+
+    def add_data_point(self, x, y, number_format=None):
+        """
+        Return an XyDataPoint object newly created with values *x* and *y*,
+        and appended to this sequence.
+        """
+        data_point = XyDataPoint(self, x, y, number_format)
+        self.append(data_point)
+        return data_point
+
+
+class BubbleSeriesData(XySeriesData):
+    """
+    The data specific to a particular Bubble chart series. It provides access
+    to the series label, the series data points, and an optional number
+    format to be applied to each data point not having a specified number
+    format.
+
+    The sequence of data points in a bubble chart series is maintained
+    throughout the chart building process because a data point has no unique
+    identifier and can only be retrieved by index.
+    """
+
+    def add_data_point(self, x, y, size, number_format=None):
+        """
+        Append a new BubbleDataPoint object having the values *x*, *y*, and
+        *size*. The optional *number_format* is used to format the Y value.
+        If not provided, the number format is inherited from the series data.
+        """
+        data_point = BubbleDataPoint(self, x, y, size, number_format)
+        self.append(data_point)
+        return data_point
+
+    @property
+    def bubble_sizes(self):
+        """
+        A sequence containing the bubble size for each datapoint in this
+        series, in data point order.
+        """
+        return [dp.bubble_size for dp in self._data_points]
+
+    @property
+    def bubble_sizes_ref(self):
+        """
+        The Excel worksheet reference for the range containing the bubble
+        sizes for this series.
+        """
+        return self._chart_data.bubble_sizes_ref(self)
+
+
+class CategoryDataPoint(_BaseDataPoint):
+    """
+    A data point in a category chart series. Provides access to the value of
+    the datapoint and the number format with which it should appear in the
+    Excel file.
+    """
+
+    def __init__(self, series_data, value, number_format):
+        super(CategoryDataPoint, self).__init__(series_data, number_format)
+        self._value = value
+
+    @property
+    def value(self):
+        """
+        The (Y) value for this category data point.
+        """
+        return self._value
+
+
+class XyDataPoint(_BaseDataPoint):
+    """
+    A data point in an XY chart series. Provides access to the x and y values
+    of the datapoint.
+    """
+
+    def __init__(self, series_data, x, y, number_format):
+        super(XyDataPoint, self).__init__(series_data, number_format)
+        self._x = x
+        self._y = y
+
+    @property
+    def x(self):
+        """
+        The X value for this XY data point.
+        """
+        return self._x
+
+    @property
+    def y(self):
+        """
+        The Y value for this XY data point.
+        """
+        return self._y
+
+
+class BubbleDataPoint(XyDataPoint):
+    """
+    A data point in a bubble chart series. Provides access to the x, y, and
+    size values of the datapoint.
+    """
+
+    def __init__(self, series_data, x, y, size, number_format):
+        super(BubbleDataPoint, self).__init__(series_data, x, y, number_format)
+        self._size = size
+
+    @property
+    def bubble_size(self):
+        """
+        The value representing the size of the bubble for this data point.
+        """
+        return self._size
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/datalabel.py b/.venv/lib/python3.12/site-packages/pptx/chart/datalabel.py
new file mode 100644
index 00000000..af7cdf5c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/datalabel.py
@@ -0,0 +1,288 @@
+"""Data label-related objects."""
+
+from __future__ import annotations
+
+from pptx.text.text import Font, TextFrame
+from pptx.util import lazyproperty
+
+
+class DataLabels(object):
+    """Provides access to properties of data labels for a plot or a series.
+
+    This is not a collection and does not provide access to individual data
+    labels. Access to individual labels is via the |Point| object. The
+    properties this object provides control formatting of *all* the data
+    labels in its scope.
+    """
+
+    def __init__(self, dLbls):
+        super(DataLabels, self).__init__()
+        self._element = dLbls
+
+    @lazyproperty
+    def font(self):
+        """
+        The |Font| object that provides access to the text properties for
+        these data labels, such as bold, italic, etc.
+        """
+        defRPr = self._element.defRPr
+        font = Font(defRPr)
+        return font
+
+    @property
+    def number_format(self):
+        """
+        Read/write string specifying the format for the numbers on this set
+        of data labels. Returns 'General' if no number format has been set.
+        Note that this format string has no effect on rendered data labels
+        when :meth:`number_format_is_linked` is |True|. Assigning a format
+        string to this property automatically sets
+        :meth:`number_format_is_linked` to |False|.
+        """
+        numFmt = self._element.numFmt
+        if numFmt is None:
+            return "General"
+        return numFmt.formatCode
+
+    @number_format.setter
+    def number_format(self, value):
+        self._element.get_or_add_numFmt().formatCode = value
+        self.number_format_is_linked = False
+
+    @property
+    def number_format_is_linked(self):
+        """
+        Read/write boolean specifying whether number formatting should be
+        taken from the source spreadsheet rather than the value of
+        :meth:`number_format`.
+        """
+        numFmt = self._element.numFmt
+        if numFmt is None:
+            return True
+        souceLinked = numFmt.sourceLinked
+        if souceLinked is None:
+            return True
+        return numFmt.sourceLinked
+
+    @number_format_is_linked.setter
+    def number_format_is_linked(self, value):
+        numFmt = self._element.get_or_add_numFmt()
+        numFmt.sourceLinked = value
+
+    @property
+    def position(self):
+        """
+        Read/write :ref:`XlDataLabelPosition` enumeration value specifying
+        the position of the data labels with respect to their data point, or
+        |None| if no position is specified. Assigning |None| causes
+        PowerPoint to choose the default position, which varies by chart
+        type.
+        """
+        dLblPos = self._element.dLblPos
+        if dLblPos is None:
+            return None
+        return dLblPos.val
+
+    @position.setter
+    def position(self, value):
+        if value is None:
+            self._element._remove_dLblPos()
+            return
+        self._element.get_or_add_dLblPos().val = value
+
+    @property
+    def show_category_name(self):
+        """Read/write. True when name of category should appear in label."""
+        return self._element.get_or_add_showCatName().val
+
+    @show_category_name.setter
+    def show_category_name(self, value):
+        self._element.get_or_add_showCatName().val = bool(value)
+
+    @property
+    def show_legend_key(self):
+        """Read/write. True when data label displays legend-color swatch."""
+        return self._element.get_or_add_showLegendKey().val
+
+    @show_legend_key.setter
+    def show_legend_key(self, value):
+        self._element.get_or_add_showLegendKey().val = bool(value)
+
+    @property
+    def show_percentage(self):
+        """Read/write. True when data label displays percentage.
+
+        This option is not operative on all chart types. Percentage appears
+        on polar charts such as pie and donut.
+        """
+        return self._element.get_or_add_showPercent().val
+
+    @show_percentage.setter
+    def show_percentage(self, value):
+        self._element.get_or_add_showPercent().val = bool(value)
+
+    @property
+    def show_series_name(self):
+        """Read/write. True when data label displays series name."""
+        return self._element.get_or_add_showSerName().val
+
+    @show_series_name.setter
+    def show_series_name(self, value):
+        self._element.get_or_add_showSerName().val = bool(value)
+
+    @property
+    def show_value(self):
+        """Read/write. True when label displays numeric value of datapoint."""
+        return self._element.get_or_add_showVal().val
+
+    @show_value.setter
+    def show_value(self, value):
+        self._element.get_or_add_showVal().val = bool(value)
+
+
+class DataLabel(object):
+    """
+    The data label associated with an individual data point.
+    """
+
+    def __init__(self, ser, idx):
+        super(DataLabel, self).__init__()
+        self._ser = self._element = ser
+        self._idx = idx
+
+    @lazyproperty
+    def font(self):
+        """The |Font| object providing text formatting for this data label.
+
+        This font object is used to customize the appearance of automatically
+        inserted text, such as the data point value. The font applies to the
+        entire data label. More granular control of the appearance of custom
+        data label text is controlled by a font object on runs in the text
+        frame.
+        """
+        txPr = self._get_or_add_txPr()
+        text_frame = TextFrame(txPr, self)
+        paragraph = text_frame.paragraphs[0]
+        return paragraph.font
+
+    @property
+    def has_text_frame(self):
+        """
+        Return |True| if this data label has a text frame (implying it has
+        custom data label text), and |False| otherwise. Assigning |True|
+        causes a text frame to be added if not already present. Assigning
+        |False| causes any existing text frame to be removed along with any
+        text contained in the text frame.
+        """
+        dLbl = self._dLbl
+        if dLbl is None:
+            return False
+        if dLbl.xpath("c:tx/c:rich"):
+            return True
+        return False
+
+    @has_text_frame.setter
+    def has_text_frame(self, value):
+        if bool(value) is True:
+            self._get_or_add_tx_rich()
+        else:
+            self._remove_tx_rich()
+
+    @property
+    def position(self):
+        """
+        Read/write :ref:`XlDataLabelPosition` member specifying the position
+        of this data label with respect to its data point, or |None| if no
+        position is specified. Assigning |None| causes PowerPoint to choose
+        the default position, which varies by chart type.
+        """
+        dLbl = self._dLbl
+        if dLbl is None:
+            return None
+        dLblPos = dLbl.dLblPos
+        if dLblPos is None:
+            return None
+        return dLblPos.val
+
+    @position.setter
+    def position(self, value):
+        if value is None:
+            dLbl = self._dLbl
+            if dLbl is None:
+                return
+            dLbl._remove_dLblPos()
+            return
+        dLbl = self._get_or_add_dLbl()
+        dLbl.get_or_add_dLblPos().val = value
+
+    @property
+    def text_frame(self):
+        """
+        |TextFrame| instance for this data label, containing the text of the
+        data label and providing access to its text formatting properties.
+        """
+        rich = self._get_or_add_rich()
+        return TextFrame(rich, self)
+
+    @property
+    def _dLbl(self):
+        """
+        Return the |CT_DLbl| instance referring specifically to this
+        individual data label (having the same index value), or |None| if not
+        present.
+        """
+        return self._ser.get_dLbl(self._idx)
+
+    def _get_or_add_dLbl(self):
+        """
+        The ``CT_DLbl`` instance referring specifically to this individual
+        data label, newly created if not yet present in the XML.
+        """
+        return self._ser.get_or_add_dLbl(self._idx)
+
+    def _get_or_add_rich(self):
+        """
+        Return the `c:rich` element representing the text frame for this data
+        label, newly created with its ancestors if not present.
+        """
+        dLbl = self._get_or_add_dLbl()
+
+        # having a c:spPr or c:txPr when a c:tx is present causes the "can't
+        # save" bug on bubble charts. Remove c:spPr and c:txPr when present.
+        dLbl._remove_spPr()
+        dLbl._remove_txPr()
+
+        return dLbl.get_or_add_rich()
+
+    def _get_or_add_tx_rich(self):
+        """
+        Return the `c:tx` element for this data label, with its `c:rich`
+        child and descendants, newly created if not yet present.
+        """
+        dLbl = self._get_or_add_dLbl()
+
+        # having a c:spPr or c:txPr when a c:tx is present causes the "can't
+        # save" bug on bubble charts. Remove c:spPr and c:txPr when present.
+        dLbl._remove_spPr()
+        dLbl._remove_txPr()
+
+        return dLbl.get_or_add_tx_rich()
+
+    def _get_or_add_txPr(self):
+        """Return the `c:txPr` element for this data label.
+
+        The `c:txPr` element and its parent `c:dLbl` element are created if
+        not yet present.
+        """
+        dLbl = self._get_or_add_dLbl()
+        return dLbl.get_or_add_txPr()
+
+    def _remove_tx_rich(self):
+        """
+        Remove any `c:tx/c:rich` child of the `c:dLbl` element for this data
+        label. Do nothing if that element is not present.
+        """
+        dLbl = self._dLbl
+        if dLbl is None:
+            return
+        dLbl.remove_tx_rich()
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/legend.py b/.venv/lib/python3.12/site-packages/pptx/chart/legend.py
new file mode 100644
index 00000000..9bc64dbf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/legend.py
@@ -0,0 +1,79 @@
+"""Legend of a chart."""
+
+from __future__ import annotations
+
+from pptx.enum.chart import XL_LEGEND_POSITION
+from pptx.text.text import Font
+from pptx.util import lazyproperty
+
+
+class Legend(object):
+    """
+    Represents the legend in a chart. A chart can have at most one legend.
+    """
+
+    def __init__(self, legend_elm):
+        super(Legend, self).__init__()
+        self._element = legend_elm
+
+    @lazyproperty
+    def font(self):
+        """
+        The |Font| object that provides access to the text properties for
+        this legend, such as bold, italic, etc.
+        """
+        defRPr = self._element.defRPr
+        font = Font(defRPr)
+        return font
+
+    @property
+    def horz_offset(self):
+        """
+        Adjustment of the x position of the legend from its default.
+        Expressed as a float between -1.0 and 1.0 representing a fraction of
+        the chart width. Negative values move the legend left, positive
+        values move it to the right. |None| if no setting is specified.
+        """
+        return self._element.horz_offset
+
+    @horz_offset.setter
+    def horz_offset(self, value):
+        self._element.horz_offset = value
+
+    @property
+    def include_in_layout(self):
+        """|True| if legend should be located inside plot area.
+
+        Read/write boolean specifying whether legend should be placed inside
+        the plot area. In many cases this will cause it to be superimposed on
+        the chart itself. Assigning |None| to this property causes any
+        `c:overlay` element to be removed, which is interpreted the same as
+        |True|. This use case should rarely be required and assigning
+        a boolean value is recommended.
+        """
+        overlay = self._element.overlay
+        if overlay is None:
+            return True
+        return overlay.val
+
+    @include_in_layout.setter
+    def include_in_layout(self, value):
+        if value is None:
+            self._element._remove_overlay()
+            return
+        self._element.get_or_add_overlay().val = bool(value)
+
+    @property
+    def position(self):
+        """
+        Read/write :ref:`XlLegendPosition` enumeration value specifying the
+        general region of the chart in which to place the legend.
+        """
+        legendPos = self._element.legendPos
+        if legendPos is None:
+            return XL_LEGEND_POSITION.RIGHT
+        return legendPos.val
+
+    @position.setter
+    def position(self, position):
+        self._element.get_or_add_legendPos().val = position
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/marker.py b/.venv/lib/python3.12/site-packages/pptx/chart/marker.py
new file mode 100644
index 00000000..cd4b7f02
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/marker.py
@@ -0,0 +1,70 @@
+"""Marker-related objects.
+
+Only the line-type charts Line, XY, and Radar have markers.
+"""
+
+from __future__ import annotations
+
+from pptx.dml.chtfmt import ChartFormat
+from pptx.shared import ElementProxy
+from pptx.util import lazyproperty
+
+
+class Marker(ElementProxy):
+    """
+    Represents a data point marker, such as a diamond or circle, on
+    a line-type chart.
+    """
+
+    @lazyproperty
+    def format(self):
+        """
+        The |ChartFormat| instance for this marker, providing access to shape
+        properties such as fill and line.
+        """
+        marker = self._element.get_or_add_marker()
+        return ChartFormat(marker)
+
+    @property
+    def size(self):
+        """
+        An integer between 2 and 72 inclusive indicating the size of this
+        marker in points. A value of |None| indicates no explicit value is
+        set and the size is inherited from a higher-level setting or the
+        PowerPoint default (which may be 9). Assigning |None| removes any
+        explicitly assigned size, causing this value to be inherited.
+        """
+        marker = self._element.marker
+        if marker is None:
+            return None
+        return marker.size_val
+
+    @size.setter
+    def size(self, value):
+        marker = self._element.get_or_add_marker()
+        marker._remove_size()
+        if value is None:
+            return
+        size = marker._add_size()
+        size.val = value
+
+    @property
+    def style(self):
+        """
+        A member of the :ref:`XlMarkerStyle` enumeration indicating the shape
+        of this marker. Returns |None| if no explicit style has been set,
+        which corresponds to the "Automatic" option in the PowerPoint UI.
+        """
+        marker = self._element.marker
+        if marker is None:
+            return None
+        return marker.symbol_val
+
+    @style.setter
+    def style(self, value):
+        marker = self._element.get_or_add_marker()
+        marker._remove_symbol()
+        if value is None:
+            return
+        symbol = marker._add_symbol()
+        symbol.val = value
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/plot.py b/.venv/lib/python3.12/site-packages/pptx/chart/plot.py
new file mode 100644
index 00000000..6e723585
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/plot.py
@@ -0,0 +1,412 @@
+"""Plot-related objects.
+
+A plot is known as a chart group in the MS API. A chart can have more than one plot overlayed on
+each other, such as a line plot layered over a bar plot.
+"""
+
+from __future__ import annotations
+
+from pptx.chart.category import Categories
+from pptx.chart.datalabel import DataLabels
+from pptx.chart.series import SeriesCollection
+from pptx.enum.chart import XL_CHART_TYPE as XL
+from pptx.oxml.ns import qn
+from pptx.oxml.simpletypes import ST_BarDir, ST_Grouping
+from pptx.util import lazyproperty
+
+
+class _BasePlot(object):
+    """
+    A distinct plot that appears in the plot area of a chart. A chart may
+    have more than one plot, in which case they appear as superimposed
+    layers, such as a line plot appearing on top of a bar chart.
+    """
+
+    def __init__(self, xChart, chart):
+        super(_BasePlot, self).__init__()
+        self._element = xChart
+        self._chart = chart
+
+    @lazyproperty
+    def categories(self):
+        """
+        Returns a |category.Categories| sequence object containing
+        a |category.Category| object for each of the category labels
+        associated with this plot. The |category.Category| class derives from
+        ``str``, so the returned value can be treated as a simple sequence of
+        strings for the common case where all you need is the labels in the
+        order they appear on the chart. |category.Categories| provides
+        additional properties for dealing with hierarchical categories when
+        required.
+        """
+        return Categories(self._element)
+
+    @property
+    def chart(self):
+        """
+        The |Chart| object containing this plot.
+        """
+        return self._chart
+
+    @property
+    def data_labels(self):
+        """
+        |DataLabels| instance providing properties and methods on the
+        collection of data labels associated with this plot.
+        """
+        dLbls = self._element.dLbls
+        if dLbls is None:
+            raise ValueError("plot has no data labels, set has_data_labels = True first")
+        return DataLabels(dLbls)
+
+    @property
+    def has_data_labels(self):
+        """
+        Read/write boolean, |True| if the series has data labels. Assigning
+        |True| causes data labels to be added to the plot. Assigning False
+        removes any existing data labels.
+        """
+        return self._element.dLbls is not None
+
+    @has_data_labels.setter
+    def has_data_labels(self, value):
+        """
+        Add, remove, or leave alone the ``<c:dLbls>`` child element depending
+        on current state and assigned *value*. If *value* is |True| and no
+        ``<c:dLbls>`` element is present, a new default element is added with
+        default child elements and settings. When |False|, any existing dLbls
+        element is removed.
+        """
+        if bool(value) is False:
+            self._element._remove_dLbls()
+        else:
+            if self._element.dLbls is None:
+                dLbls = self._element._add_dLbls()
+                dLbls.showVal.val = True
+
+    @lazyproperty
+    def series(self):
+        """
+        A sequence of |Series| objects representing the series in this plot,
+        in the order they appear in the plot.
+        """
+        return SeriesCollection(self._element)
+
+    @property
+    def vary_by_categories(self):
+        """
+        Read/write boolean value specifying whether to use a different color
+        for each of the points in this plot. Only effective when there is
+        a single series; PowerPoint automatically varies color by series when
+        more than one series is present.
+        """
+        varyColors = self._element.varyColors
+        if varyColors is None:
+            return True
+        return varyColors.val
+
+    @vary_by_categories.setter
+    def vary_by_categories(self, value):
+        self._element.get_or_add_varyColors().val = bool(value)
+
+
+class AreaPlot(_BasePlot):
+    """
+    An area plot.
+    """
+
+
+class Area3DPlot(_BasePlot):
+    """
+    A 3-dimensional area plot.
+    """
+
+
+class BarPlot(_BasePlot):
+    """
+    A bar chart-style plot.
+    """
+
+    @property
+    def gap_width(self):
+        """
+        Width of gap between bar(s) of each category, as an integer
+        percentage of the bar width. The default value for a new bar chart is
+        150, representing 150% or 1.5 times the width of a single bar.
+        """
+        gapWidth = self._element.gapWidth
+        if gapWidth is None:
+            return 150
+        return gapWidth.val
+
+    @gap_width.setter
+    def gap_width(self, value):
+        gapWidth = self._element.get_or_add_gapWidth()
+        gapWidth.val = value
+
+    @property
+    def overlap(self):
+        """
+        Read/write int value in range -100..100 specifying a percentage of
+        the bar width by which to overlap adjacent bars in a multi-series bar
+        chart. Default is 0. A setting of -100 creates a gap of a full bar
+        width and a setting of 100 causes all the bars in a category to be
+        superimposed. A stacked bar plot has overlap of 100 by default.
+        """
+        overlap = self._element.overlap
+        if overlap is None:
+            return 0
+        return overlap.val
+
+    @overlap.setter
+    def overlap(self, value):
+        """
+        Set the value of the ``<c:overlap>`` child element to *int_value*,
+        or remove the overlap element if *int_value* is 0.
+        """
+        if value == 0:
+            self._element._remove_overlap()
+            return
+        self._element.get_or_add_overlap().val = value
+
+
+class BubblePlot(_BasePlot):
+    """
+    A bubble chart plot.
+    """
+
+    @property
+    def bubble_scale(self):
+        """
+        An integer between 0 and 300 inclusive indicating the percentage of
+        the default size at which bubbles should be displayed. Assigning
+        |None| produces the same behavior as assigning `100`.
+        """
+        bubbleScale = self._element.bubbleScale
+        if bubbleScale is None:
+            return 100
+        return bubbleScale.val
+
+    @bubble_scale.setter
+    def bubble_scale(self, value):
+        bubbleChart = self._element
+        bubbleChart._remove_bubbleScale()
+        if value is None:
+            return
+        bubbleScale = bubbleChart._add_bubbleScale()
+        bubbleScale.val = value
+
+
+class DoughnutPlot(_BasePlot):
+    """
+    An doughnut plot.
+    """
+
+
+class LinePlot(_BasePlot):
+    """
+    A line chart-style plot.
+    """
+
+
+class PiePlot(_BasePlot):
+    """
+    A pie chart-style plot.
+    """
+
+
+class RadarPlot(_BasePlot):
+    """
+    A radar-style plot.
+    """
+
+
+class XyPlot(_BasePlot):
+    """
+    An XY (scatter) plot.
+    """
+
+
+def PlotFactory(xChart, chart):
+    """
+    Return an instance of the appropriate subclass of _BasePlot based on the
+    tagname of *xChart*.
+    """
+    try:
+        PlotCls = {
+            qn("c:areaChart"): AreaPlot,
+            qn("c:area3DChart"): Area3DPlot,
+            qn("c:barChart"): BarPlot,
+            qn("c:bubbleChart"): BubblePlot,
+            qn("c:doughnutChart"): DoughnutPlot,
+            qn("c:lineChart"): LinePlot,
+            qn("c:pieChart"): PiePlot,
+            qn("c:radarChart"): RadarPlot,
+            qn("c:scatterChart"): XyPlot,
+        }[xChart.tag]
+    except KeyError:
+        raise ValueError("unsupported plot type %s" % xChart.tag)
+
+    return PlotCls(xChart, chart)
+
+
+class PlotTypeInspector(object):
+    """
+    "One-shot" service object that knows how to identify the type of a plot
+    as a member of the XL_CHART_TYPE enumeration.
+    """
+
+    @classmethod
+    def chart_type(cls, plot):
+        """
+        Return the member of :ref:`XlChartType` that corresponds to the chart
+        type of *plot*.
+        """
+        try:
+            chart_type_method = {
+                "AreaPlot": cls._differentiate_area_chart_type,
+                "Area3DPlot": cls._differentiate_area_3d_chart_type,
+                "BarPlot": cls._differentiate_bar_chart_type,
+                "BubblePlot": cls._differentiate_bubble_chart_type,
+                "DoughnutPlot": cls._differentiate_doughnut_chart_type,
+                "LinePlot": cls._differentiate_line_chart_type,
+                "PiePlot": cls._differentiate_pie_chart_type,
+                "RadarPlot": cls._differentiate_radar_chart_type,
+                "XyPlot": cls._differentiate_xy_chart_type,
+            }[plot.__class__.__name__]
+        except KeyError:
+            raise NotImplementedError(
+                "chart_type() not implemented for %s" % plot.__class__.__name__
+            )
+        return chart_type_method(plot)
+
+    @classmethod
+    def _differentiate_area_3d_chart_type(cls, plot):
+        return {
+            ST_Grouping.STANDARD: XL.THREE_D_AREA,
+            ST_Grouping.STACKED: XL.THREE_D_AREA_STACKED,
+            ST_Grouping.PERCENT_STACKED: XL.THREE_D_AREA_STACKED_100,
+        }[plot._element.grouping_val]
+
+    @classmethod
+    def _differentiate_area_chart_type(cls, plot):
+        return {
+            ST_Grouping.STANDARD: XL.AREA,
+            ST_Grouping.STACKED: XL.AREA_STACKED,
+            ST_Grouping.PERCENT_STACKED: XL.AREA_STACKED_100,
+        }[plot._element.grouping_val]
+
+    @classmethod
+    def _differentiate_bar_chart_type(cls, plot):
+        barChart = plot._element
+        if barChart.barDir.val == ST_BarDir.BAR:
+            return {
+                ST_Grouping.CLUSTERED: XL.BAR_CLUSTERED,
+                ST_Grouping.STACKED: XL.BAR_STACKED,
+                ST_Grouping.PERCENT_STACKED: XL.BAR_STACKED_100,
+            }[barChart.grouping_val]
+        if barChart.barDir.val == ST_BarDir.COL:
+            return {
+                ST_Grouping.CLUSTERED: XL.COLUMN_CLUSTERED,
+                ST_Grouping.STACKED: XL.COLUMN_STACKED,
+                ST_Grouping.PERCENT_STACKED: XL.COLUMN_STACKED_100,
+            }[barChart.grouping_val]
+        raise ValueError("invalid barChart.barDir value '%s'" % barChart.barDir.val)
+
+    @classmethod
+    def _differentiate_bubble_chart_type(cls, plot):
+        def first_bubble3D(bubbleChart):
+            results = bubbleChart.xpath("c:ser/c:bubble3D")
+            return results[0] if results else None
+
+        bubbleChart = plot._element
+        bubble3D = first_bubble3D(bubbleChart)
+
+        if bubble3D is None:
+            return XL.BUBBLE
+        if bubble3D.val:
+            return XL.BUBBLE_THREE_D_EFFECT
+        return XL.BUBBLE
+
+    @classmethod
+    def _differentiate_doughnut_chart_type(cls, plot):
+        doughnutChart = plot._element
+        explosion = doughnutChart.xpath("./c:ser/c:explosion")
+        return XL.DOUGHNUT_EXPLODED if explosion else XL.DOUGHNUT
+
+    @classmethod
+    def _differentiate_line_chart_type(cls, plot):
+        lineChart = plot._element
+
+        def has_line_markers():
+            matches = lineChart.xpath('c:ser/c:marker/c:symbol[@val="none"]')
+            if matches:
+                return False
+            return True
+
+        if has_line_markers():
+            return {
+                ST_Grouping.STANDARD: XL.LINE_MARKERS,
+                ST_Grouping.STACKED: XL.LINE_MARKERS_STACKED,
+                ST_Grouping.PERCENT_STACKED: XL.LINE_MARKERS_STACKED_100,
+            }[plot._element.grouping_val]
+        else:
+            return {
+                ST_Grouping.STANDARD: XL.LINE,
+                ST_Grouping.STACKED: XL.LINE_STACKED,
+                ST_Grouping.PERCENT_STACKED: XL.LINE_STACKED_100,
+            }[plot._element.grouping_val]
+
+    @classmethod
+    def _differentiate_pie_chart_type(cls, plot):
+        pieChart = plot._element
+        explosion = pieChart.xpath("./c:ser/c:explosion")
+        return XL.PIE_EXPLODED if explosion else XL.PIE
+
+    @classmethod
+    def _differentiate_radar_chart_type(cls, plot):
+        radarChart = plot._element
+        radar_style = radarChart.xpath("c:radarStyle")[0].get("val")
+
+        def noMarkers():
+            matches = radarChart.xpath("c:ser/c:marker/c:symbol")
+            if matches and matches[0].get("val") == "none":
+                return True
+            return False
+
+        if radar_style is None:
+            return XL.RADAR
+        if radar_style == "filled":
+            return XL.RADAR_FILLED
+        if noMarkers():
+            return XL.RADAR
+        return XL.RADAR_MARKERS
+
+    @classmethod
+    def _differentiate_xy_chart_type(cls, plot):
+        scatterChart = plot._element
+
+        def noLine():
+            return bool(scatterChart.xpath("c:ser/c:spPr/a:ln/a:noFill"))
+
+        def noMarkers():
+            symbols = scatterChart.xpath("c:ser/c:marker/c:symbol")
+            if symbols and symbols[0].get("val") == "none":
+                return True
+            return False
+
+        scatter_style = scatterChart.xpath("c:scatterStyle")[0].get("val")
+
+        if scatter_style == "lineMarker":
+            if noLine():
+                return XL.XY_SCATTER
+            if noMarkers():
+                return XL.XY_SCATTER_LINES_NO_MARKERS
+            return XL.XY_SCATTER_LINES
+
+        if scatter_style == "smoothMarker":
+            if noMarkers():
+                return XL.XY_SCATTER_SMOOTH_NO_MARKERS
+            return XL.XY_SCATTER_SMOOTH
+
+        return XL.XY_SCATTER
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/point.py b/.venv/lib/python3.12/site-packages/pptx/chart/point.py
new file mode 100644
index 00000000..2d42436c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/point.py
@@ -0,0 +1,101 @@
+"""Data point-related objects."""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+from pptx.chart.datalabel import DataLabel
+from pptx.chart.marker import Marker
+from pptx.dml.chtfmt import ChartFormat
+from pptx.util import lazyproperty
+
+
+class _BasePoints(Sequence):
+    """
+    Sequence providing access to the individual data points in a series.
+    """
+
+    def __init__(self, ser):
+        super(_BasePoints, self).__init__()
+        self._element = ser
+        self._ser = ser
+
+    def __getitem__(self, idx):
+        if idx < 0 or idx >= self.__len__():
+            raise IndexError("point index out of range")
+        return Point(self._ser, idx)
+
+
+class BubblePoints(_BasePoints):
+    """
+    Sequence providing access to the individual data points in
+    a |BubbleSeries| object.
+    """
+
+    def __len__(self):
+        return min(
+            self._ser.xVal_ptCount_val,
+            self._ser.yVal_ptCount_val,
+            self._ser.bubbleSize_ptCount_val,
+        )
+
+
+class CategoryPoints(_BasePoints):
+    """
+    Sequence providing access to individual |Point| objects, each
+    representing the visual properties of a data point in the specified
+    category series.
+    """
+
+    def __len__(self):
+        return self._ser.cat_ptCount_val
+
+
+class Point(object):
+    """
+    Provides access to the properties of an individual data point in
+    a series, such as the visual properties of its marker and the text and
+    font of its data label.
+    """
+
+    def __init__(self, ser, idx):
+        super(Point, self).__init__()
+        self._element = ser
+        self._ser = ser
+        self._idx = idx
+
+    @lazyproperty
+    def data_label(self):
+        """
+        The |DataLabel| object representing the label on this data point.
+        """
+        return DataLabel(self._ser, self._idx)
+
+    @lazyproperty
+    def format(self):
+        """
+        The |ChartFormat| object providing access to the shape formatting
+        properties of this data point, such as line and fill.
+        """
+        dPt = self._ser.get_or_add_dPt_for_point(self._idx)
+        return ChartFormat(dPt)
+
+    @lazyproperty
+    def marker(self):
+        """
+        The |Marker| instance for this point, providing access to the visual
+        properties of the data point marker, such as fill and line. Setting
+        these properties overrides any value set at the series level.
+        """
+        dPt = self._ser.get_or_add_dPt_for_point(self._idx)
+        return Marker(dPt)
+
+
+class XyPoints(_BasePoints):
+    """
+    Sequence providing access to the individual data points in an |XySeries|
+    object.
+    """
+
+    def __len__(self):
+        return min(self._ser.xVal_ptCount_val, self._ser.yVal_ptCount_val)
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/series.py b/.venv/lib/python3.12/site-packages/pptx/chart/series.py
new file mode 100644
index 00000000..16112eab
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/series.py
@@ -0,0 +1,258 @@
+"""Series-related objects."""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+from pptx.chart.datalabel import DataLabels
+from pptx.chart.marker import Marker
+from pptx.chart.point import BubblePoints, CategoryPoints, XyPoints
+from pptx.dml.chtfmt import ChartFormat
+from pptx.oxml.ns import qn
+from pptx.util import lazyproperty
+
+
+class _BaseSeries(object):
+    """
+    Base class for |BarSeries| and other series classes.
+    """
+
+    def __init__(self, ser):
+        super(_BaseSeries, self).__init__()
+        self._element = ser
+        self._ser = ser
+
+    @lazyproperty
+    def format(self):
+        """
+        The |ChartFormat| instance for this series, providing access to shape
+        properties such as fill and line.
+        """
+        return ChartFormat(self._ser)
+
+    @property
+    def index(self):
+        """
+        The zero-based integer index of this series as reported in its
+        `c:ser/c:idx` element.
+        """
+        return self._element.idx.val
+
+    @property
+    def name(self):
+        """
+        The string label given to this series, appears as the title of the
+        column for this series in the Excel worksheet. It also appears as the
+        label for this series in the legend.
+        """
+        names = self._element.xpath("./c:tx//c:pt/c:v/text()")
+        name = names[0] if names else ""
+        return name
+
+
+class _BaseCategorySeries(_BaseSeries):
+    """Base class for |BarSeries| and other category chart series classes."""
+
+    @lazyproperty
+    def data_labels(self):
+        """|DataLabels| object controlling data labels for this series."""
+        return DataLabels(self._ser.get_or_add_dLbls())
+
+    @lazyproperty
+    def points(self):
+        """
+        The |CategoryPoints| object providing access to individual data
+        points in this series.
+        """
+        return CategoryPoints(self._ser)
+
+    @property
+    def values(self):
+        """
+        Read-only. A sequence containing the float values for this series, in
+        the order they appear on the chart.
+        """
+
+        def iter_values():
+            val = self._element.val
+            if val is None:
+                return
+            for idx in range(val.ptCount_val):
+                yield val.pt_v(idx)
+
+        return tuple(iter_values())
+
+
+class _MarkerMixin(object):
+    """
+    Mixin class providing `.marker` property for line-type chart series. The
+    line-type charts are Line, XY, and Radar.
+    """
+
+    @lazyproperty
+    def marker(self):
+        """
+        The |Marker| instance for this series, providing access to data point
+        marker properties such as fill and line. Setting these properties
+        determines the appearance of markers for all points in this series
+        that are not overridden by settings at the point level.
+        """
+        return Marker(self._ser)
+
+
+class AreaSeries(_BaseCategorySeries):
+    """
+    A data point series belonging to an area plot.
+    """
+
+
+class BarSeries(_BaseCategorySeries):
+    """A data point series belonging to a bar plot."""
+
+    @property
+    def invert_if_negative(self):
+        """
+        |True| if a point having a value less than zero should appear with a
+        fill different than those with a positive value. |False| if the fill
+        should be the same regardless of the bar's value. When |True|, a bar
+        with a solid fill appears with white fill; in a bar with gradient
+        fill, the direction of the gradient is reversed, e.g. dark -> light
+        instead of light -> dark. The term "invert" here should be understood
+        to mean "invert the *direction* of the *fill gradient*".
+        """
+        invertIfNegative = self._element.invertIfNegative
+        if invertIfNegative is None:
+            return True
+        return invertIfNegative.val
+
+    @invert_if_negative.setter
+    def invert_if_negative(self, value):
+        invertIfNegative = self._element.get_or_add_invertIfNegative()
+        invertIfNegative.val = value
+
+
+class LineSeries(_BaseCategorySeries, _MarkerMixin):
+    """
+    A data point series belonging to a line plot.
+    """
+
+    @property
+    def smooth(self):
+        """
+        Read/write boolean specifying whether to use curve smoothing to
+        form the line connecting the data points in this series into
+        a continuous curve. If |False|, a series of straight line segments
+        are used to connect the points.
+        """
+        smooth = self._element.smooth
+        if smooth is None:
+            return True
+        return smooth.val
+
+    @smooth.setter
+    def smooth(self, value):
+        self._element.get_or_add_smooth().val = value
+
+
+class PieSeries(_BaseCategorySeries):
+    """
+    A data point series belonging to a pie plot.
+    """
+
+
+class RadarSeries(_BaseCategorySeries, _MarkerMixin):
+    """
+    A data point series belonging to a radar plot.
+    """
+
+
+class XySeries(_BaseSeries, _MarkerMixin):
+    """
+    A data point series belonging to an XY (scatter) plot.
+    """
+
+    def iter_values(self):
+        """
+        Generate each float Y value in this series, in the order they appear
+        on the chart. A value of `None` represents a missing Y value
+        (corresponding to a blank Excel cell).
+        """
+        yVal = self._element.yVal
+        if yVal is None:
+            return
+
+        for idx in range(yVal.ptCount_val):
+            yield yVal.pt_v(idx)
+
+    @lazyproperty
+    def points(self):
+        """
+        The |XyPoints| object providing access to individual data points in
+        this series.
+        """
+        return XyPoints(self._ser)
+
+    @property
+    def values(self):
+        """
+        Read-only. A sequence containing the float values for this series, in
+        the order they appear on the chart.
+        """
+        return tuple(self.iter_values())
+
+
+class BubbleSeries(XySeries):
+    """
+    A data point series belonging to a bubble plot.
+    """
+
+    @lazyproperty
+    def points(self):
+        """
+        The |BubblePoints| object providing access to individual data point
+        objects used to discover and adjust the formatting and data labels of
+        a data point.
+        """
+        return BubblePoints(self._ser)
+
+
+class SeriesCollection(Sequence):
+    """
+    A sequence of |Series| objects.
+    """
+
+    def __init__(self, parent_elm):
+        # *parent_elm* can be either a c:plotArea or xChart element
+        super(SeriesCollection, self).__init__()
+        self._element = parent_elm
+
+    def __getitem__(self, index):
+        ser = self._element.sers[index]
+        return _SeriesFactory(ser)
+
+    def __len__(self):
+        return len(self._element.sers)
+
+
+def _SeriesFactory(ser):
+    """
+    Return an instance of the appropriate subclass of _BaseSeries based on the
+    xChart element *ser* appears in.
+    """
+    xChart_tag = ser.getparent().tag
+
+    try:
+        SeriesCls = {
+            qn("c:areaChart"): AreaSeries,
+            qn("c:barChart"): BarSeries,
+            qn("c:bubbleChart"): BubbleSeries,
+            qn("c:doughnutChart"): PieSeries,
+            qn("c:lineChart"): LineSeries,
+            qn("c:pieChart"): PieSeries,
+            qn("c:radarChart"): RadarSeries,
+            qn("c:scatterChart"): XySeries,
+        }[xChart_tag]
+    except KeyError:
+        raise NotImplementedError("series class for %s not yet implemented" % xChart_tag)
+
+    return SeriesCls(ser)
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py b/.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py
new file mode 100644
index 00000000..30b21272
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py
@@ -0,0 +1,272 @@
+"""Chart builder and related objects."""
+
+from __future__ import annotations
+
+import io
+from contextlib import contextmanager
+
+from xlsxwriter import Workbook
+
+
+class _BaseWorkbookWriter(object):
+    """Base class for workbook writers, providing shared members."""
+
+    def __init__(self, chart_data):
+        super(_BaseWorkbookWriter, self).__init__()
+        self._chart_data = chart_data
+
+    @property
+    def xlsx_blob(self):
+        """bytes for Excel file containing chart_data."""
+        xlsx_file = io.BytesIO()
+        with self._open_worksheet(xlsx_file) as (workbook, worksheet):
+            self._populate_worksheet(workbook, worksheet)
+        return xlsx_file.getvalue()
+
+    @contextmanager
+    def _open_worksheet(self, xlsx_file):
+        """
+        Enable XlsxWriter Worksheet object to be opened, operated on, and
+        then automatically closed within a `with` statement. A filename or
+        stream object (such as an `io.BytesIO` instance) is expected as
+        *xlsx_file*.
+        """
+        workbook = Workbook(xlsx_file, {"in_memory": True})
+        worksheet = workbook.add_worksheet()
+        yield workbook, worksheet
+        workbook.close()
+
+    def _populate_worksheet(self, workbook, worksheet):
+        """
+        Must be overridden by each subclass to provide the particulars of
+        writing the spreadsheet data.
+        """
+        raise NotImplementedError("must be provided by each subclass")
+
+
+class CategoryWorkbookWriter(_BaseWorkbookWriter):
+    """
+    Determines Excel worksheet layout and can write an Excel workbook from
+    a CategoryChartData object. Serves as the authority for Excel worksheet
+    ranges.
+    """
+
+    @property
+    def categories_ref(self):
+        """
+        The Excel worksheet reference to the categories for this chart (not
+        including the column heading).
+        """
+        categories = self._chart_data.categories
+        if categories.depth == 0:
+            raise ValueError("chart data contains no categories")
+        right_col = chr(ord("A") + categories.depth - 1)
+        bottom_row = categories.leaf_count + 1
+        return "Sheet1!$A$2:$%s$%d" % (right_col, bottom_row)
+
+    def series_name_ref(self, series):
+        """
+        Return the Excel worksheet reference to the cell containing the name
+        for *series*. This also serves as the column heading for the series
+        values.
+        """
+        return "Sheet1!$%s$1" % self._series_col_letter(series)
+
+    def values_ref(self, series):
+        """
+        The Excel worksheet reference to the values for this series (not
+        including the column heading).
+        """
+        return "Sheet1!${col_letter}$2:${col_letter}${bottom_row}".format(
+            **{
+                "col_letter": self._series_col_letter(series),
+                "bottom_row": len(series) + 1,
+            }
+        )
+
+    @staticmethod
+    def _column_reference(column_number):
+        """Return str Excel column reference like 'BQ' for *column_number*.
+
+        *column_number* is an int in the range 1-16384 inclusive, where
+        1 maps to column 'A'.
+        """
+        if column_number < 1 or column_number > 16384:
+            raise ValueError("column_number must be in range 1-16384")
+
+        # ---Work right-to-left, one order of magnitude at a time. Note there
+        #    is no zero representation in Excel address scheme, so this is
+        #    not just a conversion to base-26---
+
+        col_ref = ""
+        while column_number:
+            remainder = column_number % 26
+            if remainder == 0:
+                remainder = 26
+
+            col_letter = chr(ord("A") + remainder - 1)
+            col_ref = col_letter + col_ref
+
+            # ---Advance to next order of magnitude or terminate loop. The
+            # minus-one in this expression reflects the fact the next lower
+            # order of magnitude has a minumum value of 1 (not zero). This is
+            # essentially the complement to the "if it's 0 make it 26' step
+            # above.---
+            column_number = (column_number - 1) // 26
+
+        return col_ref
+
+    def _populate_worksheet(self, workbook, worksheet):
+        """
+        Write the chart data contents to *worksheet* in category chart
+        layout. Write categories starting in the first column starting in
+        the second row, and proceeding one column per category level (for
+        charts having multi-level categories). Write series as columns
+        starting in the next following column, placing the series title in
+        the first cell.
+        """
+        self._write_categories(workbook, worksheet)
+        self._write_series(workbook, worksheet)
+
+    def _series_col_letter(self, series):
+        """
+        The letter of the Excel worksheet column in which the data for a
+        series appears.
+        """
+        column_number = 1 + series.categories.depth + series.index
+        return self._column_reference(column_number)
+
+    def _write_categories(self, workbook, worksheet):
+        """
+        Write the categories column(s) to *worksheet*. Categories start in
+        the first column starting in the second row, and proceeding one
+        column per category level (for charts having multi-level categories).
+        A date category is formatted as a date. All others are formatted
+        `General`.
+        """
+        categories = self._chart_data.categories
+        num_format = workbook.add_format({"num_format": categories.number_format})
+        depth = categories.depth
+        for idx, level in enumerate(categories.levels):
+            col = depth - idx - 1
+            self._write_cat_column(worksheet, col, level, num_format)
+
+    def _write_cat_column(self, worksheet, col, level, num_format):
+        """
+        Write a category column defined by *level* to *worksheet* at offset
+        *col* and formatted with *num_format*.
+        """
+        worksheet.set_column(col, col, 10)  # wide enough for a date
+        for off, name in level:
+            row = off + 1
+            worksheet.write(row, col, name, num_format)
+
+    def _write_series(self, workbook, worksheet):
+        """
+        Write the series column(s) to *worksheet*. Series start in the column
+        following the last categories column, placing the series title in the
+        first cell.
+        """
+        col_offset = self._chart_data.categories.depth
+        for idx, series in enumerate(self._chart_data):
+            num_format = workbook.add_format({"num_format": series.number_format})
+            series_col = idx + col_offset
+            worksheet.write(0, series_col, series.name)
+            worksheet.write_column(1, series_col, series.values, num_format)
+
+
+class XyWorkbookWriter(_BaseWorkbookWriter):
+    """
+    Determines Excel worksheet layout and can write an Excel workbook from XY
+    chart data. Serves as the authority for Excel worksheet ranges.
+    """
+
+    def series_name_ref(self, series):
+        """
+        Return the Excel worksheet reference to the cell containing the name
+        for *series*. This also serves as the column heading for the series
+        Y values.
+        """
+        row = self.series_table_row_offset(series) + 1
+        return "Sheet1!$B$%d" % row
+
+    def series_table_row_offset(self, series):
+        """
+        Return the number of rows preceding the data table for *series* in
+        the Excel worksheet.
+        """
+        title_and_spacer_rows = series.index * 2
+        data_point_rows = series.data_point_offset
+        return title_and_spacer_rows + data_point_rows
+
+    def x_values_ref(self, series):
+        """
+        The Excel worksheet reference to the X values for this chart (not
+        including the column label).
+        """
+        top_row = self.series_table_row_offset(series) + 2
+        bottom_row = top_row + len(series) - 1
+        return "Sheet1!$A$%d:$A$%d" % (top_row, bottom_row)
+
+    def y_values_ref(self, series):
+        """
+        The Excel worksheet reference to the Y values for this chart (not
+        including the column label).
+        """
+        top_row = self.series_table_row_offset(series) + 2
+        bottom_row = top_row + len(series) - 1
+        return "Sheet1!$B$%d:$B$%d" % (top_row, bottom_row)
+
+    def _populate_worksheet(self, workbook, worksheet):
+        """
+        Write chart data contents to *worksheet* in the standard XY chart
+        layout. Write the data for each series to a separate two-column
+        table, X values in column A and Y values in column B. Place the
+        series label in the first (heading) cell of the column.
+        """
+        chart_num_format = workbook.add_format({"num_format": self._chart_data.number_format})
+        for series in self._chart_data:
+            series_num_format = workbook.add_format({"num_format": series.number_format})
+            offset = self.series_table_row_offset(series)
+            # write X values
+            worksheet.write_column(offset + 1, 0, series.x_values, chart_num_format)
+            # write Y values
+            worksheet.write(offset, 1, series.name)
+            worksheet.write_column(offset + 1, 1, series.y_values, series_num_format)
+
+
+class BubbleWorkbookWriter(XyWorkbookWriter):
+    """
+    Service object that knows how to write an Excel workbook from bubble
+    chart data.
+    """
+
+    def bubble_sizes_ref(self, series):
+        """
+        The Excel worksheet reference to the range containing the bubble
+        sizes for *series* (not including the column heading cell).
+        """
+        top_row = self.series_table_row_offset(series) + 2
+        bottom_row = top_row + len(series) - 1
+        return "Sheet1!$C$%d:$C$%d" % (top_row, bottom_row)
+
+    def _populate_worksheet(self, workbook, worksheet):
+        """
+        Write chart data contents to *worksheet* in the bubble chart layout.
+        Write the data for each series to a separate three-column table with
+        X values in column A, Y values in column B, and bubble sizes in
+        column C. Place the series label in the first (heading) cell of the
+        values column.
+        """
+        chart_num_format = workbook.add_format({"num_format": self._chart_data.number_format})
+        for series in self._chart_data:
+            series_num_format = workbook.add_format({"num_format": series.number_format})
+            offset = self.series_table_row_offset(series)
+            # write X values
+            worksheet.write_column(offset + 1, 0, series.x_values, chart_num_format)
+            # write Y values
+            worksheet.write(offset, 1, series.name)
+            worksheet.write_column(offset + 1, 1, series.y_values, series_num_format)
+            # write bubble sizes
+            worksheet.write(offset, 2, "Size")
+            worksheet.write_column(offset + 1, 2, series.bubble_sizes, chart_num_format)
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/xmlwriter.py b/.venv/lib/python3.12/site-packages/pptx/chart/xmlwriter.py
new file mode 100644
index 00000000..703c53dd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/xmlwriter.py
@@ -0,0 +1,1840 @@
+"""Composers for default chart XML for various chart types."""
+
+from __future__ import annotations
+
+from copy import deepcopy
+from xml.sax.saxutils import escape
+
+from pptx.enum.chart import XL_CHART_TYPE
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+
+
+def ChartXmlWriter(chart_type, chart_data):
+    """
+    Factory function returning appropriate XML writer object for
+    *chart_type*, loaded with *chart_type* and *chart_data*.
+    """
+    XL_CT = XL_CHART_TYPE
+    try:
+        BuilderCls = {
+            XL_CT.AREA: _AreaChartXmlWriter,
+            XL_CT.AREA_STACKED: _AreaChartXmlWriter,
+            XL_CT.AREA_STACKED_100: _AreaChartXmlWriter,
+            XL_CT.BAR_CLUSTERED: _BarChartXmlWriter,
+            XL_CT.BAR_STACKED: _BarChartXmlWriter,
+            XL_CT.BAR_STACKED_100: _BarChartXmlWriter,
+            XL_CT.BUBBLE: _BubbleChartXmlWriter,
+            XL_CT.BUBBLE_THREE_D_EFFECT: _BubbleChartXmlWriter,
+            XL_CT.COLUMN_CLUSTERED: _BarChartXmlWriter,
+            XL_CT.COLUMN_STACKED: _BarChartXmlWriter,
+            XL_CT.COLUMN_STACKED_100: _BarChartXmlWriter,
+            XL_CT.DOUGHNUT: _DoughnutChartXmlWriter,
+            XL_CT.DOUGHNUT_EXPLODED: _DoughnutChartXmlWriter,
+            XL_CT.LINE: _LineChartXmlWriter,
+            XL_CT.LINE_MARKERS: _LineChartXmlWriter,
+            XL_CT.LINE_MARKERS_STACKED: _LineChartXmlWriter,
+            XL_CT.LINE_MARKERS_STACKED_100: _LineChartXmlWriter,
+            XL_CT.LINE_STACKED: _LineChartXmlWriter,
+            XL_CT.LINE_STACKED_100: _LineChartXmlWriter,
+            XL_CT.PIE: _PieChartXmlWriter,
+            XL_CT.PIE_EXPLODED: _PieChartXmlWriter,
+            XL_CT.RADAR: _RadarChartXmlWriter,
+            XL_CT.RADAR_FILLED: _RadarChartXmlWriter,
+            XL_CT.RADAR_MARKERS: _RadarChartXmlWriter,
+            XL_CT.XY_SCATTER: _XyChartXmlWriter,
+            XL_CT.XY_SCATTER_LINES: _XyChartXmlWriter,
+            XL_CT.XY_SCATTER_LINES_NO_MARKERS: _XyChartXmlWriter,
+            XL_CT.XY_SCATTER_SMOOTH: _XyChartXmlWriter,
+            XL_CT.XY_SCATTER_SMOOTH_NO_MARKERS: _XyChartXmlWriter,
+        }[chart_type]
+    except KeyError:
+        raise NotImplementedError("XML writer for chart type %s not yet implemented" % chart_type)
+    return BuilderCls(chart_type, chart_data)
+
+
+def SeriesXmlRewriterFactory(chart_type, chart_data):
+    """
+    Return a |_BaseSeriesXmlRewriter| subclass appropriate to *chart_type*.
+    """
+    XL_CT = XL_CHART_TYPE
+
+    RewriterCls = {
+        # There are 73 distinct chart types, only specify non-category
+        # types, others default to _CategorySeriesXmlRewriter. Stock-type
+        # charts are multi-plot charts, so no guaratees on how they turn
+        # out.
+        XL_CT.BUBBLE: _BubbleSeriesXmlRewriter,
+        XL_CT.BUBBLE_THREE_D_EFFECT: _BubbleSeriesXmlRewriter,
+        XL_CT.XY_SCATTER: _XySeriesXmlRewriter,
+        XL_CT.XY_SCATTER_LINES: _XySeriesXmlRewriter,
+        XL_CT.XY_SCATTER_LINES_NO_MARKERS: _XySeriesXmlRewriter,
+        XL_CT.XY_SCATTER_SMOOTH: _XySeriesXmlRewriter,
+        XL_CT.XY_SCATTER_SMOOTH_NO_MARKERS: _XySeriesXmlRewriter,
+    }.get(chart_type, _CategorySeriesXmlRewriter)
+
+    return RewriterCls(chart_data)
+
+
+class _BaseChartXmlWriter(object):
+    """
+    Generates XML text (unicode) for a default chart, like the one added by
+    PowerPoint when you click the *Add Column Chart* button on the ribbon.
+    Differentiated XML for different chart types is provided by subclasses.
+    """
+
+    def __init__(self, chart_type, series_seq):
+        super(_BaseChartXmlWriter, self).__init__()
+        self._chart_type = chart_type
+        self._chart_data = series_seq
+        self._series_seq = list(series_seq)
+
+    @property
+    def xml(self):
+        """
+        The full XML stream for the chart specified by this chart builder, as
+        unicode text. This method must be overridden by each subclass.
+        """
+        raise NotImplementedError("must be implemented by all subclasses")
+
+
+class _BaseSeriesXmlWriter(object):
+    """
+    Provides shared members for series XML writers.
+    """
+
+    def __init__(self, series, date_1904=False):
+        super(_BaseSeriesXmlWriter, self).__init__()
+        self._series = series
+        self._date_1904 = date_1904
+
+    @property
+    def name(self):
+        """
+        The XML-escaped name for this series.
+        """
+        return escape(self._series.name)
+
+    def numRef_xml(self, wksht_ref, number_format, values):
+        """
+        Return the ``<c:numRef>`` element specified by the parameters as
+        unicode text.
+        """
+        pt_xml = self.pt_xml(values)
+        return (
+            "            <c:numRef>\n"
+            "              <c:f>{wksht_ref}</c:f>\n"
+            "              <c:numCache>\n"
+            "                <c:formatCode>{number_format}</c:formatCode>\n"
+            "{pt_xml}"
+            "              </c:numCache>\n"
+            "            </c:numRef>\n"
+        ).format(**{"wksht_ref": wksht_ref, "number_format": number_format, "pt_xml": pt_xml})
+
+    def pt_xml(self, values):
+        """
+        Return the ``<c:ptCount>`` and sequence of ``<c:pt>`` elements
+        corresponding to *values* as a single unicode text string.
+        `c:ptCount` refers to the number of `c:pt` elements in this sequence.
+        The `idx` attribute value for `c:pt` elements locates the data point
+        in the overall data point sequence of the chart and is started at
+        *offset*.
+        """
+        xml = ('                <c:ptCount val="{pt_count}"/>\n').format(pt_count=len(values))
+
+        pt_tmpl = (
+            '                <c:pt idx="{idx}">\n'
+            "                  <c:v>{value}</c:v>\n"
+            "                </c:pt>\n"
+        )
+        for idx, value in enumerate(values):
+            if value is None:
+                continue
+            xml += pt_tmpl.format(idx=idx, value=value)
+
+        return xml
+
+    @property
+    def tx(self):
+        """
+        Return a ``<c:tx>`` oxml element for this series, containing the
+        series name.
+        """
+        xml = self._tx_tmpl.format(
+            **{
+                "wksht_ref": self._series.name_ref,
+                "series_name": self.name,
+                "nsdecls": " %s" % nsdecls("c"),
+            }
+        )
+        return parse_xml(xml)
+
+    @property
+    def tx_xml(self):
+        """
+        Return the ``<c:tx>`` (tx is short for 'text') element for this
+        series as unicode text. This element contains the series name.
+        """
+        return self._tx_tmpl.format(
+            **{
+                "wksht_ref": self._series.name_ref,
+                "series_name": self.name,
+                "nsdecls": "",
+            }
+        )
+
+    @property
+    def _tx_tmpl(self):
+        """
+        The string formatting template for the ``<c:tx>`` element for this
+        series, containing the series title and spreadsheet range reference.
+        """
+        return (
+            "          <c:tx{nsdecls}>\n"
+            "            <c:strRef>\n"
+            "              <c:f>{wksht_ref}</c:f>\n"
+            "              <c:strCache>\n"
+            '                <c:ptCount val="1"/>\n'
+            '                <c:pt idx="0">\n'
+            "                  <c:v>{series_name}</c:v>\n"
+            "                </c:pt>\n"
+            "              </c:strCache>\n"
+            "            </c:strRef>\n"
+            "          </c:tx>\n"
+        )
+
+
+class _BaseSeriesXmlRewriter(object):
+    """
+    Base class for series XML rewriters.
+    """
+
+    def __init__(self, chart_data):
+        super(_BaseSeriesXmlRewriter, self).__init__()
+        self._chart_data = chart_data
+
+    def replace_series_data(self, chartSpace):
+        """
+        Rewrite the series data under *chartSpace* using the chart data
+        contents. All series-level formatting is left undisturbed. If
+        the chart data contains fewer series than *chartSpace*, the extra
+        series in *chartSpace* are deleted. If *chart_data* contains more
+        series than the *chartSpace* element, new series are added to the
+        last plot in the chart and series formatting is "cloned" from the
+        last series in that plot.
+        """
+        plotArea, date_1904 = chartSpace.plotArea, chartSpace.date_1904
+        chart_data = self._chart_data
+        self._adjust_ser_count(plotArea, len(chart_data))
+        for ser, series_data in zip(plotArea.sers, chart_data):
+            self._rewrite_ser_data(ser, series_data, date_1904)
+
+    def _add_cloned_sers(self, plotArea, count):
+        """
+        Add `c:ser` elements to the last xChart element in *plotArea*, cloned
+        from the last `c:ser` child of that last xChart.
+        """
+
+        def clone_ser(ser):
+            new_ser = deepcopy(ser)
+            new_ser.idx.val = plotArea.next_idx
+            new_ser.order.val = plotArea.next_order
+            ser.addnext(new_ser)
+            return new_ser
+
+        last_ser = plotArea.last_ser
+        for _ in range(count):
+            last_ser = clone_ser(last_ser)
+
+    def _adjust_ser_count(self, plotArea, new_ser_count):
+        """
+        Adjust the number of c:ser elements in *plotArea* to *new_ser_count*.
+        Excess c:ser elements are deleted from the end, along with any xChart
+        elements that are left empty as a result. Series elements are
+        considered in xChart + series order. Any new c:ser elements required
+        are added to the last xChart element and cloned from the last c:ser
+        element in that xChart.
+        """
+        ser_count_diff = new_ser_count - len(plotArea.sers)
+        if ser_count_diff > 0:
+            self._add_cloned_sers(plotArea, ser_count_diff)
+        elif ser_count_diff < 0:
+            self._trim_ser_count_by(plotArea, abs(ser_count_diff))
+
+    def _rewrite_ser_data(self, ser, series_data, date_1904):
+        """
+        Rewrite selected child elements of *ser* based on the values in
+        *series_data*.
+        """
+        raise NotImplementedError("must be implemented by each subclass")
+
+    def _trim_ser_count_by(self, plotArea, count):
+        """
+        Remove the last *count* ser elements from *plotArea*. Any xChart
+        elements having no ser child elements after trimming are also
+        removed.
+        """
+        extra_sers = plotArea.sers[-count:]
+        for ser in extra_sers:
+            parent = ser.getparent()
+            parent.remove(ser)
+        extra_xCharts = [xChart for xChart in plotArea.iter_xCharts() if len(xChart.sers) == 0]
+        for xChart in extra_xCharts:
+            parent = xChart.getparent()
+            parent.remove(xChart)
+
+
+class _AreaChartXmlWriter(_BaseChartXmlWriter):
+    """
+    Provides specialized methods particular to the ``<c:areaChart>`` element.
+    """
+
+    @property
+    def xml(self):
+        return (
+            "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+            '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+            'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+            'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+            'iceDocument/2006/relationships">\n'
+            '  <c:date1904 val="0"/>\n'
+            '  <c:roundedCorners val="0"/>\n'
+            "  <c:chart>\n"
+            '    <c:autoTitleDeleted val="0"/>\n'
+            "    <c:plotArea>\n"
+            "      <c:layout/>\n"
+            "      <c:areaChart>\n"
+            "{grouping_xml}"
+            '        <c:varyColors val="0"/>\n'
+            "{ser_xml}"
+            "        <c:dLbls>\n"
+            '          <c:showLegendKey val="0"/>\n'
+            '          <c:showVal val="0"/>\n'
+            '          <c:showCatName val="0"/>\n'
+            '          <c:showSerName val="0"/>\n'
+            '          <c:showPercent val="0"/>\n'
+            '          <c:showBubbleSize val="0"/>\n'
+            "        </c:dLbls>\n"
+            '        <c:axId val="-2101159928"/>\n'
+            '        <c:axId val="-2100718248"/>\n'
+            "      </c:areaChart>\n"
+            "{cat_ax_xml}"
+            "      <c:valAx>\n"
+            '        <c:axId val="-2100718248"/>\n'
+            "        <c:scaling>\n"
+            '          <c:orientation val="minMax"/>\n'
+            "        </c:scaling>\n"
+            '        <c:delete val="0"/>\n'
+            '        <c:axPos val="l"/>\n'
+            "        <c:majorGridlines/>\n"
+            '        <c:numFmt formatCode="General" sourceLinked="1"/>\n'
+            '        <c:majorTickMark val="out"/>\n'
+            '        <c:minorTickMark val="none"/>\n'
+            '        <c:tickLblPos val="nextTo"/>\n'
+            '        <c:crossAx val="-2101159928"/>\n'
+            '        <c:crosses val="autoZero"/>\n'
+            '        <c:crossBetween val="midCat"/>\n'
+            "      </c:valAx>\n"
+            "    </c:plotArea>\n"
+            "    <c:legend>\n"
+            '      <c:legendPos val="r"/>\n'
+            "      <c:layout/>\n"
+            '      <c:overlay val="0"/>\n'
+            "    </c:legend>\n"
+            '    <c:plotVisOnly val="1"/>\n'
+            '    <c:dispBlanksAs val="zero"/>\n'
+            '    <c:showDLblsOverMax val="0"/>\n'
+            "  </c:chart>\n"
+            "  <c:txPr>\n"
+            "    <a:bodyPr/>\n"
+            "    <a:lstStyle/>\n"
+            "    <a:p>\n"
+            "      <a:pPr>\n"
+            '        <a:defRPr sz="1800"/>\n'
+            "      </a:pPr>\n"
+            "      <a:endParaRPr/>\n"
+            "    </a:p>\n"
+            "  </c:txPr>\n"
+            "</c:chartSpace>\n"
+        ).format(
+            **{
+                "grouping_xml": self._grouping_xml,
+                "ser_xml": self._ser_xml,
+                "cat_ax_xml": self._cat_ax_xml,
+            }
+        )
+
+    @property
+    def _cat_ax_xml(self):
+        categories = self._chart_data.categories
+
+        if categories.are_dates:
+            return (
+                "      <c:dateAx>\n"
+                '        <c:axId val="-2101159928"/>\n'
+                "        <c:scaling>\n"
+                '          <c:orientation val="minMax"/>\n'
+                "        </c:scaling>\n"
+                '        <c:delete val="0"/>\n'
+                '        <c:axPos val="b"/>\n'
+                '        <c:numFmt formatCode="{nf}" sourceLinked="1"/>\n'
+                '        <c:majorTickMark val="out"/>\n'
+                '        <c:minorTickMark val="none"/>\n'
+                '        <c:tickLblPos val="nextTo"/>\n'
+                '        <c:crossAx val="-2100718248"/>\n'
+                '        <c:crosses val="autoZero"/>\n'
+                '        <c:auto val="1"/>\n'
+                '        <c:lblOffset val="100"/>\n'
+                '        <c:baseTimeUnit val="days"/>\n'
+                "      </c:dateAx>\n"
+            ).format(**{"nf": categories.number_format})
+
+        return (
+            "      <c:catAx>\n"
+            '        <c:axId val="-2101159928"/>\n'
+            "        <c:scaling>\n"
+            '          <c:orientation val="minMax"/>\n'
+            "        </c:scaling>\n"
+            '        <c:delete val="0"/>\n'
+            '        <c:axPos val="b"/>\n'
+            '        <c:numFmt formatCode="General" sourceLinked="1"/>\n'
+            '        <c:majorTickMark val="out"/>\n'
+            '        <c:minorTickMark val="none"/>\n'
+            '        <c:tickLblPos val="nextTo"/>\n'
+            '        <c:crossAx val="-2100718248"/>\n'
+            '        <c:crosses val="autoZero"/>\n'
+            '        <c:auto val="1"/>\n'
+            '        <c:lblAlgn val="ctr"/>\n'
+            '        <c:lblOffset val="100"/>\n'
+            '        <c:noMultiLvlLbl val="0"/>\n'
+            "      </c:catAx>\n"
+        )
+
+    @property
+    def _grouping_xml(self):
+        val = {
+            XL_CHART_TYPE.AREA: "standard",
+            XL_CHART_TYPE.AREA_STACKED: "stacked",
+            XL_CHART_TYPE.AREA_STACKED_100: "percentStacked",
+        }[self._chart_type]
+        return '        <c:grouping val="%s"/>\n' % val
+
+    @property
+    def _ser_xml(self):
+        xml = ""
+        for series in self._chart_data:
+            xml_writer = _CategorySeriesXmlWriter(series)
+            xml += (
+                "        <c:ser>\n"
+                '          <c:idx val="{ser_idx}"/>\n'
+                '          <c:order val="{ser_order}"/>\n'
+                "{tx_xml}"
+                "{cat_xml}"
+                "{val_xml}"
+                "        </c:ser>\n"
+            ).format(
+                **{
+                    "ser_idx": series.index,
+                    "ser_order": series.index,
+                    "tx_xml": xml_writer.tx_xml,
+                    "cat_xml": xml_writer.cat_xml,
+                    "val_xml": xml_writer.val_xml,
+                }
+            )
+        return xml
+
+
+class _BarChartXmlWriter(_BaseChartXmlWriter):
+    """
+    Provides specialized methods particular to the ``<c:barChart>`` element.
+    """
+
+    @property
+    def xml(self):
+        return (
+            "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+            '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+            'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+            'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+            'iceDocument/2006/relationships">\n'
+            '  <c:date1904 val="0"/>\n'
+            "  <c:chart>\n"
+            '    <c:autoTitleDeleted val="0"/>\n'
+            "    <c:plotArea>\n"
+            "      <c:barChart>\n"
+            "{barDir_xml}"
+            "{grouping_xml}"
+            "{ser_xml}"
+            "{overlap_xml}"
+            '        <c:axId val="-2068027336"/>\n'
+            '        <c:axId val="-2113994440"/>\n'
+            "      </c:barChart>\n"
+            "{cat_ax_xml}"
+            "      <c:valAx>\n"
+            '        <c:axId val="-2113994440"/>\n'
+            "        <c:scaling/>\n"
+            '        <c:delete val="0"/>\n'
+            '        <c:axPos val="{val_ax_pos}"/>\n'
+            "        <c:majorGridlines/>\n"
+            '        <c:majorTickMark val="out"/>\n'
+            '        <c:minorTickMark val="none"/>\n'
+            '        <c:tickLblPos val="nextTo"/>\n'
+            '        <c:crossAx val="-2068027336"/>\n'
+            '        <c:crosses val="autoZero"/>\n'
+            "      </c:valAx>\n"
+            "    </c:plotArea>\n"
+            '    <c:dispBlanksAs val="gap"/>\n'
+            "  </c:chart>\n"
+            "  <c:txPr>\n"
+            "    <a:bodyPr/>\n"
+            "    <a:lstStyle/>\n"
+            "    <a:p>\n"
+            "      <a:pPr>\n"
+            '        <a:defRPr sz="1800"/>\n'
+            "      </a:pPr>\n"
+            '      <a:endParaRPr lang="en-US"/>\n'
+            "    </a:p>\n"
+            "  </c:txPr>\n"
+            "</c:chartSpace>\n"
+        ).format(
+            **{
+                "barDir_xml": self._barDir_xml,
+                "grouping_xml": self._grouping_xml,
+                "ser_xml": self._ser_xml,
+                "overlap_xml": self._overlap_xml,
+                "cat_ax_xml": self._cat_ax_xml,
+                "val_ax_pos": self._val_ax_pos,
+            }
+        )
+
+    @property
+    def _barDir_xml(self):
+        XL = XL_CHART_TYPE
+        bar_types = (XL.BAR_CLUSTERED, XL.BAR_STACKED, XL.BAR_STACKED_100)
+        col_types = (XL.COLUMN_CLUSTERED, XL.COLUMN_STACKED, XL.COLUMN_STACKED_100)
+        if self._chart_type in bar_types:
+            return '        <c:barDir val="bar"/>\n'
+        elif self._chart_type in col_types:
+            return '        <c:barDir val="col"/>\n'
+        raise NotImplementedError("no _barDir_xml() for chart type %s" % self._chart_type)
+
+    @property
+    def _cat_ax_pos(self):
+        return {
+            XL_CHART_TYPE.BAR_CLUSTERED: "l",
+            XL_CHART_TYPE.BAR_STACKED: "l",
+            XL_CHART_TYPE.BAR_STACKED_100: "l",
+            XL_CHART_TYPE.COLUMN_CLUSTERED: "b",
+            XL_CHART_TYPE.COLUMN_STACKED: "b",
+            XL_CHART_TYPE.COLUMN_STACKED_100: "b",
+        }[self._chart_type]
+
+    @property
+    def _cat_ax_xml(self):
+        categories = self._chart_data.categories
+
+        if categories.are_dates:
+            return (
+                "      <c:dateAx>\n"
+                '        <c:axId val="-2068027336"/>\n'
+                "        <c:scaling>\n"
+                '          <c:orientation val="minMax"/>\n'
+                "        </c:scaling>\n"
+                '        <c:delete val="0"/>\n'
+                '        <c:axPos val="{cat_ax_pos}"/>\n'
+                '        <c:numFmt formatCode="{nf}" sourceLinked="1"/>\n'
+                '        <c:majorTickMark val="out"/>\n'
+                '        <c:minorTickMark val="none"/>\n'
+                '        <c:tickLblPos val="nextTo"/>\n'
+                '        <c:crossAx val="-2113994440"/>\n'
+                '        <c:crosses val="autoZero"/>\n'
+                '        <c:auto val="1"/>\n'
+                '        <c:lblOffset val="100"/>\n'
+                '        <c:baseTimeUnit val="days"/>\n'
+                "      </c:dateAx>\n"
+            ).format(**{"cat_ax_pos": self._cat_ax_pos, "nf": categories.number_format})
+
+        return (
+            "      <c:catAx>\n"
+            '        <c:axId val="-2068027336"/>\n'
+            "        <c:scaling>\n"
+            '          <c:orientation val="minMax"/>\n'
+            "        </c:scaling>\n"
+            '        <c:delete val="0"/>\n'
+            '        <c:axPos val="{cat_ax_pos}"/>\n'
+            '        <c:majorTickMark val="out"/>\n'
+            '        <c:minorTickMark val="none"/>\n'
+            '        <c:tickLblPos val="nextTo"/>\n'
+            '        <c:crossAx val="-2113994440"/>\n'
+            '        <c:crosses val="autoZero"/>\n'
+            '        <c:auto val="1"/>\n'
+            '        <c:lblAlgn val="ctr"/>\n'
+            '        <c:lblOffset val="100"/>\n'
+            '        <c:noMultiLvlLbl val="0"/>\n'
+            "      </c:catAx>\n"
+        ).format(**{"cat_ax_pos": self._cat_ax_pos})
+
+    @property
+    def _grouping_xml(self):
+        XL = XL_CHART_TYPE
+        clustered_types = (XL.BAR_CLUSTERED, XL.COLUMN_CLUSTERED)
+        stacked_types = (XL.BAR_STACKED, XL.COLUMN_STACKED)
+        percentStacked_types = (XL.BAR_STACKED_100, XL.COLUMN_STACKED_100)
+        if self._chart_type in clustered_types:
+            return '        <c:grouping val="clustered"/>\n'
+        elif self._chart_type in stacked_types:
+            return '        <c:grouping val="stacked"/>\n'
+        elif self._chart_type in percentStacked_types:
+            return '        <c:grouping val="percentStacked"/>\n'
+        raise NotImplementedError("no _grouping_xml() for chart type %s" % self._chart_type)
+
+    @property
+    def _overlap_xml(self):
+        XL = XL_CHART_TYPE
+        percentStacked_types = (
+            XL.BAR_STACKED,
+            XL.BAR_STACKED_100,
+            XL.COLUMN_STACKED,
+            XL.COLUMN_STACKED_100,
+        )
+        if self._chart_type in percentStacked_types:
+            return '        <c:overlap val="100"/>\n'
+        return ""
+
+    @property
+    def _ser_xml(self):
+        xml = ""
+        for series in self._chart_data:
+            xml_writer = _CategorySeriesXmlWriter(series)
+            xml += (
+                "        <c:ser>\n"
+                '          <c:idx val="{ser_idx}"/>\n'
+                '          <c:order val="{ser_order}"/>\n'
+                "{tx_xml}"
+                "{cat_xml}"
+                "{val_xml}"
+                "        </c:ser>\n"
+            ).format(
+                **{
+                    "ser_idx": series.index,
+                    "ser_order": series.index,
+                    "tx_xml": xml_writer.tx_xml,
+                    "cat_xml": xml_writer.cat_xml,
+                    "val_xml": xml_writer.val_xml,
+                }
+            )
+        return xml
+
+    @property
+    def _val_ax_pos(self):
+        return {
+            XL_CHART_TYPE.BAR_CLUSTERED: "b",
+            XL_CHART_TYPE.BAR_STACKED: "b",
+            XL_CHART_TYPE.BAR_STACKED_100: "b",
+            XL_CHART_TYPE.COLUMN_CLUSTERED: "l",
+            XL_CHART_TYPE.COLUMN_STACKED: "l",
+            XL_CHART_TYPE.COLUMN_STACKED_100: "l",
+        }[self._chart_type]
+
+
+class _DoughnutChartXmlWriter(_BaseChartXmlWriter):
+    """
+    Provides specialized methods particular to the ``<c:doughnutChart>``
+    element.
+    """
+
+    @property
+    def xml(self):
+        return (
+            "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+            '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+            'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+            'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+            'iceDocument/2006/relationships">\n'
+            '  <c:date1904 val="0"/>\n'
+            '  <c:roundedCorners val="0"/>\n'
+            "  <c:chart>\n"
+            '    <c:autoTitleDeleted val="0"/>\n'
+            "    <c:plotArea>\n"
+            "      <c:layout/>\n"
+            "      <c:doughnutChart>\n"
+            '        <c:varyColors val="1"/>\n'
+            "{ser_xml}"
+            "        <c:dLbls>\n"
+            '          <c:showLegendKey val="0"/>\n'
+            '          <c:showVal val="0"/>\n'
+            '          <c:showCatName val="0"/>\n'
+            '          <c:showSerName val="0"/>\n'
+            '          <c:showPercent val="0"/>\n'
+            '          <c:showBubbleSize val="0"/>\n'
+            '          <c:showLeaderLines val="1"/>\n'
+            "        </c:dLbls>\n"
+            '        <c:firstSliceAng val="0"/>\n'
+            '        <c:holeSize val="50"/>\n'
+            "      </c:doughnutChart>\n"
+            "    </c:plotArea>\n"
+            "    <c:legend>\n"
+            '      <c:legendPos val="r"/>\n'
+            "      <c:layout/>\n"
+            '      <c:overlay val="0"/>\n'
+            "    </c:legend>\n"
+            '    <c:plotVisOnly val="1"/>\n'
+            '    <c:dispBlanksAs val="gap"/>\n'
+            '    <c:showDLblsOverMax val="0"/>\n'
+            "  </c:chart>\n"
+            "  <c:txPr>\n"
+            "    <a:bodyPr/>\n"
+            "    <a:lstStyle/>\n"
+            "    <a:p>\n"
+            "      <a:pPr>\n"
+            '        <a:defRPr sz="1800"/>\n'
+            "      </a:pPr>\n"
+            "      <a:endParaRPr/>\n"
+            "    </a:p>\n"
+            "  </c:txPr>\n"
+            "</c:chartSpace>\n"
+        ).format(**{"ser_xml": self._ser_xml})
+
+    @property
+    def _explosion_xml(self):
+        if self._chart_type == XL_CHART_TYPE.DOUGHNUT_EXPLODED:
+            return '          <c:explosion val="25"/>\n'
+        return ""
+
+    @property
+    def _ser_xml(self):
+        xml = ""
+        for series in self._chart_data:
+            xml_writer = _CategorySeriesXmlWriter(series)
+            xml += (
+                "        <c:ser>\n"
+                '          <c:idx val="{ser_idx}"/>\n'
+                '          <c:order val="{ser_order}"/>\n'
+                "{tx_xml}"
+                "{explosion_xml}"
+                "{cat_xml}"
+                "{val_xml}"
+                "        </c:ser>\n"
+            ).format(
+                **{
+                    "ser_idx": series.index,
+                    "ser_order": series.index,
+                    "tx_xml": xml_writer.tx_xml,
+                    "explosion_xml": self._explosion_xml,
+                    "cat_xml": xml_writer.cat_xml,
+                    "val_xml": xml_writer.val_xml,
+                }
+            )
+        return xml
+
+
+class _LineChartXmlWriter(_BaseChartXmlWriter):
+    """
+    Provides specialized methods particular to the ``<c:lineChart>`` element.
+    """
+
+    @property
+    def xml(self):
+        return (
+            "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+            '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+            'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+            'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+            'iceDocument/2006/relationships">\n'
+            '  <c:date1904 val="0"/>\n'
+            "  <c:chart>\n"
+            '    <c:autoTitleDeleted val="0"/>\n'
+            "    <c:plotArea>\n"
+            "      <c:lineChart>\n"
+            "{grouping_xml}"
+            '        <c:varyColors val="0"/>\n'
+            "{ser_xml}"
+            '        <c:marker val="1"/>\n'
+            '        <c:smooth val="0"/>\n'
+            '        <c:axId val="2118791784"/>\n'
+            '        <c:axId val="2140495176"/>\n'
+            "      </c:lineChart>\n"
+            "{cat_ax_xml}"
+            "      <c:valAx>\n"
+            '        <c:axId val="2140495176"/>\n'
+            "        <c:scaling/>\n"
+            '        <c:delete val="0"/>\n'
+            '        <c:axPos val="l"/>\n'
+            "        <c:majorGridlines/>\n"
+            '        <c:majorTickMark val="out"/>\n'
+            '        <c:minorTickMark val="none"/>\n'
+            '        <c:tickLblPos val="nextTo"/>\n'
+            '        <c:crossAx val="2118791784"/>\n'
+            '        <c:crosses val="autoZero"/>\n'
+            "      </c:valAx>\n"
+            "    </c:plotArea>\n"
+            "    <c:legend>\n"
+            '      <c:legendPos val="r"/>\n'
+            "      <c:layout/>\n"
+            '      <c:overlay val="0"/>\n'
+            "    </c:legend>\n"
+            '    <c:plotVisOnly val="1"/>\n'
+            '    <c:dispBlanksAs val="gap"/>\n'
+            '    <c:showDLblsOverMax val="0"/>\n'
+            "  </c:chart>\n"
+            "  <c:txPr>\n"
+            "    <a:bodyPr/>\n"
+            "    <a:lstStyle/>\n"
+            "    <a:p>\n"
+            "      <a:pPr>\n"
+            '        <a:defRPr sz="1800"/>\n'
+            "      </a:pPr>\n"
+            '      <a:endParaRPr lang="en-US"/>\n'
+            "    </a:p>\n"
+            "  </c:txPr>\n"
+            "</c:chartSpace>\n"
+        ).format(
+            **{
+                "grouping_xml": self._grouping_xml,
+                "ser_xml": self._ser_xml,
+                "cat_ax_xml": self._cat_ax_xml,
+            }
+        )
+
+    @property
+    def _cat_ax_xml(self):
+        categories = self._chart_data.categories
+
+        if categories.are_dates:
+            return (
+                "      <c:dateAx>\n"
+                '        <c:axId val="2118791784"/>\n'
+                "        <c:scaling>\n"
+                '          <c:orientation val="minMax"/>\n'
+                "        </c:scaling>\n"
+                '        <c:delete val="0"/>\n'
+                '        <c:axPos val="b"/>\n'
+                '        <c:numFmt formatCode="{nf}" sourceLinked="1"/>\n'
+                '        <c:majorTickMark val="out"/>\n'
+                '        <c:minorTickMark val="none"/>\n'
+                '        <c:tickLblPos val="nextTo"/>\n'
+                '        <c:crossAx val="2140495176"/>\n'
+                '        <c:crosses val="autoZero"/>\n'
+                '        <c:auto val="1"/>\n'
+                '        <c:lblOffset val="100"/>\n'
+                '        <c:baseTimeUnit val="days"/>\n'
+                "      </c:dateAx>\n"
+            ).format(**{"nf": categories.number_format})
+
+        return (
+            "      <c:catAx>\n"
+            '        <c:axId val="2118791784"/>\n'
+            "        <c:scaling>\n"
+            '          <c:orientation val="minMax"/>\n'
+            "        </c:scaling>\n"
+            '        <c:delete val="0"/>\n'
+            '        <c:axPos val="b"/>\n'
+            '        <c:majorTickMark val="out"/>\n'
+            '        <c:minorTickMark val="none"/>\n'
+            '        <c:tickLblPos val="nextTo"/>\n'
+            '        <c:crossAx val="2140495176"/>\n'
+            '        <c:crosses val="autoZero"/>\n'
+            '        <c:auto val="1"/>\n'
+            '        <c:lblAlgn val="ctr"/>\n'
+            '        <c:lblOffset val="100"/>\n'
+            '        <c:noMultiLvlLbl val="0"/>\n'
+            "      </c:catAx>\n"
+        )
+
+    @property
+    def _grouping_xml(self):
+        XL = XL_CHART_TYPE
+        standard_types = (XL.LINE, XL.LINE_MARKERS)
+        stacked_types = (XL.LINE_STACKED, XL.LINE_MARKERS_STACKED)
+        percentStacked_types = (XL.LINE_STACKED_100, XL.LINE_MARKERS_STACKED_100)
+        if self._chart_type in standard_types:
+            return '        <c:grouping val="standard"/>\n'
+        elif self._chart_type in stacked_types:
+            return '        <c:grouping val="stacked"/>\n'
+        elif self._chart_type in percentStacked_types:
+            return '        <c:grouping val="percentStacked"/>\n'
+        raise NotImplementedError("no _grouping_xml() for chart type %s" % self._chart_type)
+
+    @property
+    def _marker_xml(self):
+        XL = XL_CHART_TYPE
+        no_marker_types = (XL.LINE, XL.LINE_STACKED, XL.LINE_STACKED_100)
+        if self._chart_type in no_marker_types:
+            return (
+                "          <c:marker>\n"
+                '            <c:symbol val="none"/>\n'
+                "          </c:marker>\n"
+            )
+        return ""
+
+    @property
+    def _ser_xml(self):
+        xml = ""
+        for series in self._chart_data:
+            xml_writer = _CategorySeriesXmlWriter(series)
+            xml += (
+                "        <c:ser>\n"
+                '          <c:idx val="{ser_idx}"/>\n'
+                '          <c:order val="{ser_order}"/>\n'
+                "{tx_xml}"
+                "{marker_xml}"
+                "{cat_xml}"
+                "{val_xml}"
+                '          <c:smooth val="0"/>\n'
+                "        </c:ser>\n"
+            ).format(
+                **{
+                    "ser_idx": series.index,
+                    "ser_order": series.index,
+                    "tx_xml": xml_writer.tx_xml,
+                    "marker_xml": self._marker_xml,
+                    "cat_xml": xml_writer.cat_xml,
+                    "val_xml": xml_writer.val_xml,
+                }
+            )
+        return xml
+
+
+class _PieChartXmlWriter(_BaseChartXmlWriter):
+    """
+    Provides specialized methods particular to the ``<c:pieChart>`` element.
+    """
+
+    @property
+    def xml(self):
+        return (
+            "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+            '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+            'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+            'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+            'iceDocument/2006/relationships">\n'
+            "  <c:chart>\n"
+            '    <c:autoTitleDeleted val="0"/>\n'
+            "    <c:plotArea>\n"
+            "      <c:pieChart>\n"
+            '        <c:varyColors val="1"/>\n'
+            "{ser_xml}"
+            "      </c:pieChart>\n"
+            "    </c:plotArea>\n"
+            '    <c:dispBlanksAs val="gap"/>\n'
+            "  </c:chart>\n"
+            "  <c:txPr>\n"
+            "    <a:bodyPr/>\n"
+            "    <a:lstStyle/>\n"
+            "    <a:p>\n"
+            "      <a:pPr>\n"
+            '        <a:defRPr sz="1800"/>\n'
+            "      </a:pPr>\n"
+            '      <a:endParaRPr lang="en-US"/>\n'
+            "    </a:p>\n"
+            "  </c:txPr>\n"
+            "</c:chartSpace>\n"
+        ).format(**{"ser_xml": self._ser_xml})
+
+    @property
+    def _explosion_xml(self):
+        if self._chart_type == XL_CHART_TYPE.PIE_EXPLODED:
+            return '          <c:explosion val="25"/>\n'
+        return ""
+
+    @property
+    def _ser_xml(self):
+        xml_writer = _CategorySeriesXmlWriter(self._chart_data[0])
+        xml = (
+            "        <c:ser>\n"
+            '          <c:idx val="0"/>\n'
+            '          <c:order val="0"/>\n'
+            "{tx_xml}"
+            "{explosion_xml}"
+            "{cat_xml}"
+            "{val_xml}"
+            "        </c:ser>\n"
+        ).format(
+            **{
+                "tx_xml": xml_writer.tx_xml,
+                "explosion_xml": self._explosion_xml,
+                "cat_xml": xml_writer.cat_xml,
+                "val_xml": xml_writer.val_xml,
+            }
+        )
+        return xml
+
+
+class _RadarChartXmlWriter(_BaseChartXmlWriter):
+    """
+    Generates XML for the ``<c:radarChart>`` element.
+    """
+
+    @property
+    def xml(self):
+        return (
+            "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+            '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+            'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+            'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+            'iceDocument/2006/relationships">\n'
+            '  <c:date1904 val="0"/>\n'
+            '  <c:roundedCorners val="0"/>\n'
+            '  <mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.'
+            'org/markup-compatibility/2006">\n'
+            '    <mc:Choice xmlns:c14="http://schemas.microsoft.com/office/d'
+            'rawing/2007/8/2/chart" Requires="c14">\n'
+            '      <c14:style val="118"/>\n'
+            "    </mc:Choice>\n"
+            "    <mc:Fallback>\n"
+            '      <c:style val="18"/>\n'
+            "    </mc:Fallback>\n"
+            "  </mc:AlternateContent>\n"
+            "  <c:chart>\n"
+            '    <c:autoTitleDeleted val="0"/>\n'
+            "    <c:plotArea>\n"
+            "      <c:layout/>\n"
+            "      <c:radarChart>\n"
+            '        <c:radarStyle val="{radar_style}"/>\n'
+            '        <c:varyColors val="0"/>\n'
+            "{ser_xml}"
+            '        <c:axId val="2073612648"/>\n'
+            '        <c:axId val="-2112772216"/>\n'
+            "      </c:radarChart>\n"
+            "      <c:catAx>\n"
+            '        <c:axId val="2073612648"/>\n'
+            "        <c:scaling>\n"
+            '          <c:orientation val="minMax"/>\n'
+            "        </c:scaling>\n"
+            '        <c:delete val="0"/>\n'
+            '        <c:axPos val="b"/>\n'
+            "        <c:majorGridlines/>\n"
+            '        <c:numFmt formatCode="m/d/yy" sourceLinked="1"/>\n'
+            '        <c:majorTickMark val="out"/>\n'
+            '        <c:minorTickMark val="none"/>\n'
+            '        <c:tickLblPos val="nextTo"/>\n'
+            '        <c:crossAx val="-2112772216"/>\n'
+            '        <c:crosses val="autoZero"/>\n'
+            '        <c:auto val="1"/>\n'
+            '        <c:lblAlgn val="ctr"/>\n'
+            '        <c:lblOffset val="100"/>\n'
+            '        <c:noMultiLvlLbl val="0"/>\n'
+            "      </c:catAx>\n"
+            "      <c:valAx>\n"
+            '        <c:axId val="-2112772216"/>\n'
+            "        <c:scaling>\n"
+            '          <c:orientation val="minMax"/>\n'
+            "        </c:scaling>\n"
+            '        <c:delete val="0"/>\n'
+            '        <c:axPos val="l"/>\n'
+            "        <c:majorGridlines/>\n"
+            '        <c:numFmt formatCode="General" sourceLinked="1"/>\n'
+            '        <c:majorTickMark val="cross"/>\n'
+            '        <c:minorTickMark val="none"/>\n'
+            '        <c:tickLblPos val="nextTo"/>\n'
+            '        <c:crossAx val="2073612648"/>\n'
+            '        <c:crosses val="autoZero"/>\n'
+            '        <c:crossBetween val="between"/>\n'
+            "      </c:valAx>\n"
+            "    </c:plotArea>\n"
+            '    <c:plotVisOnly val="1"/>\n'
+            '    <c:dispBlanksAs val="gap"/>\n'
+            '    <c:showDLblsOverMax val="0"/>\n'
+            "  </c:chart>\n"
+            "  <c:txPr>\n"
+            "    <a:bodyPr/>\n"
+            "    <a:lstStyle/>\n"
+            "    <a:p>\n"
+            "      <a:pPr>\n"
+            '        <a:defRPr sz="1800"/>\n'
+            "      </a:pPr>\n"
+            '      <a:endParaRPr lang="en-US"/>\n'
+            "    </a:p>\n"
+            "  </c:txPr>\n"
+            "</c:chartSpace>\n"
+        ).format(**{"radar_style": self._radar_style, "ser_xml": self._ser_xml})
+
+    @property
+    def _marker_xml(self):
+        if self._chart_type == XL_CHART_TYPE.RADAR:
+            return (
+                "          <c:marker>\n"
+                '            <c:symbol val="none"/>\n'
+                "          </c:marker>\n"
+            )
+        return ""
+
+    @property
+    def _radar_style(self):
+        if self._chart_type == XL_CHART_TYPE.RADAR_FILLED:
+            return "filled"
+        return "marker"
+
+    @property
+    def _ser_xml(self):
+        xml = ""
+        for series in self._chart_data:
+            xml_writer = _CategorySeriesXmlWriter(series)
+            xml += (
+                "        <c:ser>\n"
+                '          <c:idx val="{ser_idx}"/>\n'
+                '          <c:order val="{ser_order}"/>\n'
+                "{tx_xml}"
+                "{marker_xml}"
+                "{cat_xml}"
+                "{val_xml}"
+                '          <c:smooth val="0"/>\n'
+                "        </c:ser>\n"
+            ).format(
+                **{
+                    "ser_idx": series.index,
+                    "ser_order": series.index,
+                    "tx_xml": xml_writer.tx_xml,
+                    "marker_xml": self._marker_xml,
+                    "cat_xml": xml_writer.cat_xml,
+                    "val_xml": xml_writer.val_xml,
+                }
+            )
+        return xml
+
+
+class _XyChartXmlWriter(_BaseChartXmlWriter):
+    """
+    Generates XML for the ``<c:scatterChart>`` element.
+    """
+
+    @property
+    def xml(self):
+        xml = (
+            "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+            '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+            'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+            'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+            'iceDocument/2006/relationships">\n'
+            "  <c:chart>\n"
+            "    <c:plotArea>\n"
+            "      <c:scatterChart>\n"
+            '        <c:scatterStyle val="%s"/>\n'
+            '        <c:varyColors val="0"/>\n'
+            "%s"
+            '        <c:axId val="-2128940872"/>\n'
+            '        <c:axId val="-2129643912"/>\n'
+            "      </c:scatterChart>\n"
+            "      <c:valAx>\n"
+            '        <c:axId val="-2128940872"/>\n'
+            "        <c:scaling>\n"
+            '          <c:orientation val="minMax"/>\n'
+            "        </c:scaling>\n"
+            '        <c:delete val="0"/>\n'
+            '        <c:axPos val="b"/>\n'
+            '        <c:numFmt formatCode="General" sourceLinked="1"/>\n'
+            '        <c:majorTickMark val="out"/>\n'
+            '        <c:minorTickMark val="none"/>\n'
+            '        <c:tickLblPos val="nextTo"/>\n'
+            '        <c:crossAx val="-2129643912"/>\n'
+            '        <c:crosses val="autoZero"/>\n'
+            '        <c:crossBetween val="midCat"/>\n'
+            "      </c:valAx>\n"
+            "      <c:valAx>\n"
+            '        <c:axId val="-2129643912"/>\n'
+            "        <c:scaling>\n"
+            '          <c:orientation val="minMax"/>\n'
+            "        </c:scaling>\n"
+            '        <c:delete val="0"/>\n'
+            '        <c:axPos val="l"/>\n'
+            "        <c:majorGridlines/>\n"
+            '        <c:numFmt formatCode="General" sourceLinked="1"/>\n'
+            '        <c:majorTickMark val="out"/>\n'
+            '        <c:minorTickMark val="none"/>\n'
+            '        <c:tickLblPos val="nextTo"/>\n'
+            '        <c:crossAx val="-2128940872"/>\n'
+            '        <c:crosses val="autoZero"/>\n'
+            '        <c:crossBetween val="midCat"/>\n'
+            "      </c:valAx>\n"
+            "    </c:plotArea>\n"
+            "    <c:legend>\n"
+            '      <c:legendPos val="r"/>\n'
+            "      <c:layout/>\n"
+            '      <c:overlay val="0"/>\n'
+            "    </c:legend>\n"
+            '    <c:plotVisOnly val="1"/>\n'
+            '    <c:dispBlanksAs val="gap"/>\n'
+            '    <c:showDLblsOverMax val="0"/>\n'
+            "  </c:chart>\n"
+            "  <c:txPr>\n"
+            "    <a:bodyPr/>\n"
+            "    <a:lstStyle/>\n"
+            "    <a:p>\n"
+            "      <a:pPr>\n"
+            '        <a:defRPr sz="1800"/>\n'
+            "      </a:pPr>\n"
+            '      <a:endParaRPr lang="en-US"/>\n'
+            "    </a:p>\n"
+            "  </c:txPr>\n"
+            "</c:chartSpace>\n"
+        ) % (self._scatterStyle_val, self._ser_xml)
+        return xml
+
+    @property
+    def _marker_xml(self):
+        no_marker_types = (
+            XL_CHART_TYPE.XY_SCATTER_LINES_NO_MARKERS,
+            XL_CHART_TYPE.XY_SCATTER_SMOOTH_NO_MARKERS,
+        )
+        if self._chart_type in no_marker_types:
+            return (
+                "          <c:marker>\n"
+                '            <c:symbol val="none"/>\n'
+                "          </c:marker>\n"
+            )
+        return ""
+
+    @property
+    def _scatterStyle_val(self):
+        smooth_types = (
+            XL_CHART_TYPE.XY_SCATTER_SMOOTH,
+            XL_CHART_TYPE.XY_SCATTER_SMOOTH_NO_MARKERS,
+        )
+        if self._chart_type in smooth_types:
+            return "smoothMarker"
+        return "lineMarker"
+
+    @property
+    def _ser_xml(self):
+        xml = ""
+        for series in self._chart_data:
+            xml_writer = _XySeriesXmlWriter(series)
+            xml += (
+                "        <c:ser>\n"
+                '          <c:idx val="{ser_idx}"/>\n'
+                '          <c:order val="{ser_order}"/>\n'
+                "{tx_xml}"
+                "{spPr_xml}"
+                "{marker_xml}"
+                "{xVal_xml}"
+                "{yVal_xml}"
+                '          <c:smooth val="0"/>\n'
+                "        </c:ser>\n"
+            ).format(
+                **{
+                    "ser_idx": series.index,
+                    "ser_order": series.index,
+                    "tx_xml": xml_writer.tx_xml,
+                    "spPr_xml": self._spPr_xml,
+                    "marker_xml": self._marker_xml,
+                    "xVal_xml": xml_writer.xVal_xml,
+                    "yVal_xml": xml_writer.yVal_xml,
+                }
+            )
+        return xml
+
+    @property
+    def _spPr_xml(self):
+        if self._chart_type == XL_CHART_TYPE.XY_SCATTER:
+            return (
+                "          <c:spPr>\n"
+                '            <a:ln w="47625">\n'
+                "              <a:noFill/>\n"
+                "            </a:ln>\n"
+                "          </c:spPr>\n"
+            )
+        return ""
+
+
+class _BubbleChartXmlWriter(_XyChartXmlWriter):
+    """
+    Provides specialized methods particular to the ``<c:bubbleChart>``
+    element.
+    """
+
+    @property
+    def xml(self):
+        xml = (
+            "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+            '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+            'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+            'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+            'iceDocument/2006/relationships">\n'
+            "  <c:chart>\n"
+            '    <c:autoTitleDeleted val="0"/>\n'
+            "    <c:plotArea>\n"
+            "      <c:layout/>\n"
+            "      <c:bubbleChart>\n"
+            '        <c:varyColors val="0"/>\n'
+            "%s"
+            "        <c:dLbls>\n"
+            '          <c:showLegendKey val="0"/>\n'
+            '          <c:showVal val="0"/>\n'
+            '          <c:showCatName val="0"/>\n'
+            '          <c:showSerName val="0"/>\n'
+            '          <c:showPercent val="0"/>\n'
+            '          <c:showBubbleSize val="0"/>\n'
+            "        </c:dLbls>\n"
+            '        <c:bubbleScale val="100"/>\n'
+            '        <c:showNegBubbles val="0"/>\n'
+            '        <c:axId val="-2115720072"/>\n'
+            '        <c:axId val="-2115723560"/>\n'
+            "      </c:bubbleChart>\n"
+            "      <c:valAx>\n"
+            '        <c:axId val="-2115720072"/>\n'
+            "        <c:scaling>\n"
+            '          <c:orientation val="minMax"/>\n'
+            "        </c:scaling>\n"
+            '        <c:delete val="0"/>\n'
+            '        <c:axPos val="b"/>\n'
+            '        <c:numFmt formatCode="General" sourceLinked="1"/>\n'
+            '        <c:majorTickMark val="out"/>\n'
+            '        <c:minorTickMark val="none"/>\n'
+            '        <c:tickLblPos val="nextTo"/>\n'
+            '        <c:crossAx val="-2115723560"/>\n'
+            '        <c:crosses val="autoZero"/>\n'
+            '        <c:crossBetween val="midCat"/>\n'
+            "      </c:valAx>\n"
+            "      <c:valAx>\n"
+            '        <c:axId val="-2115723560"/>\n'
+            "        <c:scaling>\n"
+            '          <c:orientation val="minMax"/>\n'
+            "        </c:scaling>\n"
+            '        <c:delete val="0"/>\n'
+            '        <c:axPos val="l"/>\n'
+            "        <c:majorGridlines/>\n"
+            '        <c:numFmt formatCode="General" sourceLinked="1"/>\n'
+            '        <c:majorTickMark val="out"/>\n'
+            '        <c:minorTickMark val="none"/>\n'
+            '        <c:tickLblPos val="nextTo"/>\n'
+            '        <c:crossAx val="-2115720072"/>\n'
+            '        <c:crosses val="autoZero"/>\n'
+            '        <c:crossBetween val="midCat"/>\n'
+            "      </c:valAx>\n"
+            "    </c:plotArea>\n"
+            "    <c:legend>\n"
+            '      <c:legendPos val="r"/>\n'
+            "      <c:layout/>\n"
+            '      <c:overlay val="0"/>\n'
+            "    </c:legend>\n"
+            '    <c:plotVisOnly val="1"/>\n'
+            '    <c:dispBlanksAs val="gap"/>\n'
+            '    <c:showDLblsOverMax val="0"/>\n'
+            "  </c:chart>\n"
+            "  <c:txPr>\n"
+            "    <a:bodyPr/>\n"
+            "    <a:lstStyle/>\n"
+            "    <a:p>\n"
+            "      <a:pPr>\n"
+            '        <a:defRPr sz="1800"/>\n'
+            "      </a:pPr>\n"
+            '      <a:endParaRPr lang="en-US"/>\n'
+            "    </a:p>\n"
+            "  </c:txPr>\n"
+            "</c:chartSpace>\n"
+        ) % self._ser_xml
+        return xml
+
+    @property
+    def _bubble3D_val(self):
+        if self._chart_type == XL_CHART_TYPE.BUBBLE_THREE_D_EFFECT:
+            return "1"
+        return "0"
+
+    @property
+    def _ser_xml(self):
+        xml = ""
+        for series in self._chart_data:
+            xml_writer = _BubbleSeriesXmlWriter(series)
+            xml += (
+                "        <c:ser>\n"
+                '          <c:idx val="{ser_idx}"/>\n'
+                '          <c:order val="{ser_order}"/>\n'
+                "{tx_xml}"
+                '          <c:invertIfNegative val="0"/>\n'
+                "{xVal_xml}"
+                "{yVal_xml}"
+                "{bubbleSize_xml}"
+                '          <c:bubble3D val="{bubble3D_val}"/>\n'
+                "        </c:ser>\n"
+            ).format(
+                **{
+                    "ser_idx": series.index,
+                    "ser_order": series.index,
+                    "tx_xml": xml_writer.tx_xml,
+                    "xVal_xml": xml_writer.xVal_xml,
+                    "yVal_xml": xml_writer.yVal_xml,
+                    "bubbleSize_xml": xml_writer.bubbleSize_xml,
+                    "bubble3D_val": self._bubble3D_val,
+                }
+            )
+        return xml
+
+
+class _CategorySeriesXmlWriter(_BaseSeriesXmlWriter):
+    """
+    Generates XML snippets particular to a category chart series.
+    """
+
+    @property
+    def cat(self):
+        """
+        Return the ``<c:cat>`` element XML for this series, as an oxml
+        element.
+        """
+        categories = self._series.categories
+
+        if categories.are_numeric:
+            return parse_xml(
+                self._numRef_cat_tmpl.format(
+                    **{
+                        "wksht_ref": self._series.categories_ref,
+                        "number_format": categories.number_format,
+                        "cat_count": categories.leaf_count,
+                        "cat_pt_xml": self._cat_num_pt_xml,
+                        "nsdecls": " %s" % nsdecls("c"),
+                    }
+                )
+            )
+
+        if categories.depth == 1:
+            return parse_xml(
+                self._cat_tmpl.format(
+                    **{
+                        "wksht_ref": self._series.categories_ref,
+                        "cat_count": categories.leaf_count,
+                        "cat_pt_xml": self._cat_pt_xml,
+                        "nsdecls": " %s" % nsdecls("c"),
+                    }
+                )
+            )
+
+        return parse_xml(
+            self._multiLvl_cat_tmpl.format(
+                **{
+                    "wksht_ref": self._series.categories_ref,
+                    "cat_count": categories.leaf_count,
+                    "lvl_xml": self._lvl_xml(categories),
+                    "nsdecls": " %s" % nsdecls("c"),
+                }
+            )
+        )
+
+    @property
+    def cat_xml(self):
+        """
+        The unicode XML snippet for the ``<c:cat>`` element for this series,
+        containing the category labels and spreadsheet reference.
+        """
+        categories = self._series.categories
+
+        if categories.are_numeric:
+            return self._numRef_cat_tmpl.format(
+                **{
+                    "wksht_ref": self._series.categories_ref,
+                    "number_format": categories.number_format,
+                    "cat_count": categories.leaf_count,
+                    "cat_pt_xml": self._cat_num_pt_xml,
+                    "nsdecls": "",
+                }
+            )
+
+        if categories.depth == 1:
+            return self._cat_tmpl.format(
+                **{
+                    "wksht_ref": self._series.categories_ref,
+                    "cat_count": categories.leaf_count,
+                    "cat_pt_xml": self._cat_pt_xml,
+                    "nsdecls": "",
+                }
+            )
+
+        return self._multiLvl_cat_tmpl.format(
+            **{
+                "wksht_ref": self._series.categories_ref,
+                "cat_count": categories.leaf_count,
+                "lvl_xml": self._lvl_xml(categories),
+                "nsdecls": "",
+            }
+        )
+
+    @property
+    def val(self):
+        """
+        The ``<c:val>`` XML for this series, as an oxml element.
+        """
+        xml = self._val_tmpl.format(
+            **{
+                "nsdecls": " %s" % nsdecls("c"),
+                "values_ref": self._series.values_ref,
+                "number_format": self._series.number_format,
+                "val_count": len(self._series),
+                "val_pt_xml": self._val_pt_xml,
+            }
+        )
+        return parse_xml(xml)
+
+    @property
+    def val_xml(self):
+        """
+        Return the unicode XML snippet for the ``<c:val>`` element describing
+        this series, containing the series values and their spreadsheet range
+        reference.
+        """
+        return self._val_tmpl.format(
+            **{
+                "nsdecls": "",
+                "values_ref": self._series.values_ref,
+                "number_format": self._series.number_format,
+                "val_count": len(self._series),
+                "val_pt_xml": self._val_pt_xml,
+            }
+        )
+
+    @property
+    def _cat_num_pt_xml(self):
+        """
+        The unicode XML snippet for the ``<c:pt>`` elements when category
+        labels are numeric (including date type).
+        """
+        xml = ""
+        for idx, category in enumerate(self._series.categories):
+            xml += (
+                '                <c:pt idx="{cat_idx}">\n'
+                "                  <c:v>{cat_lbl_str}</c:v>\n"
+                "                </c:pt>\n"
+            ).format(
+                **{
+                    "cat_idx": idx,
+                    "cat_lbl_str": category.numeric_str_val(self._date_1904),
+                }
+            )
+        return xml
+
+    @property
+    def _cat_pt_xml(self):
+        """
+        The unicode XML snippet for the ``<c:pt>`` elements containing the
+        category names for this series.
+        """
+        xml = ""
+        for idx, category in enumerate(self._series.categories):
+            xml += (
+                '                <c:pt idx="{cat_idx}">\n'
+                "                  <c:v>{cat_label}</c:v>\n"
+                "                </c:pt>\n"
+            ).format(**{"cat_idx": idx, "cat_label": escape(str(category.label))})
+        return xml
+
+    @property
+    def _cat_tmpl(self):
+        """
+        The template for the ``<c:cat>`` element for this series, containing
+        the category labels and spreadsheet reference.
+        """
+        return (
+            "          <c:cat{nsdecls}>\n"
+            "            <c:strRef>\n"
+            "              <c:f>{wksht_ref}</c:f>\n"
+            "              <c:strCache>\n"
+            '                <c:ptCount val="{cat_count}"/>\n'
+            "{cat_pt_xml}"
+            "              </c:strCache>\n"
+            "            </c:strRef>\n"
+            "          </c:cat>\n"
+        )
+
+    def _lvl_xml(self, categories):
+        """
+        The unicode XML snippet for the ``<c:lvl>`` elements containing
+        multi-level category names.
+        """
+
+        def lvl_pt_xml(level):
+            xml = ""
+            for idx, name in level:
+                xml += (
+                    '                  <c:pt idx="%d">\n'
+                    "                    <c:v>%s</c:v>\n"
+                    "                  </c:pt>\n"
+                ) % (idx, escape("%s" % name))
+            return xml
+
+        xml = ""
+        for level in categories.levels:
+            xml += ("                <c:lvl>\n" "{lvl_pt_xml}" "                </c:lvl>\n").format(
+                **{"lvl_pt_xml": lvl_pt_xml(level)}
+            )
+        return xml
+
+    @property
+    def _multiLvl_cat_tmpl(self):
+        """
+        The template for the ``<c:cat>`` element for this series when there
+        are multi-level (nested) categories.
+        """
+        return (
+            "          <c:cat{nsdecls}>\n"
+            "            <c:multiLvlStrRef>\n"
+            "              <c:f>{wksht_ref}</c:f>\n"
+            "              <c:multiLvlStrCache>\n"
+            '                <c:ptCount val="{cat_count}"/>\n'
+            "{lvl_xml}"
+            "              </c:multiLvlStrCache>\n"
+            "            </c:multiLvlStrRef>\n"
+            "          </c:cat>\n"
+        )
+
+    @property
+    def _numRef_cat_tmpl(self):
+        """
+        The template for the ``<c:cat>`` element for this series when the
+        labels are numeric (or date) values.
+        """
+        return (
+            "          <c:cat{nsdecls}>\n"
+            "            <c:numRef>\n"
+            "              <c:f>{wksht_ref}</c:f>\n"
+            "              <c:numCache>\n"
+            "                <c:formatCode>{number_format}</c:formatCode>\n"
+            '                <c:ptCount val="{cat_count}"/>\n'
+            "{cat_pt_xml}"
+            "              </c:numCache>\n"
+            "            </c:numRef>\n"
+            "          </c:cat>\n"
+        )
+
+    @property
+    def _val_pt_xml(self):
+        """
+        The unicode XML snippet containing the ``<c:pt>`` elements containing
+        the values for this series.
+        """
+        xml = ""
+        for idx, value in enumerate(self._series.values):
+            if value is None:
+                continue
+            xml += (
+                '                <c:pt idx="{val_idx:d}">\n'
+                "                  <c:v>{value}</c:v>\n"
+                "                </c:pt>\n"
+            ).format(**{"val_idx": idx, "value": value})
+        return xml
+
+    @property
+    def _val_tmpl(self):
+        """
+        The template for the ``<c:val>`` element for this series, containing
+        the series values and their spreadsheet range reference.
+        """
+        return (
+            "          <c:val{nsdecls}>\n"
+            "            <c:numRef>\n"
+            "              <c:f>{values_ref}</c:f>\n"
+            "              <c:numCache>\n"
+            "                <c:formatCode>{number_format}</c:formatCode>\n"
+            '                <c:ptCount val="{val_count}"/>\n'
+            "{val_pt_xml}"
+            "              </c:numCache>\n"
+            "            </c:numRef>\n"
+            "          </c:val>\n"
+        )
+
+
+class _XySeriesXmlWriter(_BaseSeriesXmlWriter):
+    """
+    Generates XML snippets particular to an XY series.
+    """
+
+    @property
+    def xVal(self):
+        """
+        Return the ``<c:xVal>`` element for this series as an oxml element.
+        This element contains the X values for this series.
+        """
+        xml = self._xVal_tmpl.format(
+            **{
+                "nsdecls": " %s" % nsdecls("c"),
+                "numRef_xml": self.numRef_xml(
+                    self._series.x_values_ref,
+                    self._series.number_format,
+                    self._series.x_values,
+                ),
+            }
+        )
+        return parse_xml(xml)
+
+    @property
+    def xVal_xml(self):
+        """
+        Return the ``<c:xVal>`` element for this series as unicode text. This
+        element contains the X values for this series.
+        """
+        return self._xVal_tmpl.format(
+            **{
+                "nsdecls": "",
+                "numRef_xml": self.numRef_xml(
+                    self._series.x_values_ref,
+                    self._series.number_format,
+                    self._series.x_values,
+                ),
+            }
+        )
+
+    @property
+    def yVal(self):
+        """
+        Return the ``<c:yVal>`` element for this series as an oxml element.
+        This element contains the Y values for this series.
+        """
+        xml = self._yVal_tmpl.format(
+            **{
+                "nsdecls": " %s" % nsdecls("c"),
+                "numRef_xml": self.numRef_xml(
+                    self._series.y_values_ref,
+                    self._series.number_format,
+                    self._series.y_values,
+                ),
+            }
+        )
+        return parse_xml(xml)
+
+    @property
+    def yVal_xml(self):
+        """
+        Return the ``<c:yVal>`` element for this series as unicode text. This
+        element contains the Y values for this series.
+        """
+        return self._yVal_tmpl.format(
+            **{
+                "nsdecls": "",
+                "numRef_xml": self.numRef_xml(
+                    self._series.y_values_ref,
+                    self._series.number_format,
+                    self._series.y_values,
+                ),
+            }
+        )
+
+    @property
+    def _xVal_tmpl(self):
+        """
+        The template for the ``<c:xVal>`` element for this series, containing
+        the X values and their spreadsheet range reference.
+        """
+        return "          <c:xVal{nsdecls}>\n" "{numRef_xml}" "          </c:xVal>\n"
+
+    @property
+    def _yVal_tmpl(self):
+        """
+        The template for the ``<c:yVal>`` element for this series, containing
+        the Y values and their spreadsheet range reference.
+        """
+        return "          <c:yVal{nsdecls}>\n" "{numRef_xml}" "          </c:yVal>\n"
+
+
+class _BubbleSeriesXmlWriter(_XySeriesXmlWriter):
+    """
+    Generates XML snippets particular to a bubble chart series.
+    """
+
+    @property
+    def bubbleSize(self):
+        """
+        Return the ``<c:bubbleSize>`` element for this series as an oxml
+        element. This element contains the bubble size values for this
+        series.
+        """
+        xml = self._bubbleSize_tmpl.format(
+            **{
+                "nsdecls": " %s" % nsdecls("c"),
+                "numRef_xml": self.numRef_xml(
+                    self._series.bubble_sizes_ref,
+                    self._series.number_format,
+                    self._series.bubble_sizes,
+                ),
+            }
+        )
+        return parse_xml(xml)
+
+    @property
+    def bubbleSize_xml(self):
+        """
+        Return the ``<c:bubbleSize>`` element for this series as unicode
+        text. This element contains the bubble size values for all the
+        data points in the chart.
+        """
+        return self._bubbleSize_tmpl.format(
+            **{
+                "nsdecls": "",
+                "numRef_xml": self.numRef_xml(
+                    self._series.bubble_sizes_ref,
+                    self._series.number_format,
+                    self._series.bubble_sizes,
+                ),
+            }
+        )
+
+    @property
+    def _bubbleSize_tmpl(self):
+        """
+        The template for the ``<c:bubbleSize>`` element for this series,
+        containing the bubble size values and their spreadsheet range
+        reference.
+        """
+        return "          <c:bubbleSize{nsdecls}>\n" "{numRef_xml}" "          </c:bubbleSize>\n"
+
+
+class _BubbleSeriesXmlRewriter(_BaseSeriesXmlRewriter):
+    """
+    A series rewriter suitable for bubble charts.
+    """
+
+    def _rewrite_ser_data(self, ser, series_data, date_1904):
+        """
+        Rewrite the ``<c:tx>``, ``<c:cat>`` and ``<c:val>`` child elements
+        of *ser* based on the values in *series_data*.
+        """
+        ser._remove_tx()
+        ser._remove_xVal()
+        ser._remove_yVal()
+        ser._remove_bubbleSize()
+
+        xml_writer = _BubbleSeriesXmlWriter(series_data)
+
+        ser._insert_tx(xml_writer.tx)
+        ser._insert_xVal(xml_writer.xVal)
+        ser._insert_yVal(xml_writer.yVal)
+        ser._insert_bubbleSize(xml_writer.bubbleSize)
+
+
+class _CategorySeriesXmlRewriter(_BaseSeriesXmlRewriter):
+    """
+    A series rewriter suitable for category charts.
+    """
+
+    def _rewrite_ser_data(self, ser, series_data, date_1904):
+        """
+        Rewrite the ``<c:tx>``, ``<c:cat>`` and ``<c:val>`` child elements
+        of *ser* based on the values in *series_data*.
+        """
+        ser._remove_tx()
+        ser._remove_cat()
+        ser._remove_val()
+
+        xml_writer = _CategorySeriesXmlWriter(series_data, date_1904)
+
+        ser._insert_tx(xml_writer.tx)
+        ser._insert_cat(xml_writer.cat)
+        ser._insert_val(xml_writer.val)
+
+
+class _XySeriesXmlRewriter(_BaseSeriesXmlRewriter):
+    """
+    A series rewriter suitable for XY (aka. scatter) charts.
+    """
+
+    def _rewrite_ser_data(self, ser, series_data, date_1904):
+        """
+        Rewrite the ``<c:tx>``, ``<c:xVal>`` and ``<c:yVal>`` child elements
+        of *ser* based on the values in *series_data*.
+        """
+        ser._remove_tx()
+        ser._remove_xVal()
+        ser._remove_yVal()
+
+        xml_writer = _XySeriesXmlWriter(series_data)
+
+        ser._insert_tx(xml_writer.tx)
+        ser._insert_xVal(xml_writer.xVal)
+        ser._insert_yVal(xml_writer.yVal)
diff --git a/.venv/lib/python3.12/site-packages/pptx/dml/__init__.py b/.venv/lib/python3.12/site-packages/pptx/dml/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/dml/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/dml/chtfmt.py b/.venv/lib/python3.12/site-packages/pptx/dml/chtfmt.py
new file mode 100644
index 00000000..c37e4844
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/dml/chtfmt.py
@@ -0,0 +1,40 @@
+"""|ChartFormat| and related objects.
+
+|ChartFormat| acts as proxy for the `spPr` element, which provides visual shape properties such as
+line and fill for chart elements.
+"""
+
+from __future__ import annotations
+
+from pptx.dml.fill import FillFormat
+from pptx.dml.line import LineFormat
+from pptx.shared import ElementProxy
+from pptx.util import lazyproperty
+
+
+class ChartFormat(ElementProxy):
+    """
+    The |ChartFormat| object provides access to visual shape properties for
+    chart elements like |Axis|, |Series|, and |MajorGridlines|. It has two
+    properties, :attr:`fill` and :attr:`line`, which return a |FillFormat|
+    and |LineFormat| object respectively. The |ChartFormat| object is
+    provided by the :attr:`format` property on the target axis, series, etc.
+    """
+
+    @lazyproperty
+    def fill(self):
+        """
+        |FillFormat| instance for this object, providing access to fill
+        properties such as fill color.
+        """
+        spPr = self._element.get_or_add_spPr()
+        return FillFormat.from_fill_parent(spPr)
+
+    @lazyproperty
+    def line(self):
+        """
+        The |LineFormat| object providing access to the visual properties of
+        this object, such as line color and line style.
+        """
+        spPr = self._element.get_or_add_spPr()
+        return LineFormat(spPr)
diff --git a/.venv/lib/python3.12/site-packages/pptx/dml/color.py b/.venv/lib/python3.12/site-packages/pptx/dml/color.py
new file mode 100644
index 00000000..54155823
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/dml/color.py
@@ -0,0 +1,301 @@
+"""DrawingML objects related to color, ColorFormat being the most prominent."""
+
+from __future__ import annotations
+
+from pptx.enum.dml import MSO_COLOR_TYPE, MSO_THEME_COLOR
+from pptx.oxml.dml.color import (
+    CT_HslColor,
+    CT_PresetColor,
+    CT_SchemeColor,
+    CT_ScRgbColor,
+    CT_SRgbColor,
+    CT_SystemColor,
+)
+
+
+class ColorFormat(object):
+    """
+    Provides access to color settings such as RGB color, theme color, and
+    luminance adjustments.
+    """
+
+    def __init__(self, eg_colorChoice_parent, color):
+        super(ColorFormat, self).__init__()
+        self._xFill = eg_colorChoice_parent
+        self._color = color
+
+    @property
+    def brightness(self):
+        """
+        Read/write float value between -1.0 and 1.0 indicating the brightness
+        adjustment for this color, e.g. -0.25 is 25% darker and 0.4 is 40%
+        lighter. 0 means no brightness adjustment.
+        """
+        return self._color.brightness
+
+    @brightness.setter
+    def brightness(self, value):
+        self._validate_brightness_value(value)
+        self._color.brightness = value
+
+    @classmethod
+    def from_colorchoice_parent(cls, eg_colorChoice_parent):
+        xClr = eg_colorChoice_parent.eg_colorChoice
+        color = _Color(xClr)
+        color_format = cls(eg_colorChoice_parent, color)
+        return color_format
+
+    @property
+    def rgb(self):
+        """
+        |RGBColor| value of this color, or None if no RGB color is explicitly
+        defined for this font. Setting this value to an |RGBColor| instance
+        causes its type to change to MSO_COLOR_TYPE.RGB. If the color was a
+        theme color with a brightness adjustment, the brightness adjustment
+        is removed when changing it to an RGB color.
+        """
+        return self._color.rgb
+
+    @rgb.setter
+    def rgb(self, rgb):
+        if not isinstance(rgb, RGBColor):
+            raise ValueError("assigned value must be type RGBColor")
+        # change to rgb color format if not already
+        if not isinstance(self._color, _SRgbColor):
+            srgbClr = self._xFill.get_or_change_to_srgbClr()
+            self._color = _SRgbColor(srgbClr)
+        # call _SRgbColor instance to do the setting
+        self._color.rgb = rgb
+
+    @property
+    def theme_color(self):
+        """Theme color value of this color.
+
+        Value is a member of :ref:`MsoThemeColorIndex`, e.g.
+        ``MSO_THEME_COLOR.ACCENT_1``. Raises AttributeError on access if the
+        color is not type ``MSO_COLOR_TYPE.SCHEME``. Assigning a member of
+        :ref:`MsoThemeColorIndex` causes the color's type to change to
+        ``MSO_COLOR_TYPE.SCHEME``.
+        """
+        return self._color.theme_color
+
+    @theme_color.setter
+    def theme_color(self, mso_theme_color_idx):
+        # change to theme color format if not already
+        if not isinstance(self._color, _SchemeColor):
+            schemeClr = self._xFill.get_or_change_to_schemeClr()
+            self._color = _SchemeColor(schemeClr)
+        self._color.theme_color = mso_theme_color_idx
+
+    @property
+    def type(self):
+        """
+        Read-only. A value from :ref:`MsoColorType`, either RGB or SCHEME,
+        corresponding to the way this color is defined, or None if no color
+        is defined at the level of this font.
+        """
+        return self._color.color_type
+
+    def _validate_brightness_value(self, value):
+        if value < -1.0 or value > 1.0:
+            raise ValueError("brightness must be number in range -1.0 to 1.0")
+        if isinstance(self._color, _NoneColor):
+            msg = (
+                "can't set brightness when color.type is None. Set color.rgb"
+                " or .theme_color first."
+            )
+            raise ValueError(msg)
+
+
+class _Color(object):
+    """
+    Object factory for color object of the appropriate type, also the base
+    class for all color type classes such as SRgbColor.
+    """
+
+    def __new__(cls, xClr):
+        color_cls = {
+            type(None): _NoneColor,
+            CT_HslColor: _HslColor,
+            CT_PresetColor: _PrstColor,
+            CT_SchemeColor: _SchemeColor,
+            CT_ScRgbColor: _ScRgbColor,
+            CT_SRgbColor: _SRgbColor,
+            CT_SystemColor: _SysColor,
+        }[type(xClr)]
+        return super(_Color, cls).__new__(color_cls)
+
+    def __init__(self, xClr):
+        super(_Color, self).__init__()
+        self._xClr = xClr
+
+    @property
+    def brightness(self):
+        lumMod, lumOff = self._xClr.lumMod, self._xClr.lumOff
+        # a tint is lighter, a shade is darker
+        # only tints have lumOff child
+        if lumOff is not None:
+            brightness = lumOff.val
+            return brightness
+        # which leaves shades, if lumMod is present
+        if lumMod is not None:
+            brightness = lumMod.val - 1.0
+            return brightness
+        # there's no brightness adjustment if no lum{Mod|Off} elements
+        return 0
+
+    @brightness.setter
+    def brightness(self, value):
+        if value > 0:
+            self._tint(value)
+        elif value < 0:
+            self._shade(value)
+        else:
+            self._xClr.clear_lum()
+
+    @property
+    def color_type(self):  # pragma: no cover
+        tmpl = ".color_type property must be implemented on %s"
+        raise NotImplementedError(tmpl % self.__class__.__name__)
+
+    @property
+    def rgb(self):
+        """
+        Raises TypeError on access unless overridden by subclass.
+        """
+        tmpl = "no .rgb property on color type '%s'"
+        raise AttributeError(tmpl % self.__class__.__name__)
+
+    @property
+    def theme_color(self):
+        """
+        Raises TypeError on access unless overridden by subclass.
+        """
+        return MSO_THEME_COLOR.NOT_THEME_COLOR
+
+    def _shade(self, value):
+        lumMod_val = 1.0 - abs(value)
+        color_elm = self._xClr.clear_lum()
+        color_elm.add_lumMod(lumMod_val)
+
+    def _tint(self, value):
+        lumOff_val = value
+        lumMod_val = 1.0 - lumOff_val
+        color_elm = self._xClr.clear_lum()
+        color_elm.add_lumMod(lumMod_val)
+        color_elm.add_lumOff(lumOff_val)
+
+
+class _HslColor(_Color):
+    @property
+    def color_type(self):
+        return MSO_COLOR_TYPE.HSL
+
+
+class _NoneColor(_Color):
+    @property
+    def color_type(self):
+        return None
+
+    @property
+    def theme_color(self):
+        """
+        Raise TypeError on attempt to access .theme_color when no color
+        choice is present.
+        """
+        tmpl = "no .theme_color property on color type '%s'"
+        raise AttributeError(tmpl % self.__class__.__name__)
+
+
+class _PrstColor(_Color):
+    @property
+    def color_type(self):
+        return MSO_COLOR_TYPE.PRESET
+
+
+class _SchemeColor(_Color):
+    def __init__(self, schemeClr):
+        super(_SchemeColor, self).__init__(schemeClr)
+        self._schemeClr = schemeClr
+
+    @property
+    def color_type(self):
+        return MSO_COLOR_TYPE.SCHEME
+
+    @property
+    def theme_color(self):
+        """
+        Theme color value of this color, one of those defined in the
+        MSO_THEME_COLOR enumeration, e.g. MSO_THEME_COLOR.ACCENT_1. None if
+        no theme color is explicitly defined for this font. Setting this to a
+        value in MSO_THEME_COLOR causes the color's type to change to
+        ``MSO_COLOR_TYPE.SCHEME``.
+        """
+        return self._schemeClr.val
+
+    @theme_color.setter
+    def theme_color(self, mso_theme_color_idx):
+        self._schemeClr.val = mso_theme_color_idx
+
+
+class _ScRgbColor(_Color):
+    @property
+    def color_type(self):
+        return MSO_COLOR_TYPE.SCRGB
+
+
+class _SRgbColor(_Color):
+    def __init__(self, srgbClr):
+        super(_SRgbColor, self).__init__(srgbClr)
+        self._srgbClr = srgbClr
+
+    @property
+    def color_type(self):
+        return MSO_COLOR_TYPE.RGB
+
+    @property
+    def rgb(self):
+        """
+        |RGBColor| value of this color, corresponding to the value in the
+        required ``val`` attribute of the ``<a:srgbColr>`` element.
+        """
+        return RGBColor.from_string(self._srgbClr.val)
+
+    @rgb.setter
+    def rgb(self, rgb):
+        self._srgbClr.val = str(rgb)
+
+
+class _SysColor(_Color):
+    @property
+    def color_type(self):
+        return MSO_COLOR_TYPE.SYSTEM
+
+
+class RGBColor(tuple):
+    """
+    Immutable value object defining a particular RGB color.
+    """
+
+    def __new__(cls, r, g, b):
+        msg = "RGBColor() takes three integer values 0-255"
+        for val in (r, g, b):
+            if not isinstance(val, int) or val < 0 or val > 255:
+                raise ValueError(msg)
+        return super(RGBColor, cls).__new__(cls, (r, g, b))
+
+    def __str__(self):
+        """
+        Return a hex string rgb value, like '3C2F80'
+        """
+        return "%02X%02X%02X" % self
+
+    @classmethod
+    def from_string(cls, rgb_hex_str):
+        """
+        Return a new instance from an RGB color hex string like ``'3C2F80'``.
+        """
+        r = int(rgb_hex_str[:2], 16)
+        g = int(rgb_hex_str[2:4], 16)
+        b = int(rgb_hex_str[4:], 16)
+        return cls(r, g, b)
diff --git a/.venv/lib/python3.12/site-packages/pptx/dml/effect.py b/.venv/lib/python3.12/site-packages/pptx/dml/effect.py
new file mode 100644
index 00000000..9df69ce4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/dml/effect.py
@@ -0,0 +1,41 @@
+"""Visual effects on a shape such as shadow, glow, and reflection."""
+
+from __future__ import annotations
+
+
+class ShadowFormat(object):
+    """Provides access to shadow effect on a shape."""
+
+    def __init__(self, spPr):
+        # ---spPr may also be a grpSpPr; both have a:effectLst child---
+        self._element = spPr
+
+    @property
+    def inherit(self):
+        """True if shape inherits shadow settings.
+
+        Read/write. An explicitly-defined shadow setting on a shape causes
+        this property to return |False|. A shape with no explicitly-defined
+        shadow setting inherits its shadow settings from the style hierarchy
+        (and so returns |True|).
+
+        Assigning |True| causes any explicitly-defined shadow setting to be
+        removed and inheritance is restored. Note this has the side-effect of
+        removing **all** explicitly-defined effects, such as glow and
+        reflection, and restoring inheritance for all effects on the shape.
+        Assigning |False| causes the inheritance link to be broken and **no**
+        effects to appear on the shape.
+        """
+        if self._element.effectLst is None:
+            return True
+        return False
+
+    @inherit.setter
+    def inherit(self, value):
+        inherit = bool(value)
+        if inherit:
+            # ---remove any explicitly-defined effects
+            self._element._remove_effectLst()
+        else:
+            # ---ensure at least the effectLst element is present
+            self._element.get_or_add_effectLst()
diff --git a/.venv/lib/python3.12/site-packages/pptx/dml/fill.py b/.venv/lib/python3.12/site-packages/pptx/dml/fill.py
new file mode 100644
index 00000000..8212af9e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/dml/fill.py
@@ -0,0 +1,398 @@
+"""DrawingML objects related to fill."""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+from pptx.dml.color import ColorFormat
+from pptx.enum.dml import MSO_FILL
+from pptx.oxml.dml.fill import (
+    CT_BlipFillProperties,
+    CT_GradientFillProperties,
+    CT_GroupFillProperties,
+    CT_NoFillProperties,
+    CT_PatternFillProperties,
+    CT_SolidColorFillProperties,
+)
+from pptx.oxml.xmlchemy import BaseOxmlElement
+from pptx.shared import ElementProxy
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.enum.dml import MSO_FILL_TYPE
+    from pptx.oxml.xmlchemy import BaseOxmlElement
+
+
+class FillFormat(object):
+    """Provides access to the current fill properties.
+
+    Also provides methods to change the fill type.
+    """
+
+    def __init__(self, eg_fill_properties_parent: BaseOxmlElement, fill_obj: _Fill):
+        super(FillFormat, self).__init__()
+        self._xPr = eg_fill_properties_parent
+        self._fill = fill_obj
+
+    @classmethod
+    def from_fill_parent(cls, eg_fillProperties_parent: BaseOxmlElement) -> FillFormat:
+        """
+        Return a |FillFormat| instance initialized to the settings contained
+        in *eg_fillProperties_parent*, which must be an element having
+        EG_FillProperties in its child element sequence in the XML schema.
+        """
+        fill_elm = eg_fillProperties_parent.eg_fillProperties
+        fill = _Fill(fill_elm)
+        fill_format = cls(eg_fillProperties_parent, fill)
+        return fill_format
+
+    @property
+    def back_color(self):
+        """Return a |ColorFormat| object representing background color.
+
+        This property is only applicable to pattern fills and lines.
+        """
+        return self._fill.back_color
+
+    def background(self):
+        """
+        Sets the fill type to noFill, i.e. transparent.
+        """
+        noFill = self._xPr.get_or_change_to_noFill()
+        self._fill = _NoFill(noFill)
+
+    @property
+    def fore_color(self):
+        """
+        Return a |ColorFormat| instance representing the foreground color of
+        this fill.
+        """
+        return self._fill.fore_color
+
+    def gradient(self):
+        """Sets the fill type to gradient.
+
+        If the fill is not already a gradient, a default gradient is added.
+        The default gradient corresponds to the default in the built-in
+        PowerPoint "White" template. This gradient is linear at angle
+        90-degrees (upward), with two stops. The first stop is Accent-1 with
+        tint 100%, shade 100%, and satMod 130%. The second stop is Accent-1
+        with tint 50%, shade 100%, and satMod 350%.
+        """
+        gradFill = self._xPr.get_or_change_to_gradFill()
+        self._fill = _GradFill(gradFill)
+
+    @property
+    def gradient_angle(self):
+        """Angle in float degrees of line of a linear gradient.
+
+        Read/Write. May be |None|, indicating the angle should be inherited
+        from the style hierarchy. An angle of 0.0 corresponds to
+        a left-to-right gradient. Increasing angles represent
+        counter-clockwise rotation of the line, for example 90.0 represents
+        a bottom-to-top gradient. Raises |TypeError| when the fill type is
+        not MSO_FILL_TYPE.GRADIENT. Raises |ValueError| for a non-linear
+        gradient (e.g. a radial gradient).
+        """
+        if self.type != MSO_FILL.GRADIENT:
+            raise TypeError("Fill is not of type MSO_FILL_TYPE.GRADIENT")
+        return self._fill.gradient_angle
+
+    @gradient_angle.setter
+    def gradient_angle(self, value):
+        if self.type != MSO_FILL.GRADIENT:
+            raise TypeError("Fill is not of type MSO_FILL_TYPE.GRADIENT")
+        self._fill.gradient_angle = value
+
+    @property
+    def gradient_stops(self):
+        """|GradientStops| object providing access to stops of this gradient.
+
+        Raises |TypeError| when fill is not gradient (call `fill.gradient()`
+        first). Each stop represents a color between which the gradient
+        smoothly transitions.
+        """
+        if self.type != MSO_FILL.GRADIENT:
+            raise TypeError("Fill is not of type MSO_FILL_TYPE.GRADIENT")
+        return self._fill.gradient_stops
+
+    @property
+    def pattern(self):
+        """Return member of :ref:`MsoPatternType` indicating fill pattern.
+
+        Raises |TypeError| when fill is not patterned (call
+        `fill.patterned()` first). Returns |None| if no pattern has been set;
+        PowerPoint may display the default `PERCENT_5` pattern in this case.
+        Assigning |None| will remove any explicit pattern setting, although
+        relying on the default behavior is discouraged and may produce
+        rendering differences across client applications.
+        """
+        return self._fill.pattern
+
+    @pattern.setter
+    def pattern(self, pattern_type):
+        self._fill.pattern = pattern_type
+
+    def patterned(self):
+        """Selects the pattern fill type.
+
+        Note that calling this method does not by itself set a foreground or
+        background color of the pattern. Rather it enables subsequent
+        assignments to properties like fore_color to set the pattern and
+        colors.
+        """
+        pattFill = self._xPr.get_or_change_to_pattFill()
+        self._fill = _PattFill(pattFill)
+
+    def solid(self):
+        """
+        Sets the fill type to solid, i.e. a solid color. Note that calling
+        this method does not set a color or by itself cause the shape to
+        appear with a solid color fill; rather it enables subsequent
+        assignments to properties like fore_color to set the color.
+        """
+        solidFill = self._xPr.get_or_change_to_solidFill()
+        self._fill = _SolidFill(solidFill)
+
+    @property
+    def type(self) -> MSO_FILL_TYPE:
+        """The type of this fill, e.g. `MSO_FILL_TYPE.SOLID`."""
+        return self._fill.type
+
+
+class _Fill(object):
+    """
+    Object factory for fill object of class matching fill element, such as
+    _SolidFill for ``<a:solidFill>``; also serves as the base class for all
+    fill classes
+    """
+
+    def __new__(cls, xFill):
+        if xFill is None:
+            fill_cls = _NoneFill
+        elif isinstance(xFill, CT_BlipFillProperties):
+            fill_cls = _BlipFill
+        elif isinstance(xFill, CT_GradientFillProperties):
+            fill_cls = _GradFill
+        elif isinstance(xFill, CT_GroupFillProperties):
+            fill_cls = _GrpFill
+        elif isinstance(xFill, CT_NoFillProperties):
+            fill_cls = _NoFill
+        elif isinstance(xFill, CT_PatternFillProperties):
+            fill_cls = _PattFill
+        elif isinstance(xFill, CT_SolidColorFillProperties):
+            fill_cls = _SolidFill
+        else:
+            fill_cls = _Fill
+        return super(_Fill, cls).__new__(fill_cls)
+
+    @property
+    def back_color(self):
+        """Raise TypeError for types that do not override this property."""
+        tmpl = "fill type %s has no background color, call .patterned() first"
+        raise TypeError(tmpl % self.__class__.__name__)
+
+    @property
+    def fore_color(self):
+        """Raise TypeError for types that do not override this property."""
+        tmpl = "fill type %s has no foreground color, call .solid() or .pattern" "ed() first"
+        raise TypeError(tmpl % self.__class__.__name__)
+
+    @property
+    def pattern(self):
+        """Raise TypeError for fills that do not override this property."""
+        tmpl = "fill type %s has no pattern, call .patterned() first"
+        raise TypeError(tmpl % self.__class__.__name__)
+
+    @property
+    def type(self) -> MSO_FILL_TYPE:  # pragma: no cover
+        raise NotImplementedError(
+            f".type property must be implemented on {self.__class__.__name__}"
+        )
+
+
+class _BlipFill(_Fill):
+    @property
+    def type(self):
+        return MSO_FILL.PICTURE
+
+
+class _GradFill(_Fill):
+    """Proxies an `a:gradFill` element."""
+
+    def __init__(self, gradFill):
+        self._element = self._gradFill = gradFill
+
+    @property
+    def gradient_angle(self):
+        """Angle in float degrees of line of a linear gradient.
+
+        Read/Write. May be |None|, indicating the angle is inherited from the
+        style hierarchy. An angle of 0.0 corresponds to a left-to-right
+        gradient. Increasing angles represent clockwise rotation of the line,
+        for example 90.0 represents a top-to-bottom gradient. Raises
+        |TypeError| when the fill type is not MSO_FILL_TYPE.GRADIENT. Raises
+        |ValueError| for a non-linear gradient (e.g. a radial gradient).
+        """
+        # ---case 1: gradient path is explicit, but not linear---
+        path = self._gradFill.path
+        if path is not None:
+            raise ValueError("not a linear gradient")
+
+        # ---case 2: gradient path is inherited (no a:lin OR a:path)---
+        lin = self._gradFill.lin
+        if lin is None:
+            return None
+
+        # ---case 3: gradient path is explicitly linear---
+        # angle is stored in XML as a clockwise angle, whereas the UI
+        # reports it as counter-clockwise from horizontal-pointing-right.
+        # Since the UI is consistent with trigonometry conventions, we
+        # respect that in the API.
+        clockwise_angle = lin.ang
+        counter_clockwise_angle = 0.0 if clockwise_angle == 0.0 else (360.0 - clockwise_angle)
+        return counter_clockwise_angle
+
+    @gradient_angle.setter
+    def gradient_angle(self, value):
+        lin = self._gradFill.lin
+        if lin is None:
+            raise ValueError("not a linear gradient")
+        lin.ang = 360.0 - value
+
+    @lazyproperty
+    def gradient_stops(self):
+        """|_GradientStops| object providing access to gradient colors.
+
+        Each stop represents a color between which the gradient smoothly
+        transitions.
+        """
+        return _GradientStops(self._gradFill.get_or_add_gsLst())
+
+    @property
+    def type(self):
+        return MSO_FILL.GRADIENT
+
+
+class _GrpFill(_Fill):
+    @property
+    def type(self):
+        return MSO_FILL.GROUP
+
+
+class _NoFill(_Fill):
+    @property
+    def type(self):
+        return MSO_FILL.BACKGROUND
+
+
+class _NoneFill(_Fill):
+    @property
+    def type(self):
+        return None
+
+
+class _PattFill(_Fill):
+    """Provides access to patterned fill properties."""
+
+    def __init__(self, pattFill):
+        super(_PattFill, self).__init__()
+        self._element = self._pattFill = pattFill
+
+    @lazyproperty
+    def back_color(self):
+        """Return |ColorFormat| object that controls background color."""
+        bgClr = self._pattFill.get_or_add_bgClr()
+        return ColorFormat.from_colorchoice_parent(bgClr)
+
+    @lazyproperty
+    def fore_color(self):
+        """Return |ColorFormat| object that controls foreground color."""
+        fgClr = self._pattFill.get_or_add_fgClr()
+        return ColorFormat.from_colorchoice_parent(fgClr)
+
+    @property
+    def pattern(self):
+        """Return member of :ref:`MsoPatternType` indicating fill pattern.
+
+        Returns |None| if no pattern has been set; PowerPoint may display the
+        default `PERCENT_5` pattern in this case. Assigning |None| will
+        remove any explicit pattern setting.
+        """
+        return self._pattFill.prst
+
+    @pattern.setter
+    def pattern(self, pattern_type):
+        self._pattFill.prst = pattern_type
+
+    @property
+    def type(self):
+        return MSO_FILL.PATTERNED
+
+
+class _SolidFill(_Fill):
+    """Provides access to fill properties such as color for solid fills."""
+
+    def __init__(self, solidFill):
+        super(_SolidFill, self).__init__()
+        self._solidFill = solidFill
+
+    @lazyproperty
+    def fore_color(self):
+        """Return |ColorFormat| object controlling fill color."""
+        return ColorFormat.from_colorchoice_parent(self._solidFill)
+
+    @property
+    def type(self):
+        return MSO_FILL.SOLID
+
+
+class _GradientStops(Sequence):
+    """Collection of |GradientStop| objects defining gradient colors.
+
+    A gradient must have a minimum of two stops, but can have as many more
+    than that as required to achieve the desired effect (three is perhaps
+    most common). Stops are sequenced in the order they are transitioned
+    through.
+    """
+
+    def __init__(self, gsLst):
+        self._gsLst = gsLst
+
+    def __getitem__(self, idx):
+        return _GradientStop(self._gsLst[idx])
+
+    def __len__(self):
+        return len(self._gsLst)
+
+
+class _GradientStop(ElementProxy):
+    """A single gradient stop.
+
+    A gradient stop defines a color and a position.
+    """
+
+    def __init__(self, gs):
+        super(_GradientStop, self).__init__(gs)
+        self._gs = gs
+
+    @lazyproperty
+    def color(self):
+        """Return |ColorFormat| object controlling stop color."""
+        return ColorFormat.from_colorchoice_parent(self._gs)
+
+    @property
+    def position(self):
+        """Location of stop in gradient path as float between 0.0 and 1.0.
+
+        The value represents a percentage, where 0.0 (0%) represents the
+        start of the path and 1.0 (100%) represents the end of the path. For
+        a linear gradient, these would represent opposing extents of the
+        filled area.
+        """
+        return self._gs.pos
+
+    @position.setter
+    def position(self, value):
+        self._gs.pos = float(value)
diff --git a/.venv/lib/python3.12/site-packages/pptx/dml/line.py b/.venv/lib/python3.12/site-packages/pptx/dml/line.py
new file mode 100644
index 00000000..82be47a4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/dml/line.py
@@ -0,0 +1,100 @@
+"""DrawingML objects related to line formatting."""
+
+from __future__ import annotations
+
+from pptx.dml.fill import FillFormat
+from pptx.enum.dml import MSO_FILL
+from pptx.util import Emu, lazyproperty
+
+
+class LineFormat(object):
+    """Provides access to line properties such as color, style, and width.
+
+    A LineFormat object is typically accessed via the ``.line`` property of
+    a shape such as |Shape| or |Picture|.
+    """
+
+    def __init__(self, parent):
+        super(LineFormat, self).__init__()
+        self._parent = parent
+
+    @lazyproperty
+    def color(self):
+        """
+        The |ColorFormat| instance that provides access to the color settings
+        for this line. Essentially a shortcut for ``line.fill.fore_color``.
+        As a side-effect, accessing this property causes the line fill type
+        to be set to ``MSO_FILL.SOLID``. If this sounds risky for your use
+        case, use ``line.fill.type`` to non-destructively discover the
+        existing fill type.
+        """
+        if self.fill.type != MSO_FILL.SOLID:
+            self.fill.solid()
+        return self.fill.fore_color
+
+    @property
+    def dash_style(self):
+        """Return value indicating line style.
+
+        Returns a member of :ref:`MsoLineDashStyle` indicating line style, or
+        |None| if no explicit value has been set. When no explicit value has
+        been set, the line dash style is inherited from the style hierarchy.
+
+        Assigning |None| removes any existing explicitly-defined dash style.
+        """
+        ln = self._ln
+        if ln is None:
+            return None
+        return ln.prstDash_val
+
+    @dash_style.setter
+    def dash_style(self, dash_style):
+        if dash_style is None:
+            ln = self._ln
+            if ln is None:
+                return
+            ln._remove_prstDash()
+            ln._remove_custDash()
+            return
+        ln = self._get_or_add_ln()
+        ln.prstDash_val = dash_style
+
+    @lazyproperty
+    def fill(self):
+        """
+        |FillFormat| instance for this line, providing access to fill
+        properties such as foreground color.
+        """
+        ln = self._get_or_add_ln()
+        return FillFormat.from_fill_parent(ln)
+
+    @property
+    def width(self):
+        """
+        The width of the line expressed as an integer number of :ref:`English
+        Metric Units <EMU>`. The returned value is an instance of |Length|,
+        a value class having properties such as `.inches`, `.cm`, and `.pt`
+        for converting the value into convenient units.
+        """
+        ln = self._ln
+        if ln is None:
+            return Emu(0)
+        return ln.w
+
+    @width.setter
+    def width(self, emu):
+        if emu is None:
+            emu = 0
+        ln = self._get_or_add_ln()
+        ln.w = emu
+
+    def _get_or_add_ln(self):
+        """
+        Return the ``<a:ln>`` element containing the line format properties
+        in the XML.
+        """
+        return self._parent.get_or_add_ln()
+
+    @property
+    def _ln(self):
+        return self._parent.ln
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/__init__.py b/.venv/lib/python3.12/site-packages/pptx/enum/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/action.py b/.venv/lib/python3.12/site-packages/pptx/enum/action.py
new file mode 100644
index 00000000..bc447226
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/action.py
@@ -0,0 +1,71 @@
+"""Enumerations that describe click-action settings."""
+
+from __future__ import annotations
+
+from pptx.enum.base import BaseEnum
+
+
+class PP_ACTION_TYPE(BaseEnum):
+    """
+    Specifies the type of a mouse action (click or hover action).
+
+    Alias: ``PP_ACTION``
+
+    Example::
+
+        from pptx.enum.action import PP_ACTION
+
+        assert shape.click_action.action == PP_ACTION.HYPERLINK
+
+    MS API name: `PpActionType`
+
+    https://msdn.microsoft.com/EN-US/library/office/ff744895.aspx
+    """
+
+    END_SHOW = (6, "Slide show ends.")
+    """Slide show ends."""
+
+    FIRST_SLIDE = (3, "Returns to the first slide.")
+    """Returns to the first slide."""
+
+    HYPERLINK = (7, "Hyperlink.")
+    """Hyperlink."""
+
+    LAST_SLIDE = (4, "Moves to the last slide.")
+    """Moves to the last slide."""
+
+    LAST_SLIDE_VIEWED = (5, "Moves to the last slide viewed.")
+    """Moves to the last slide viewed."""
+
+    NAMED_SLIDE = (101, "Moves to slide specified by slide number.")
+    """Moves to slide specified by slide number."""
+
+    NAMED_SLIDE_SHOW = (10, "Runs the slideshow.")
+    """Runs the slideshow."""
+
+    NEXT_SLIDE = (1, "Moves to the next slide.")
+    """Moves to the next slide."""
+
+    NONE = (0, "No action is performed.")
+    """No action is performed."""
+
+    OPEN_FILE = (102, "Opens the specified file.")
+    """Opens the specified file."""
+
+    OLE_VERB = (11, "OLE Verb.")
+    """OLE Verb."""
+
+    PLAY = (12, "Begins the slideshow.")
+    """Begins the slideshow."""
+
+    PREVIOUS_SLIDE = (2, "Moves to the previous slide.")
+    """Moves to the previous slide."""
+
+    RUN_MACRO = (8, "Runs a macro.")
+    """Runs a macro."""
+
+    RUN_PROGRAM = (9, "Runs a program.")
+    """Runs a program."""
+
+
+PP_ACTION = PP_ACTION_TYPE
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/base.py b/.venv/lib/python3.12/site-packages/pptx/enum/base.py
new file mode 100644
index 00000000..1d49b9c1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/base.py
@@ -0,0 +1,175 @@
+"""Base classes and other objects used by enumerations."""
+
+from __future__ import annotations
+
+import enum
+import textwrap
+from typing import TYPE_CHECKING, Any, Type, TypeVar
+
+if TYPE_CHECKING:
+    from typing_extensions import Self
+
+_T = TypeVar("_T", bound="BaseXmlEnum")
+
+
+class BaseEnum(int, enum.Enum):
+    """Base class for Enums that do not map XML attr values.
+
+    The enum's value will be an integer, corresponding to the integer assigned the
+    corresponding member in the MS API enum of the same name.
+    """
+
+    def __new__(cls, ms_api_value: int, docstr: str):
+        self = int.__new__(cls, ms_api_value)
+        self._value_ = ms_api_value
+        self.__doc__ = docstr.strip()
+        return self
+
+    def __str__(self):
+        """The symbolic name and string value of this member, e.g. 'MIDDLE (3)'."""
+        return f"{self.name} ({self.value})"
+
+
+class BaseXmlEnum(int, enum.Enum):
+    """Base class for Enums that also map XML attr values.
+
+    The enum's value will be an integer, corresponding to the integer assigned the
+    corresponding member in the MS API enum of the same name.
+    """
+
+    xml_value: str | None
+
+    def __new__(cls, ms_api_value: int, xml_value: str | None, docstr: str):
+        self = int.__new__(cls, ms_api_value)
+        self._value_ = ms_api_value
+        self.xml_value = xml_value
+        self.__doc__ = docstr.strip()
+        return self
+
+    def __str__(self):
+        """The symbolic name and string value of this member, e.g. 'MIDDLE (3)'."""
+        return f"{self.name} ({self.value})"
+
+    @classmethod
+    def from_xml(cls, xml_value: str) -> Self:
+        """Enumeration member corresponding to XML attribute value `xml_value`.
+
+        Raises `ValueError` if `xml_value` is the empty string ("") or is not an XML attribute
+        value registered on the enumeration. Note that enum members that do not correspond to one
+        of the defined values for an XML attribute have `xml_value == ""`. These
+        "return-value only" members cannot be automatically mapped from an XML attribute value and
+        must be selected explicitly by code, based on the appropriate conditions.
+
+        Example::
+
+            >>> WD_PARAGRAPH_ALIGNMENT.from_xml("center")
+            WD_PARAGRAPH_ALIGNMENT.CENTER
+
+        """
+        # -- the empty string never maps to a member --
+        member = (
+            next((member for member in cls if member.xml_value == xml_value), None)
+            if xml_value
+            else None
+        )
+
+        if member is None:
+            raise ValueError(f"{cls.__name__} has no XML mapping for {repr(xml_value)}")
+
+        return member
+
+    @classmethod
+    def to_xml(cls: Type[_T], value: int | _T) -> str:
+        """XML value of this enum member, generally an XML attribute value."""
+        # -- presence of multi-arg `__new__()` method fools type-checker, but getting a
+        # -- member by its value using EnumCls(val) works as usual.
+        member = cls(value)
+        xml_value = member.xml_value
+        if not xml_value:
+            raise ValueError(f"{cls.__name__}.{member.name} has no XML representation")
+        return xml_value
+
+    @classmethod
+    def validate(cls: Type[_T], value: _T):
+        """Raise |ValueError| if `value` is not an assignable value."""
+        if value not in cls:
+            raise ValueError(f"{value} not a member of {cls.__name__} enumeration")
+
+
+class DocsPageFormatter(object):
+    """Formats a reStructuredText documention page (string) for an enumeration."""
+
+    def __init__(self, clsname: str, clsdict: dict[str, Any]):
+        self._clsname = clsname
+        self._clsdict = clsdict
+
+    @property
+    def page_str(self):
+        """
+        The RestructuredText documentation page for the enumeration. This is
+        the only API member for the class.
+        """
+        tmpl = ".. _%s:\n\n%s\n\n%s\n\n----\n\n%s"
+        components = (
+            self._ms_name,
+            self._page_title,
+            self._intro_text,
+            self._member_defs,
+        )
+        return tmpl % components
+
+    @property
+    def _intro_text(self):
+        """
+        The docstring of the enumeration, formatted for use at the top of the
+        documentation page
+        """
+        try:
+            cls_docstring = self._clsdict["__doc__"]
+        except KeyError:
+            cls_docstring = ""
+
+        if cls_docstring is None:
+            return ""
+
+        return textwrap.dedent(cls_docstring).strip()
+
+    def _member_def(self, member: BaseEnum | BaseXmlEnum):
+        """Return an individual member definition formatted as an RST glossary entry.
+
+        Output is wrapped to fit within 78 columns.
+        """
+        member_docstring = textwrap.dedent(member.__doc__ or "").strip()
+        member_docstring = textwrap.fill(
+            member_docstring,
+            width=78,
+            initial_indent=" " * 4,
+            subsequent_indent=" " * 4,
+        )
+        return "%s\n%s\n" % (member.name, member_docstring)
+
+    @property
+    def _member_defs(self):
+        """
+        A single string containing the aggregated member definitions section
+        of the documentation page
+        """
+        members = self._clsdict["__members__"]
+        member_defs = [self._member_def(member) for member in members if member.name is not None]
+        return "\n".join(member_defs)
+
+    @property
+    def _ms_name(self):
+        """
+        The Microsoft API name for this enumeration
+        """
+        return self._clsdict["__ms_name__"]
+
+    @property
+    def _page_title(self):
+        """
+        The title for the documentation page, formatted as code (surrounded
+        in double-backtics) and underlined with '=' characters
+        """
+        title_underscore = "=" * (len(self._clsname) + 4)
+        return "``%s``\n%s" % (self._clsname, title_underscore)
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/chart.py b/.venv/lib/python3.12/site-packages/pptx/enum/chart.py
new file mode 100644
index 00000000..2599cf4d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/chart.py
@@ -0,0 +1,492 @@
+"""Enumerations used by charts and related objects."""
+
+from __future__ import annotations
+
+from pptx.enum.base import BaseEnum, BaseXmlEnum
+
+
+class XL_AXIS_CROSSES(BaseXmlEnum):
+    """Specifies the point on an axis where the other axis crosses.
+
+    Example::
+
+        from pptx.enum.chart import XL_AXIS_CROSSES
+
+        value_axis.crosses = XL_AXIS_CROSSES.MAXIMUM
+
+    MS API Name: `XlAxisCrosses`
+
+    https://msdn.microsoft.com/en-us/library/office/ff745402.aspx
+    """
+
+    AUTOMATIC = (-4105, "autoZero", "The axis crossing point is set automatically, often at zero.")
+    """The axis crossing point is set automatically, often at zero."""
+
+    CUSTOM = (-4114, "", "The .crosses_at property specifies the axis crossing point.")
+    """The .crosses_at property specifies the axis crossing point."""
+
+    MAXIMUM = (2, "max", "The axis crosses at the maximum value.")
+    """The axis crosses at the maximum value."""
+
+    MINIMUM = (4, "min", "The axis crosses at the minimum value.")
+    """The axis crosses at the minimum value."""
+
+
+class XL_CATEGORY_TYPE(BaseEnum):
+    """Specifies the type of the category axis.
+
+    Example::
+
+        from pptx.enum.chart import XL_CATEGORY_TYPE
+
+        date_axis = chart.category_axis
+        assert date_axis.category_type == XL_CATEGORY_TYPE.TIME_SCALE
+
+    MS API Name: `XlCategoryType`
+
+    https://msdn.microsoft.com/EN-US/library/office/ff746136.aspx
+    """
+
+    AUTOMATIC_SCALE = (-4105, "The application controls the axis type.")
+    """The application controls the axis type."""
+
+    CATEGORY_SCALE = (2, "Axis groups data by an arbitrary set of categories")
+    """Axis groups data by an arbitrary set of categories"""
+
+    TIME_SCALE = (3, "Axis groups data on a time scale of days, months, or years.")
+    """Axis groups data on a time scale of days, months, or years."""
+
+
+class XL_CHART_TYPE(BaseEnum):
+    """Specifies the type of a chart.
+
+    Example::
+
+        from pptx.enum.chart import XL_CHART_TYPE
+
+        assert chart.chart_type == XL_CHART_TYPE.BAR_STACKED
+
+    MS API Name: `XlChartType`
+
+    http://msdn.microsoft.com/en-us/library/office/ff838409.aspx
+    """
+
+    THREE_D_AREA = (-4098, "3D Area.")
+    """3D Area."""
+
+    THREE_D_AREA_STACKED = (78, "3D Stacked Area.")
+    """3D Stacked Area."""
+
+    THREE_D_AREA_STACKED_100 = (79, "100% Stacked Area.")
+    """100% Stacked Area."""
+
+    THREE_D_BAR_CLUSTERED = (60, "3D Clustered Bar.")
+    """3D Clustered Bar."""
+
+    THREE_D_BAR_STACKED = (61, "3D Stacked Bar.")
+    """3D Stacked Bar."""
+
+    THREE_D_BAR_STACKED_100 = (62, "3D 100% Stacked Bar.")
+    """3D 100% Stacked Bar."""
+
+    THREE_D_COLUMN = (-4100, "3D Column.")
+    """3D Column."""
+
+    THREE_D_COLUMN_CLUSTERED = (54, "3D Clustered Column.")
+    """3D Clustered Column."""
+
+    THREE_D_COLUMN_STACKED = (55, "3D Stacked Column.")
+    """3D Stacked Column."""
+
+    THREE_D_COLUMN_STACKED_100 = (56, "3D 100% Stacked Column.")
+    """3D 100% Stacked Column."""
+
+    THREE_D_LINE = (-4101, "3D Line.")
+    """3D Line."""
+
+    THREE_D_PIE = (-4102, "3D Pie.")
+    """3D Pie."""
+
+    THREE_D_PIE_EXPLODED = (70, "Exploded 3D Pie.")
+    """Exploded 3D Pie."""
+
+    AREA = (1, "Area")
+    """Area"""
+
+    AREA_STACKED = (76, "Stacked Area.")
+    """Stacked Area."""
+
+    AREA_STACKED_100 = (77, "100% Stacked Area.")
+    """100% Stacked Area."""
+
+    BAR_CLUSTERED = (57, "Clustered Bar.")
+    """Clustered Bar."""
+
+    BAR_OF_PIE = (71, "Bar of Pie.")
+    """Bar of Pie."""
+
+    BAR_STACKED = (58, "Stacked Bar.")
+    """Stacked Bar."""
+
+    BAR_STACKED_100 = (59, "100% Stacked Bar.")
+    """100% Stacked Bar."""
+
+    BUBBLE = (15, "Bubble.")
+    """Bubble."""
+
+    BUBBLE_THREE_D_EFFECT = (87, "Bubble with 3D effects.")
+    """Bubble with 3D effects."""
+
+    COLUMN_CLUSTERED = (51, "Clustered Column.")
+    """Clustered Column."""
+
+    COLUMN_STACKED = (52, "Stacked Column.")
+    """Stacked Column."""
+
+    COLUMN_STACKED_100 = (53, "100% Stacked Column.")
+    """100% Stacked Column."""
+
+    CONE_BAR_CLUSTERED = (102, "Clustered Cone Bar.")
+    """Clustered Cone Bar."""
+
+    CONE_BAR_STACKED = (103, "Stacked Cone Bar.")
+    """Stacked Cone Bar."""
+
+    CONE_BAR_STACKED_100 = (104, "100% Stacked Cone Bar.")
+    """100% Stacked Cone Bar."""
+
+    CONE_COL = (105, "3D Cone Column.")
+    """3D Cone Column."""
+
+    CONE_COL_CLUSTERED = (99, "Clustered Cone Column.")
+    """Clustered Cone Column."""
+
+    CONE_COL_STACKED = (100, "Stacked Cone Column.")
+    """Stacked Cone Column."""
+
+    CONE_COL_STACKED_100 = (101, "100% Stacked Cone Column.")
+    """100% Stacked Cone Column."""
+
+    CYLINDER_BAR_CLUSTERED = (95, "Clustered Cylinder Bar.")
+    """Clustered Cylinder Bar."""
+
+    CYLINDER_BAR_STACKED = (96, "Stacked Cylinder Bar.")
+    """Stacked Cylinder Bar."""
+
+    CYLINDER_BAR_STACKED_100 = (97, "100% Stacked Cylinder Bar.")
+    """100% Stacked Cylinder Bar."""
+
+    CYLINDER_COL = (98, "3D Cylinder Column.")
+    """3D Cylinder Column."""
+
+    CYLINDER_COL_CLUSTERED = (92, "Clustered Cone Column.")
+    """Clustered Cone Column."""
+
+    CYLINDER_COL_STACKED = (93, "Stacked Cone Column.")
+    """Stacked Cone Column."""
+
+    CYLINDER_COL_STACKED_100 = (94, "100% Stacked Cylinder Column.")
+    """100% Stacked Cylinder Column."""
+
+    DOUGHNUT = (-4120, "Doughnut.")
+    """Doughnut."""
+
+    DOUGHNUT_EXPLODED = (80, "Exploded Doughnut.")
+    """Exploded Doughnut."""
+
+    LINE = (4, "Line.")
+    """Line."""
+
+    LINE_MARKERS = (65, "Line with Markers.")
+    """Line with Markers."""
+
+    LINE_MARKERS_STACKED = (66, "Stacked Line with Markers.")
+    """Stacked Line with Markers."""
+
+    LINE_MARKERS_STACKED_100 = (67, "100% Stacked Line with Markers.")
+    """100% Stacked Line with Markers."""
+
+    LINE_STACKED = (63, "Stacked Line.")
+    """Stacked Line."""
+
+    LINE_STACKED_100 = (64, "100% Stacked Line.")
+    """100% Stacked Line."""
+
+    PIE = (5, "Pie.")
+    """Pie."""
+
+    PIE_EXPLODED = (69, "Exploded Pie.")
+    """Exploded Pie."""
+
+    PIE_OF_PIE = (68, "Pie of Pie.")
+    """Pie of Pie."""
+
+    PYRAMID_BAR_CLUSTERED = (109, "Clustered Pyramid Bar.")
+    """Clustered Pyramid Bar."""
+
+    PYRAMID_BAR_STACKED = (110, "Stacked Pyramid Bar.")
+    """Stacked Pyramid Bar."""
+
+    PYRAMID_BAR_STACKED_100 = (111, "100% Stacked Pyramid Bar.")
+    """100% Stacked Pyramid Bar."""
+
+    PYRAMID_COL = (112, "3D Pyramid Column.")
+    """3D Pyramid Column."""
+
+    PYRAMID_COL_CLUSTERED = (106, "Clustered Pyramid Column.")
+    """Clustered Pyramid Column."""
+
+    PYRAMID_COL_STACKED = (107, "Stacked Pyramid Column.")
+    """Stacked Pyramid Column."""
+
+    PYRAMID_COL_STACKED_100 = (108, "100% Stacked Pyramid Column.")
+    """100% Stacked Pyramid Column."""
+
+    RADAR = (-4151, "Radar.")
+    """Radar."""
+
+    RADAR_FILLED = (82, "Filled Radar.")
+    """Filled Radar."""
+
+    RADAR_MARKERS = (81, "Radar with Data Markers.")
+    """Radar with Data Markers."""
+
+    STOCK_HLC = (88, "High-Low-Close.")
+    """High-Low-Close."""
+
+    STOCK_OHLC = (89, "Open-High-Low-Close.")
+    """Open-High-Low-Close."""
+
+    STOCK_VHLC = (90, "Volume-High-Low-Close.")
+    """Volume-High-Low-Close."""
+
+    STOCK_VOHLC = (91, "Volume-Open-High-Low-Close.")
+    """Volume-Open-High-Low-Close."""
+
+    SURFACE = (83, "3D Surface.")
+    """3D Surface."""
+
+    SURFACE_TOP_VIEW = (85, "Surface (Top View).")
+    """Surface (Top View)."""
+
+    SURFACE_TOP_VIEW_WIREFRAME = (86, "Surface (Top View wireframe).")
+    """Surface (Top View wireframe)."""
+
+    SURFACE_WIREFRAME = (84, "3D Surface (wireframe).")
+    """3D Surface (wireframe)."""
+
+    XY_SCATTER = (-4169, "Scatter.")
+    """Scatter."""
+
+    XY_SCATTER_LINES = (74, "Scatter with Lines.")
+    """Scatter with Lines."""
+
+    XY_SCATTER_LINES_NO_MARKERS = (75, "Scatter with Lines and No Data Markers.")
+    """Scatter with Lines and No Data Markers."""
+
+    XY_SCATTER_SMOOTH = (72, "Scatter with Smoothed Lines.")
+    """Scatter with Smoothed Lines."""
+
+    XY_SCATTER_SMOOTH_NO_MARKERS = (73, "Scatter with Smoothed Lines and No Data Markers.")
+    """Scatter with Smoothed Lines and No Data Markers."""
+
+
+class XL_DATA_LABEL_POSITION(BaseXmlEnum):
+    """Specifies where the data label is positioned.
+
+    Example::
+
+        from pptx.enum.chart import XL_LABEL_POSITION
+
+        data_labels = chart.plots[0].data_labels
+        data_labels.position = XL_LABEL_POSITION.OUTSIDE_END
+
+    MS API Name: `XlDataLabelPosition`
+
+    http://msdn.microsoft.com/en-us/library/office/ff745082.aspx
+    """
+
+    ABOVE = (0, "t", "The data label is positioned above the data point.")
+    """The data label is positioned above the data point."""
+
+    BELOW = (1, "b", "The data label is positioned below the data point.")
+    """The data label is positioned below the data point."""
+
+    BEST_FIT = (5, "bestFit", "Word sets the position of the data label.")
+    """Word sets the position of the data label."""
+
+    CENTER = (
+        -4108,
+        "ctr",
+        "The data label is centered on the data point or inside a bar or a pie slice.",
+    )
+    """The data label is centered on the data point or inside a bar or a pie slice."""
+
+    INSIDE_BASE = (
+        4,
+        "inBase",
+        "The data label is positioned inside the data point at the bottom edge.",
+    )
+    """The data label is positioned inside the data point at the bottom edge."""
+
+    INSIDE_END = (3, "inEnd", "The data label is positioned inside the data point at the top edge.")
+    """The data label is positioned inside the data point at the top edge."""
+
+    LEFT = (-4131, "l", "The data label is positioned to the left of the data point.")
+    """The data label is positioned to the left of the data point."""
+
+    MIXED = (6, "", "Data labels are in multiple positions (read-only).")
+    """Data labels are in multiple positions (read-only)."""
+
+    OUTSIDE_END = (
+        2,
+        "outEnd",
+        "The data label is positioned outside the data point at the top edge.",
+    )
+    """The data label is positioned outside the data point at the top edge."""
+
+    RIGHT = (-4152, "r", "The data label is positioned to the right of the data point.")
+    """The data label is positioned to the right of the data point."""
+
+
+XL_LABEL_POSITION = XL_DATA_LABEL_POSITION
+
+
+class XL_LEGEND_POSITION(BaseXmlEnum):
+    """Specifies the position of the legend on a chart.
+
+    Example::
+
+        from pptx.enum.chart import XL_LEGEND_POSITION
+
+        chart.has_legend = True
+        chart.legend.position = XL_LEGEND_POSITION.BOTTOM
+
+    MS API Name: `XlLegendPosition`
+
+    http://msdn.microsoft.com/en-us/library/office/ff745840.aspx
+    """
+
+    BOTTOM = (-4107, "b", "Below the chart.")
+    """Below the chart."""
+
+    CORNER = (2, "tr", "In the upper-right corner of the chart border.")
+    """In the upper-right corner of the chart border."""
+
+    CUSTOM = (-4161, "", "A custom position (read-only).")
+    """A custom position (read-only)."""
+
+    LEFT = (-4131, "l", "Left of the chart.")
+    """Left of the chart."""
+
+    RIGHT = (-4152, "r", "Right of the chart.")
+    """Right of the chart."""
+
+    TOP = (-4160, "t", "Above the chart.")
+    """Above the chart."""
+
+
+class XL_MARKER_STYLE(BaseXmlEnum):
+    """Specifies the marker style for a point or series in a line, scatter, or radar chart.
+
+    Example::
+
+        from pptx.enum.chart import XL_MARKER_STYLE
+
+        series.marker.style = XL_MARKER_STYLE.CIRCLE
+
+    MS API Name: `XlMarkerStyle`
+
+    http://msdn.microsoft.com/en-us/library/office/ff197219.aspx
+    """
+
+    AUTOMATIC = (-4105, "auto", "Automatic markers")
+    """Automatic markers"""
+
+    CIRCLE = (8, "circle", "Circular markers")
+    """Circular markers"""
+
+    DASH = (-4115, "dash", "Long bar markers")
+    """Long bar markers"""
+
+    DIAMOND = (2, "diamond", "Diamond-shaped markers")
+    """Diamond-shaped markers"""
+
+    DOT = (-4118, "dot", "Short bar markers")
+    """Short bar markers"""
+
+    NONE = (-4142, "none", "No markers")
+    """No markers"""
+
+    PICTURE = (-4147, "picture", "Picture markers")
+    """Picture markers"""
+
+    PLUS = (9, "plus", "Square markers with a plus sign")
+    """Square markers with a plus sign"""
+
+    SQUARE = (1, "square", "Square markers")
+    """Square markers"""
+
+    STAR = (5, "star", "Square markers with an  asterisk")
+    """Square markers with an  asterisk"""
+
+    TRIANGLE = (3, "triangle", "Triangular markers")
+    """Triangular markers"""
+
+    X = (-4168, "x", "Square markers with an X")
+    """Square markers with an X"""
+
+
+class XL_TICK_MARK(BaseXmlEnum):
+    """Specifies a type of axis tick for a chart.
+
+    Example::
+
+        from pptx.enum.chart import XL_TICK_MARK
+
+        chart.value_axis.minor_tick_mark = XL_TICK_MARK.INSIDE
+
+    MS API Name: `XlTickMark`
+
+    http://msdn.microsoft.com/en-us/library/office/ff193878.aspx
+    """
+
+    CROSS = (4, "cross", "Tick mark crosses the axis")
+    """Tick mark crosses the axis"""
+
+    INSIDE = (2, "in", "Tick mark appears inside the axis")
+    """Tick mark appears inside the axis"""
+
+    NONE = (-4142, "none", "No tick mark")
+    """No tick mark"""
+
+    OUTSIDE = (3, "out", "Tick mark appears outside the axis")
+    """Tick mark appears outside the axis"""
+
+
+class XL_TICK_LABEL_POSITION(BaseXmlEnum):
+    """Specifies the position of tick-mark labels on a chart axis.
+
+    Example::
+
+        from pptx.enum.chart import XL_TICK_LABEL_POSITION
+
+        category_axis = chart.category_axis
+        category_axis.tick_label_position = XL_TICK_LABEL_POSITION.LOW
+
+    MS API Name: `XlTickLabelPosition`
+
+    http://msdn.microsoft.com/en-us/library/office/ff822561.aspx
+    """
+
+    HIGH = (-4127, "high", "Top or right side of the chart.")
+    """Top or right side of the chart."""
+
+    LOW = (-4134, "low", "Bottom or left side of the chart.")
+    """Bottom or left side of the chart."""
+
+    NEXT_TO_AXIS = (4, "nextTo", "Next to axis (where axis is not at either side of the chart).")
+    """Next to axis (where axis is not at either side of the chart)."""
+
+    NONE = (-4142, "none", "No tick labels.")
+    """No tick labels."""
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/dml.py b/.venv/lib/python3.12/site-packages/pptx/enum/dml.py
new file mode 100644
index 00000000..40d5c5cd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/dml.py
@@ -0,0 +1,405 @@
+"""Enumerations used by DrawingML objects."""
+
+from __future__ import annotations
+
+from pptx.enum.base import BaseEnum, BaseXmlEnum
+
+
+class MSO_COLOR_TYPE(BaseEnum):
+    """
+    Specifies the color specification scheme
+
+    Example::
+
+        from pptx.enum.dml import MSO_COLOR_TYPE
+
+        assert shape.fill.fore_color.type == MSO_COLOR_TYPE.SCHEME
+
+    MS API Name: "MsoColorType"
+
+    http://msdn.microsoft.com/en-us/library/office/ff864912(v=office.15).aspx
+    """
+
+    RGB = (1, "Color is specified by an |RGBColor| value.")
+    """Color is specified by an |RGBColor| value."""
+
+    SCHEME = (2, "Color is one of the preset theme colors")
+    """Color is one of the preset theme colors"""
+
+    HSL = (101, "Color is specified using Hue, Saturation, and Luminosity values")
+    """Color is specified using Hue, Saturation, and Luminosity values"""
+
+    PRESET = (102, "Color is specified using a named built-in color")
+    """Color is specified using a named built-in color"""
+
+    SCRGB = (103, "Color is an scRGB color, a wide color gamut RGB color space")
+    """Color is an scRGB color, a wide color gamut RGB color space"""
+
+    SYSTEM = (
+        104,
+        "Color is one specified by the operating system, such as the window background color.",
+    )
+    """Color is one specified by the operating system, such as the window background color."""
+
+
+class MSO_FILL_TYPE(BaseEnum):
+    """
+    Specifies the type of bitmap used for the fill of a shape.
+
+    Alias: ``MSO_FILL``
+
+    Example::
+
+        from pptx.enum.dml import MSO_FILL
+
+        assert shape.fill.type == MSO_FILL.SOLID
+
+    MS API Name: `MsoFillType`
+
+    http://msdn.microsoft.com/EN-US/library/office/ff861408.aspx
+    """
+
+    BACKGROUND = (
+        5,
+        "The shape is transparent, such that whatever is behind the shape shows through."
+        " Often this is the slide background, but if a visible shape is behind, that will"
+        " show through.",
+    )
+    """The shape is transparent, such that whatever is behind the shape shows through.
+
+    Often this is the slide background, but if a visible shape is behind, that will show through.
+    """
+
+    GRADIENT = (3, "Shape is filled with a gradient")
+    """Shape is filled with a gradient"""
+
+    GROUP = (101, "Shape is part of a group and should inherit the fill properties of the group.")
+    """Shape is part of a group and should inherit the fill properties of the group."""
+
+    PATTERNED = (2, "Shape is filled with a pattern")
+    """Shape is filled with a pattern"""
+
+    PICTURE = (6, "Shape is filled with a bitmapped image")
+    """Shape is filled with a bitmapped image"""
+
+    SOLID = (1, "Shape is filled with a solid color")
+    """Shape is filled with a solid color"""
+
+    TEXTURED = (4, "Shape is filled with a texture")
+    """Shape is filled with a texture"""
+
+
+MSO_FILL = MSO_FILL_TYPE
+
+
+class MSO_LINE_DASH_STYLE(BaseXmlEnum):
+    """Specifies the dash style for a line.
+
+    Alias: ``MSO_LINE``
+
+    Example::
+
+        from pptx.enum.dml import MSO_LINE
+
+        shape.line.dash_style = MSO_LINE.DASH_DOT_DOT
+
+    MS API name: `MsoLineDashStyle`
+
+    https://learn.microsoft.com/en-us/office/vba/api/Office.MsoLineDashStyle
+    """
+
+    DASH = (4, "dash", "Line consists of dashes only.")
+    """Line consists of dashes only."""
+
+    DASH_DOT = (5, "dashDot", "Line is a dash-dot pattern.")
+    """Line is a dash-dot pattern."""
+
+    DASH_DOT_DOT = (6, "lgDashDotDot", "Line is a dash-dot-dot pattern.")
+    """Line is a dash-dot-dot pattern."""
+
+    LONG_DASH = (7, "lgDash", "Line consists of long dashes.")
+    """Line consists of long dashes."""
+
+    LONG_DASH_DOT = (8, "lgDashDot", "Line is a long dash-dot pattern.")
+    """Line is a long dash-dot pattern."""
+
+    ROUND_DOT = (3, "sysDot", "Line is made up of round dots.")
+    """Line is made up of round dots."""
+
+    SOLID = (1, "solid", "Line is solid.")
+    """Line is solid."""
+
+    SQUARE_DOT = (2, "sysDash", "Line is made up of square dots.")
+    """Line is made up of square dots."""
+
+    DASH_STYLE_MIXED = (-2, "", "Not supported.")
+    """Return value only, indicating more than one dash style applies."""
+
+
+MSO_LINE = MSO_LINE_DASH_STYLE
+
+
+class MSO_PATTERN_TYPE(BaseXmlEnum):
+    """Specifies the fill pattern used in a shape.
+
+    Alias: ``MSO_PATTERN``
+
+    Example::
+
+        from pptx.enum.dml import MSO_PATTERN
+
+        fill = shape.fill
+        fill.patterned()
+        fill.pattern = MSO_PATTERN.WAVE
+
+    MS API Name: `MsoPatternType`
+
+    https://learn.microsoft.com/en-us/office/vba/api/Office.MsoPatternType
+    """
+
+    CROSS = (51, "cross", "Cross")
+    """Cross"""
+
+    DARK_DOWNWARD_DIAGONAL = (15, "dkDnDiag", "Dark Downward Diagonal")
+    """Dark Downward Diagonal"""
+
+    DARK_HORIZONTAL = (13, "dkHorz", "Dark Horizontal")
+    """Dark Horizontal"""
+
+    DARK_UPWARD_DIAGONAL = (16, "dkUpDiag", "Dark Upward Diagonal")
+    """Dark Upward Diagonal"""
+
+    DARK_VERTICAL = (14, "dkVert", "Dark Vertical")
+    """Dark Vertical"""
+
+    DASHED_DOWNWARD_DIAGONAL = (28, "dashDnDiag", "Dashed Downward Diagonal")
+    """Dashed Downward Diagonal"""
+
+    DASHED_HORIZONTAL = (32, "dashHorz", "Dashed Horizontal")
+    """Dashed Horizontal"""
+
+    DASHED_UPWARD_DIAGONAL = (27, "dashUpDiag", "Dashed Upward Diagonal")
+    """Dashed Upward Diagonal"""
+
+    DASHED_VERTICAL = (31, "dashVert", "Dashed Vertical")
+    """Dashed Vertical"""
+
+    DIAGONAL_BRICK = (40, "diagBrick", "Diagonal Brick")
+    """Diagonal Brick"""
+
+    DIAGONAL_CROSS = (54, "diagCross", "Diagonal Cross")
+    """Diagonal Cross"""
+
+    DIVOT = (46, "divot", "Pattern Divot")
+    """Pattern Divot"""
+
+    DOTTED_DIAMOND = (24, "dotDmnd", "Dotted Diamond")
+    """Dotted Diamond"""
+
+    DOTTED_GRID = (45, "dotGrid", "Dotted Grid")
+    """Dotted Grid"""
+
+    DOWNWARD_DIAGONAL = (52, "dnDiag", "Downward Diagonal")
+    """Downward Diagonal"""
+
+    HORIZONTAL = (49, "horz", "Horizontal")
+    """Horizontal"""
+
+    HORIZONTAL_BRICK = (35, "horzBrick", "Horizontal Brick")
+    """Horizontal Brick"""
+
+    LARGE_CHECKER_BOARD = (36, "lgCheck", "Large Checker Board")
+    """Large Checker Board"""
+
+    LARGE_CONFETTI = (33, "lgConfetti", "Large Confetti")
+    """Large Confetti"""
+
+    LARGE_GRID = (34, "lgGrid", "Large Grid")
+    """Large Grid"""
+
+    LIGHT_DOWNWARD_DIAGONAL = (21, "ltDnDiag", "Light Downward Diagonal")
+    """Light Downward Diagonal"""
+
+    LIGHT_HORIZONTAL = (19, "ltHorz", "Light Horizontal")
+    """Light Horizontal"""
+
+    LIGHT_UPWARD_DIAGONAL = (22, "ltUpDiag", "Light Upward Diagonal")
+    """Light Upward Diagonal"""
+
+    LIGHT_VERTICAL = (20, "ltVert", "Light Vertical")
+    """Light Vertical"""
+
+    NARROW_HORIZONTAL = (30, "narHorz", "Narrow Horizontal")
+    """Narrow Horizontal"""
+
+    NARROW_VERTICAL = (29, "narVert", "Narrow Vertical")
+    """Narrow Vertical"""
+
+    OUTLINED_DIAMOND = (41, "openDmnd", "Outlined Diamond")
+    """Outlined Diamond"""
+
+    PERCENT_10 = (2, "pct10", "10% of the foreground color.")
+    """10% of the foreground color."""
+
+    PERCENT_20 = (3, "pct20", "20% of the foreground color.")
+    """20% of the foreground color."""
+
+    PERCENT_25 = (4, "pct25", "25% of the foreground color.")
+    """25% of the foreground color."""
+
+    PERCENT_30 = (5, "pct30", "30% of the foreground color.")
+    """30% of the foreground color."""
+
+    ERCENT_40 = (6, "pct40", "40% of the foreground color.")
+    """40% of the foreground color."""
+
+    PERCENT_5 = (1, "pct5", "5% of the foreground color.")
+    """5% of the foreground color."""
+
+    PERCENT_50 = (7, "pct50", "50% of the foreground color.")
+    """50% of the foreground color."""
+
+    PERCENT_60 = (8, "pct60", "60% of the foreground color.")
+    """60% of the foreground color."""
+
+    PERCENT_70 = (9, "pct70", "70% of the foreground color.")
+    """70% of the foreground color."""
+
+    PERCENT_75 = (10, "pct75", "75% of the foreground color.")
+    """75% of the foreground color."""
+
+    PERCENT_80 = (11, "pct80", "80% of the foreground color.")
+    """80% of the foreground color."""
+
+    PERCENT_90 = (12, "pct90", "90% of the foreground color.")
+    """90% of the foreground color."""
+
+    PLAID = (42, "plaid", "Plaid")
+    """Plaid"""
+
+    SHINGLE = (47, "shingle", "Shingle")
+    """Shingle"""
+
+    SMALL_CHECKER_BOARD = (17, "smCheck", "Small Checker Board")
+    """Small Checker Board"""
+
+    SMALL_CONFETTI = (37, "smConfetti", "Small Confetti")
+    """Small Confetti"""
+
+    SMALL_GRID = (23, "smGrid", "Small Grid")
+    """Small Grid"""
+
+    SOLID_DIAMOND = (39, "solidDmnd", "Solid Diamond")
+    """Solid Diamond"""
+
+    SPHERE = (43, "sphere", "Sphere")
+    """Sphere"""
+
+    TRELLIS = (18, "trellis", "Trellis")
+    """Trellis"""
+
+    UPWARD_DIAGONAL = (53, "upDiag", "Upward Diagonal")
+    """Upward Diagonal"""
+
+    VERTICAL = (50, "vert", "Vertical")
+    """Vertical"""
+
+    WAVE = (48, "wave", "Wave")
+    """Wave"""
+
+    WEAVE = (44, "weave", "Weave")
+    """Weave"""
+
+    WIDE_DOWNWARD_DIAGONAL = (25, "wdDnDiag", "Wide Downward Diagonal")
+    """Wide Downward Diagonal"""
+
+    WIDE_UPWARD_DIAGONAL = (26, "wdUpDiag", "Wide Upward Diagonal")
+    """Wide Upward Diagonal"""
+
+    ZIG_ZAG = (38, "zigZag", "Zig Zag")
+    """Zig Zag"""
+
+    MIXED = (-2, "", "Mixed pattern (read-only).")
+    """Mixed pattern (read-only)."""
+
+
+MSO_PATTERN = MSO_PATTERN_TYPE
+
+
+class MSO_THEME_COLOR_INDEX(BaseXmlEnum):
+    """An Office theme color, one of those shown in the color gallery on the formatting ribbon.
+
+    Alias: ``MSO_THEME_COLOR``
+
+    Example::
+
+        from pptx.enum.dml import MSO_THEME_COLOR
+
+        shape.fill.solid()
+        shape.fill.fore_color.theme_color = MSO_THEME_COLOR.ACCENT_1
+
+    MS API Name: `MsoThemeColorIndex`
+
+    http://msdn.microsoft.com/en-us/library/office/ff860782(v=office.15).aspx
+    """
+
+    NOT_THEME_COLOR = (0, "", "Indicates the color is not a theme color.")
+    """Indicates the color is not a theme color."""
+
+    ACCENT_1 = (5, "accent1", "Specifies the Accent 1 theme color.")
+    """Specifies the Accent 1 theme color."""
+
+    ACCENT_2 = (6, "accent2", "Specifies the Accent 2 theme color.")
+    """Specifies the Accent 2 theme color."""
+
+    ACCENT_3 = (7, "accent3", "Specifies the Accent 3 theme color.")
+    """Specifies the Accent 3 theme color."""
+
+    ACCENT_4 = (8, "accent4", "Specifies the Accent 4 theme color.")
+    """Specifies the Accent 4 theme color."""
+
+    ACCENT_5 = (9, "accent5", "Specifies the Accent 5 theme color.")
+    """Specifies the Accent 5 theme color."""
+
+    ACCENT_6 = (10, "accent6", "Specifies the Accent 6 theme color.")
+    """Specifies the Accent 6 theme color."""
+
+    BACKGROUND_1 = (14, "bg1", "Specifies the Background 1 theme color.")
+    """Specifies the Background 1 theme color."""
+
+    BACKGROUND_2 = (16, "bg2", "Specifies the Background 2 theme color.")
+    """Specifies the Background 2 theme color."""
+
+    DARK_1 = (1, "dk1", "Specifies the Dark 1 theme color.")
+    """Specifies the Dark 1 theme color."""
+
+    DARK_2 = (3, "dk2", "Specifies the Dark 2 theme color.")
+    """Specifies the Dark 2 theme color."""
+
+    FOLLOWED_HYPERLINK = (12, "folHlink", "Specifies the theme color for a clicked hyperlink.")
+    """Specifies the theme color for a clicked hyperlink."""
+
+    HYPERLINK = (11, "hlink", "Specifies the theme color for a hyperlink.")
+    """Specifies the theme color for a hyperlink."""
+
+    LIGHT_1 = (2, "lt1", "Specifies the Light 1 theme color.")
+    """Specifies the Light 1 theme color."""
+
+    LIGHT_2 = (4, "lt2", "Specifies the Light 2 theme color.")
+    """Specifies the Light 2 theme color."""
+
+    TEXT_1 = (13, "tx1", "Specifies the Text 1 theme color.")
+    """Specifies the Text 1 theme color."""
+
+    TEXT_2 = (15, "tx2", "Specifies the Text 2 theme color.")
+    """Specifies the Text 2 theme color."""
+
+    MIXED = (
+        -2,
+        "",
+        "Indicates multiple theme colors are used, such as in a group shape (read-only).",
+    )
+    """Indicates multiple theme colors are used, such as in a group shape (read-only)."""
+
+
+MSO_THEME_COLOR = MSO_THEME_COLOR_INDEX
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/lang.py b/.venv/lib/python3.12/site-packages/pptx/enum/lang.py
new file mode 100644
index 00000000..a6bc1c8b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/lang.py
@@ -0,0 +1,685 @@
+"""Enumerations used for specifying language."""
+
+from __future__ import annotations
+
+from pptx.enum.base import BaseXmlEnum
+
+
+class MSO_LANGUAGE_ID(BaseXmlEnum):
+    """
+    Specifies the language identifier.
+
+    Example::
+
+        from pptx.enum.lang import MSO_LANGUAGE_ID
+
+        font.language_id = MSO_LANGUAGE_ID.POLISH
+
+    MS API Name: `MsoLanguageId`
+
+    https://msdn.microsoft.com/en-us/library/office/ff862134.aspx
+    """
+
+    NONE = (0, "", "No language specified.")
+    """No language specified."""
+
+    AFRIKAANS = (1078, "af-ZA", "The Afrikaans language.")
+    """The Afrikaans language."""
+
+    ALBANIAN = (1052, "sq-AL", "The Albanian language.")
+    """The Albanian language."""
+
+    AMHARIC = (1118, "am-ET", "The Amharic language.")
+    """The Amharic language."""
+
+    ARABIC = (1025, "ar-SA", "The Arabic language.")
+    """The Arabic language."""
+
+    ARABIC_ALGERIA = (5121, "ar-DZ", "The Arabic Algeria language.")
+    """The Arabic Algeria language."""
+
+    ARABIC_BAHRAIN = (15361, "ar-BH", "The Arabic Bahrain language.")
+    """The Arabic Bahrain language."""
+
+    ARABIC_EGYPT = (3073, "ar-EG", "The Arabic Egypt language.")
+    """The Arabic Egypt language."""
+
+    ARABIC_IRAQ = (2049, "ar-IQ", "The Arabic Iraq language.")
+    """The Arabic Iraq language."""
+
+    ARABIC_JORDAN = (11265, "ar-JO", "The Arabic Jordan language.")
+    """The Arabic Jordan language."""
+
+    ARABIC_KUWAIT = (13313, "ar-KW", "The Arabic Kuwait language.")
+    """The Arabic Kuwait language."""
+
+    ARABIC_LEBANON = (12289, "ar-LB", "The Arabic Lebanon language.")
+    """The Arabic Lebanon language."""
+
+    ARABIC_LIBYA = (4097, "ar-LY", "The Arabic Libya language.")
+    """The Arabic Libya language."""
+
+    ARABIC_MOROCCO = (6145, "ar-MA", "The Arabic Morocco language.")
+    """The Arabic Morocco language."""
+
+    ARABIC_OMAN = (8193, "ar-OM", "The Arabic Oman language.")
+    """The Arabic Oman language."""
+
+    ARABIC_QATAR = (16385, "ar-QA", "The Arabic Qatar language.")
+    """The Arabic Qatar language."""
+
+    ARABIC_SYRIA = (10241, "ar-SY", "The Arabic Syria language.")
+    """The Arabic Syria language."""
+
+    ARABIC_TUNISIA = (7169, "ar-TN", "The Arabic Tunisia language.")
+    """The Arabic Tunisia language."""
+
+    ARABIC_UAE = (14337, "ar-AE", "The Arabic UAE language.")
+    """The Arabic UAE language."""
+
+    ARABIC_YEMEN = (9217, "ar-YE", "The Arabic Yemen language.")
+    """The Arabic Yemen language."""
+
+    ARMENIAN = (1067, "hy-AM", "The Armenian language.")
+    """The Armenian language."""
+
+    ASSAMESE = (1101, "as-IN", "The Assamese language.")
+    """The Assamese language."""
+
+    AZERI_CYRILLIC = (2092, "az-AZ", "The Azeri Cyrillic language.")
+    """The Azeri Cyrillic language."""
+
+    AZERI_LATIN = (1068, "az-Latn-AZ", "The Azeri Latin language.")
+    """The Azeri Latin language."""
+
+    BASQUE = (1069, "eu-ES", "The Basque language.")
+    """The Basque language."""
+
+    BELGIAN_DUTCH = (2067, "nl-BE", "The Belgian Dutch language.")
+    """The Belgian Dutch language."""
+
+    BELGIAN_FRENCH = (2060, "fr-BE", "The Belgian French language.")
+    """The Belgian French language."""
+
+    BENGALI = (1093, "bn-IN", "The Bengali language.")
+    """The Bengali language."""
+
+    BOSNIAN = (4122, "hr-BA", "The Bosnian language.")
+    """The Bosnian language."""
+
+    BOSNIAN_BOSNIA_HERZEGOVINA_CYRILLIC = (
+        8218,
+        "bs-BA",
+        "The Bosnian Bosnia Herzegovina Cyrillic language.",
+    )
+    """The Bosnian Bosnia Herzegovina Cyrillic language."""
+
+    BOSNIAN_BOSNIA_HERZEGOVINA_LATIN = (
+        5146,
+        "bs-Latn-BA",
+        "The Bosnian Bosnia Herzegovina Latin language.",
+    )
+    """The Bosnian Bosnia Herzegovina Latin language."""
+
+    BRAZILIAN_PORTUGUESE = (1046, "pt-BR", "The Brazilian Portuguese language.")
+    """The Brazilian Portuguese language."""
+
+    BULGARIAN = (1026, "bg-BG", "The Bulgarian language.")
+    """The Bulgarian language."""
+
+    BURMESE = (1109, "my-MM", "The Burmese language.")
+    """The Burmese language."""
+
+    BYELORUSSIAN = (1059, "be-BY", "The Byelorussian language.")
+    """The Byelorussian language."""
+
+    CATALAN = (1027, "ca-ES", "The Catalan language.")
+    """The Catalan language."""
+
+    CHEROKEE = (1116, "chr-US", "The Cherokee language.")
+    """The Cherokee language."""
+
+    CHINESE_HONG_KONG_SAR = (3076, "zh-HK", "The Chinese Hong Kong SAR language.")
+    """The Chinese Hong Kong SAR language."""
+
+    CHINESE_MACAO_SAR = (5124, "zh-MO", "The Chinese Macao SAR language.")
+    """The Chinese Macao SAR language."""
+
+    CHINESE_SINGAPORE = (4100, "zh-SG", "The Chinese Singapore language.")
+    """The Chinese Singapore language."""
+
+    CROATIAN = (1050, "hr-HR", "The Croatian language.")
+    """The Croatian language."""
+
+    CZECH = (1029, "cs-CZ", "The Czech language.")
+    """The Czech language."""
+
+    DANISH = (1030, "da-DK", "The Danish language.")
+    """The Danish language."""
+
+    DIVEHI = (1125, "div-MV", "The Divehi language.")
+    """The Divehi language."""
+
+    DUTCH = (1043, "nl-NL", "The Dutch language.")
+    """The Dutch language."""
+
+    EDO = (1126, "bin-NG", "The Edo language.")
+    """The Edo language."""
+
+    ENGLISH_AUS = (3081, "en-AU", "The English AUS language.")
+    """The English AUS language."""
+
+    ENGLISH_BELIZE = (10249, "en-BZ", "The English Belize language.")
+    """The English Belize language."""
+
+    ENGLISH_CANADIAN = (4105, "en-CA", "The English Canadian language.")
+    """The English Canadian language."""
+
+    ENGLISH_CARIBBEAN = (9225, "en-CB", "The English Caribbean language.")
+    """The English Caribbean language."""
+
+    ENGLISH_INDONESIA = (14345, "en-ID", "The English Indonesia language.")
+    """The English Indonesia language."""
+
+    ENGLISH_IRELAND = (6153, "en-IE", "The English Ireland language.")
+    """The English Ireland language."""
+
+    ENGLISH_JAMAICA = (8201, "en-JA", "The English Jamaica language.")
+    """The English Jamaica language."""
+
+    ENGLISH_NEW_ZEALAND = (5129, "en-NZ", "The English NewZealand language.")
+    """The English NewZealand language."""
+
+    ENGLISH_PHILIPPINES = (13321, "en-PH", "The English Philippines language.")
+    """The English Philippines language."""
+
+    ENGLISH_SOUTH_AFRICA = (7177, "en-ZA", "The English South Africa language.")
+    """The English South Africa language."""
+
+    ENGLISH_TRINIDAD_TOBAGO = (11273, "en-TT", "The English Trinidad Tobago language.")
+    """The English Trinidad Tobago language."""
+
+    ENGLISH_UK = (2057, "en-GB", "The English UK language.")
+    """The English UK language."""
+
+    ENGLISH_US = (1033, "en-US", "The English US language.")
+    """The English US language."""
+
+    ENGLISH_ZIMBABWE = (12297, "en-ZW", "The English Zimbabwe language.")
+    """The English Zimbabwe language."""
+
+    ESTONIAN = (1061, "et-EE", "The Estonian language.")
+    """The Estonian language."""
+
+    FAEROESE = (1080, "fo-FO", "The Faeroese language.")
+    """The Faeroese language."""
+
+    FARSI = (1065, "fa-IR", "The Farsi language.")
+    """The Farsi language."""
+
+    FILIPINO = (1124, "fil-PH", "The Filipino language.")
+    """The Filipino language."""
+
+    FINNISH = (1035, "fi-FI", "The Finnish language.")
+    """The Finnish language."""
+
+    FRANCH_CONGO_DRC = (9228, "fr-CD", "The French Congo DRC language.")
+    """The French Congo DRC language."""
+
+    FRENCH = (1036, "fr-FR", "The French language.")
+    """The French language."""
+
+    FRENCH_CAMEROON = (11276, "fr-CM", "The French Cameroon language.")
+    """The French Cameroon language."""
+
+    FRENCH_CANADIAN = (3084, "fr-CA", "The French Canadian language.")
+    """The French Canadian language."""
+
+    FRENCH_COTED_IVOIRE = (12300, "fr-CI", "The French Coted Ivoire language.")
+    """The French Coted Ivoire language."""
+
+    FRENCH_HAITI = (15372, "fr-HT", "The French Haiti language.")
+    """The French Haiti language."""
+
+    FRENCH_LUXEMBOURG = (5132, "fr-LU", "The French Luxembourg language.")
+    """The French Luxembourg language."""
+
+    FRENCH_MALI = (13324, "fr-ML", "The French Mali language.")
+    """The French Mali language."""
+
+    FRENCH_MONACO = (6156, "fr-MC", "The French Monaco language.")
+    """The French Monaco language."""
+
+    FRENCH_MOROCCO = (14348, "fr-MA", "The French Morocco language.")
+    """The French Morocco language."""
+
+    FRENCH_REUNION = (8204, "fr-RE", "The French Reunion language.")
+    """The French Reunion language."""
+
+    FRENCH_SENEGAL = (10252, "fr-SN", "The French Senegal language.")
+    """The French Senegal language."""
+
+    FRENCH_WEST_INDIES = (7180, "fr-WINDIES", "The French West Indies language.")
+    """The French West Indies language."""
+
+    FRISIAN_NETHERLANDS = (1122, "fy-NL", "The Frisian Netherlands language.")
+    """The Frisian Netherlands language."""
+
+    FULFULDE = (1127, "ff-NG", "The Fulfulde language.")
+    """The Fulfulde language."""
+
+    GAELIC_IRELAND = (2108, "ga-IE", "The Gaelic Ireland language.")
+    """The Gaelic Ireland language."""
+
+    GAELIC_SCOTLAND = (1084, "en-US", "The Gaelic Scotland language.")
+    """The Gaelic Scotland language."""
+
+    GALICIAN = (1110, "gl-ES", "The Galician language.")
+    """The Galician language."""
+
+    GEORGIAN = (1079, "ka-GE", "The Georgian language.")
+    """The Georgian language."""
+
+    GERMAN = (1031, "de-DE", "The German language.")
+    """The German language."""
+
+    GERMAN_AUSTRIA = (3079, "de-AT", "The German Austria language.")
+    """The German Austria language."""
+
+    GERMAN_LIECHTENSTEIN = (5127, "de-LI", "The German Liechtenstein language.")
+    """The German Liechtenstein language."""
+
+    GERMAN_LUXEMBOURG = (4103, "de-LU", "The German Luxembourg language.")
+    """The German Luxembourg language."""
+
+    GREEK = (1032, "el-GR", "The Greek language.")
+    """The Greek language."""
+
+    GUARANI = (1140, "gn-PY", "The Guarani language.")
+    """The Guarani language."""
+
+    GUJARATI = (1095, "gu-IN", "The Gujarati language.")
+    """The Gujarati language."""
+
+    HAUSA = (1128, "ha-NG", "The Hausa language.")
+    """The Hausa language."""
+
+    HAWAIIAN = (1141, "haw-US", "The Hawaiian language.")
+    """The Hawaiian language."""
+
+    HEBREW = (1037, "he-IL", "The Hebrew language.")
+    """The Hebrew language."""
+
+    HINDI = (1081, "hi-IN", "The Hindi language.")
+    """The Hindi language."""
+
+    HUNGARIAN = (1038, "hu-HU", "The Hungarian language.")
+    """The Hungarian language."""
+
+    IBIBIO = (1129, "ibb-NG", "The Ibibio language.")
+    """The Ibibio language."""
+
+    ICELANDIC = (1039, "is-IS", "The Icelandic language.")
+    """The Icelandic language."""
+
+    IGBO = (1136, "ig-NG", "The Igbo language.")
+    """The Igbo language."""
+
+    INDONESIAN = (1057, "id-ID", "The Indonesian language.")
+    """The Indonesian language."""
+
+    INUKTITUT = (1117, "iu-Cans-CA", "The Inuktitut language.")
+    """The Inuktitut language."""
+
+    ITALIAN = (1040, "it-IT", "The Italian language.")
+    """The Italian language."""
+
+    JAPANESE = (1041, "ja-JP", "The Japanese language.")
+    """The Japanese language."""
+
+    KANNADA = (1099, "kn-IN", "The Kannada language.")
+    """The Kannada language."""
+
+    KANURI = (1137, "kr-NG", "The Kanuri language.")
+    """The Kanuri language."""
+
+    KASHMIRI = (1120, "ks-Arab", "The Kashmiri language.")
+    """The Kashmiri language."""
+
+    KASHMIRI_DEVANAGARI = (2144, "ks-Deva", "The Kashmiri Devanagari language.")
+    """The Kashmiri Devanagari language."""
+
+    KAZAKH = (1087, "kk-KZ", "The Kazakh language.")
+    """The Kazakh language."""
+
+    KHMER = (1107, "kh-KH", "The Khmer language.")
+    """The Khmer language."""
+
+    KIRGHIZ = (1088, "ky-KG", "The Kirghiz language.")
+    """The Kirghiz language."""
+
+    KONKANI = (1111, "kok-IN", "The Konkani language.")
+    """The Konkani language."""
+
+    KOREAN = (1042, "ko-KR", "The Korean language.")
+    """The Korean language."""
+
+    KYRGYZ = (1088, "ky-KG", "The Kyrgyz language.")
+    """The Kyrgyz language."""
+
+    LAO = (1108, "lo-LA", "The Lao language.")
+    """The Lao language."""
+
+    LATIN = (1142, "la-Latn", "The Latin language.")
+    """The Latin language."""
+
+    LATVIAN = (1062, "lv-LV", "The Latvian language.")
+    """The Latvian language."""
+
+    LITHUANIAN = (1063, "lt-LT", "The Lithuanian language.")
+    """The Lithuanian language."""
+
+    MACEDONINAN_FYROM = (1071, "mk-MK", "The Macedonian FYROM language.")
+    """The Macedonian FYROM language."""
+
+    MALAY_BRUNEI_DARUSSALAM = (2110, "ms-BN", "The Malay Brunei Darussalam language.")
+    """The Malay Brunei Darussalam language."""
+
+    MALAYALAM = (1100, "ml-IN", "The Malayalam language.")
+    """The Malayalam language."""
+
+    MALAYSIAN = (1086, "ms-MY", "The Malaysian language.")
+    """The Malaysian language."""
+
+    MALTESE = (1082, "mt-MT", "The Maltese language.")
+    """The Maltese language."""
+
+    MANIPURI = (1112, "mni-IN", "The Manipuri language.")
+    """The Manipuri language."""
+
+    MAORI = (1153, "mi-NZ", "The Maori language.")
+    """The Maori language."""
+
+    MARATHI = (1102, "mr-IN", "The Marathi language.")
+    """The Marathi language."""
+
+    MEXICAN_SPANISH = (2058, "es-MX", "The Mexican Spanish language.")
+    """The Mexican Spanish language."""
+
+    MONGOLIAN = (1104, "mn-MN", "The Mongolian language.")
+    """The Mongolian language."""
+
+    NEPALI = (1121, "ne-NP", "The Nepali language.")
+    """The Nepali language."""
+
+    NO_PROOFING = (1024, "en-US", "No proofing.")
+    """No proofing."""
+
+    NORWEGIAN_BOKMOL = (1044, "nb-NO", "The Norwegian Bokmol language.")
+    """The Norwegian Bokmol language."""
+
+    NORWEGIAN_NYNORSK = (2068, "nn-NO", "The Norwegian Nynorsk language.")
+    """The Norwegian Nynorsk language."""
+
+    ORIYA = (1096, "or-IN", "The Oriya language.")
+    """The Oriya language."""
+
+    OROMO = (1138, "om-Ethi-ET", "The Oromo language.")
+    """The Oromo language."""
+
+    PASHTO = (1123, "ps-AF", "The Pashto language.")
+    """The Pashto language."""
+
+    POLISH = (1045, "pl-PL", "The Polish language.")
+    """The Polish language."""
+
+    PORTUGUESE = (2070, "pt-PT", "The Portuguese language.")
+    """The Portuguese language."""
+
+    PUNJABI = (1094, "pa-IN", "The Punjabi language.")
+    """The Punjabi language."""
+
+    QUECHUA_BOLIVIA = (1131, "quz-BO", "The Quechua Bolivia language.")
+    """The Quechua Bolivia language."""
+
+    QUECHUA_ECUADOR = (2155, "quz-EC", "The Quechua Ecuador language.")
+    """The Quechua Ecuador language."""
+
+    QUECHUA_PERU = (3179, "quz-PE", "The Quechua Peru language.")
+    """The Quechua Peru language."""
+
+    RHAETO_ROMANIC = (1047, "rm-CH", "The Rhaeto Romanic language.")
+    """The Rhaeto Romanic language."""
+
+    ROMANIAN = (1048, "ro-RO", "The Romanian language.")
+    """The Romanian language."""
+
+    ROMANIAN_MOLDOVA = (2072, "ro-MO", "The Romanian Moldova language.")
+    """The Romanian Moldova language."""
+
+    RUSSIAN = (1049, "ru-RU", "The Russian language.")
+    """The Russian language."""
+
+    RUSSIAN_MOLDOVA = (2073, "ru-MO", "The Russian Moldova language.")
+    """The Russian Moldova language."""
+
+    SAMI_LAPPISH = (1083, "se-NO", "The Sami Lappish language.")
+    """The Sami Lappish language."""
+
+    SANSKRIT = (1103, "sa-IN", "The Sanskrit language.")
+    """The Sanskrit language."""
+
+    SEPEDI = (1132, "ns-ZA", "The Sepedi language.")
+    """The Sepedi language."""
+
+    SERBIAN_BOSNIA_HERZEGOVINA_CYRILLIC = (
+        7194,
+        "sr-BA",
+        "The Serbian Bosnia Herzegovina Cyrillic language.",
+    )
+    """The Serbian Bosnia Herzegovina Cyrillic language."""
+
+    SERBIAN_BOSNIA_HERZEGOVINA_LATIN = (
+        6170,
+        "sr-Latn-BA",
+        "The Serbian Bosnia Herzegovina Latin language.",
+    )
+    """The Serbian Bosnia Herzegovina Latin language."""
+
+    SERBIAN_CYRILLIC = (3098, "sr-SP", "The Serbian Cyrillic language.")
+    """The Serbian Cyrillic language."""
+
+    SERBIAN_LATIN = (2074, "sr-Latn-CS", "The Serbian Latin language.")
+    """The Serbian Latin language."""
+
+    SESOTHO = (1072, "st-ZA", "The Sesotho language.")
+    """The Sesotho language."""
+
+    SIMPLIFIED_CHINESE = (2052, "zh-CN", "The Simplified Chinese language.")
+    """The Simplified Chinese language."""
+
+    SINDHI = (1113, "sd-Deva-IN", "The Sindhi language.")
+    """The Sindhi language."""
+
+    SINDHI_PAKISTAN = (2137, "sd-Arab-PK", "The Sindhi Pakistan language.")
+    """The Sindhi Pakistan language."""
+
+    SINHALESE = (1115, "si-LK", "The Sinhalese language.")
+    """The Sinhalese language."""
+
+    SLOVAK = (1051, "sk-SK", "The Slovak language.")
+    """The Slovak language."""
+
+    SLOVENIAN = (1060, "sl-SI", "The Slovenian language.")
+    """The Slovenian language."""
+
+    SOMALI = (1143, "so-SO", "The Somali language.")
+    """The Somali language."""
+
+    SORBIAN = (1070, "wen-DE", "The Sorbian language.")
+    """The Sorbian language."""
+
+    SPANISH = (1034, "es-ES_tradnl", "The Spanish language.")
+    """The Spanish language."""
+
+    SPANISH_ARGENTINA = (11274, "es-AR", "The Spanish Argentina language.")
+    """The Spanish Argentina language."""
+
+    SPANISH_BOLIVIA = (16394, "es-BO", "The Spanish Bolivia language.")
+    """The Spanish Bolivia language."""
+
+    SPANISH_CHILE = (13322, "es-CL", "The Spanish Chile language.")
+    """The Spanish Chile language."""
+
+    SPANISH_COLOMBIA = (9226, "es-CO", "The Spanish Colombia language.")
+    """The Spanish Colombia language."""
+
+    SPANISH_COSTA_RICA = (5130, "es-CR", "The Spanish Costa Rica language.")
+    """The Spanish Costa Rica language."""
+
+    SPANISH_DOMINICAN_REPUBLIC = (7178, "es-DO", "The Spanish Dominican Republic language.")
+    """The Spanish Dominican Republic language."""
+
+    SPANISH_ECUADOR = (12298, "es-EC", "The Spanish Ecuador language.")
+    """The Spanish Ecuador language."""
+
+    SPANISH_EL_SALVADOR = (17418, "es-SV", "The Spanish El Salvador language.")
+    """The Spanish El Salvador language."""
+
+    SPANISH_GUATEMALA = (4106, "es-GT", "The Spanish Guatemala language.")
+    """The Spanish Guatemala language."""
+
+    SPANISH_HONDURAS = (18442, "es-HN", "The Spanish Honduras language.")
+    """The Spanish Honduras language."""
+
+    SPANISH_MODERN_SORT = (3082, "es-ES", "The Spanish Modern Sort language.")
+    """The Spanish Modern Sort language."""
+
+    SPANISH_NICARAGUA = (19466, "es-NI", "The Spanish Nicaragua language.")
+    """The Spanish Nicaragua language."""
+
+    SPANISH_PANAMA = (6154, "es-PA", "The Spanish Panama language.")
+    """The Spanish Panama language."""
+
+    SPANISH_PARAGUAY = (15370, "es-PY", "The Spanish Paraguay language.")
+    """The Spanish Paraguay language."""
+
+    SPANISH_PERU = (10250, "es-PE", "The Spanish Peru language.")
+    """The Spanish Peru language."""
+
+    SPANISH_PUERTO_RICO = (20490, "es-PR", "The Spanish Puerto Rico language.")
+    """The Spanish Puerto Rico language."""
+
+    SPANISH_URUGUAY = (14346, "es-UR", "The Spanish Uruguay language.")
+    """The Spanish Uruguay language."""
+
+    SPANISH_VENEZUELA = (8202, "es-VE", "The Spanish Venezuela language.")
+    """The Spanish Venezuela language."""
+
+    SUTU = (1072, "st-ZA", "The Sutu language.")
+    """The Sutu language."""
+
+    SWAHILI = (1089, "sw-KE", "The Swahili language.")
+    """The Swahili language."""
+
+    SWEDISH = (1053, "sv-SE", "The Swedish language.")
+    """The Swedish language."""
+
+    SWEDISH_FINLAND = (2077, "sv-FI", "The Swedish Finland language.")
+    """The Swedish Finland language."""
+
+    SWISS_FRENCH = (4108, "fr-CH", "The Swiss French language.")
+    """The Swiss French language."""
+
+    SWISS_GERMAN = (2055, "de-CH", "The Swiss German language.")
+    """The Swiss German language."""
+
+    SWISS_ITALIAN = (2064, "it-CH", "The Swiss Italian language.")
+    """The Swiss Italian language."""
+
+    SYRIAC = (1114, "syr-SY", "The Syriac language.")
+    """The Syriac language."""
+
+    TAJIK = (1064, "tg-TJ", "The Tajik language.")
+    """The Tajik language."""
+
+    TAMAZIGHT = (1119, "tzm-Arab-MA", "The Tamazight language.")
+    """The Tamazight language."""
+
+    TAMAZIGHT_LATIN = (2143, "tmz-DZ", "The Tamazight Latin language.")
+    """The Tamazight Latin language."""
+
+    TAMIL = (1097, "ta-IN", "The Tamil language.")
+    """The Tamil language."""
+
+    TATAR = (1092, "tt-RU", "The Tatar language.")
+    """The Tatar language."""
+
+    TELUGU = (1098, "te-IN", "The Telugu language.")
+    """The Telugu language."""
+
+    THAI = (1054, "th-TH", "The Thai language.")
+    """The Thai language."""
+
+    TIBETAN = (1105, "bo-CN", "The Tibetan language.")
+    """The Tibetan language."""
+
+    TIGRIGNA_ERITREA = (2163, "ti-ER", "The Tigrigna Eritrea language.")
+    """The Tigrigna Eritrea language."""
+
+    TIGRIGNA_ETHIOPIC = (1139, "ti-ET", "The Tigrigna Ethiopic language.")
+    """The Tigrigna Ethiopic language."""
+
+    TRADITIONAL_CHINESE = (1028, "zh-TW", "The Traditional Chinese language.")
+    """The Traditional Chinese language."""
+
+    TSONGA = (1073, "ts-ZA", "The Tsonga language.")
+    """The Tsonga language."""
+
+    TSWANA = (1074, "tn-ZA", "The Tswana language.")
+    """The Tswana language."""
+
+    TURKISH = (1055, "tr-TR", "The Turkish language.")
+    """The Turkish language."""
+
+    TURKMEN = (1090, "tk-TM", "The Turkmen language.")
+    """The Turkmen language."""
+
+    UKRAINIAN = (1058, "uk-UA", "The Ukrainian language.")
+    """The Ukrainian language."""
+
+    URDU = (1056, "ur-PK", "The Urdu language.")
+    """The Urdu language."""
+
+    UZBEK_CYRILLIC = (2115, "uz-UZ", "The Uzbek Cyrillic language.")
+    """The Uzbek Cyrillic language."""
+
+    UZBEK_LATIN = (1091, "uz-Latn-UZ", "The Uzbek Latin language.")
+    """The Uzbek Latin language."""
+
+    VENDA = (1075, "ve-ZA", "The Venda language.")
+    """The Venda language."""
+
+    VIETNAMESE = (1066, "vi-VN", "The Vietnamese language.")
+    """The Vietnamese language."""
+
+    WELSH = (1106, "cy-GB", "The Welsh language.")
+    """The Welsh language."""
+
+    XHOSA = (1076, "xh-ZA", "The Xhosa language.")
+    """The Xhosa language."""
+
+    YI = (1144, "ii-CN", "The Yi language.")
+    """The Yi language."""
+
+    YIDDISH = (1085, "yi-Hebr", "The Yiddish language.")
+    """The Yiddish language."""
+
+    YORUBA = (1130, "yo-NG", "The Yoruba language.")
+    """The Yoruba language."""
+
+    ZULU = (1077, "zu-ZA", "The Zulu language.")
+    """The Zulu language."""
+
+    MIXED = (-2, "", "More than one language in specified range (read-only).")
+    """More than one language in specified range (read-only)."""
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/shapes.py b/.venv/lib/python3.12/site-packages/pptx/enum/shapes.py
new file mode 100644
index 00000000..86f521f4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/shapes.py
@@ -0,0 +1,1029 @@
+"""Enumerations used by shapes and related objects."""
+
+from __future__ import annotations
+
+import enum
+
+from pptx.enum.base import BaseEnum, BaseXmlEnum
+
+
+class MSO_AUTO_SHAPE_TYPE(BaseXmlEnum):
+    """Specifies a type of AutoShape, e.g. DOWN_ARROW.
+
+    Alias: ``MSO_SHAPE``
+
+    Example::
+
+        from pptx.enum.shapes import MSO_SHAPE
+        from pptx.util import Inches
+
+        left = top = width = height = Inches(1.0)
+        slide.shapes.add_shape(
+            MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height
+        )
+
+    MS API Name: `MsoAutoShapeType`
+
+    https://learn.microsoft.com/en-us/office/vba/api/Office.MsoAutoShapeType
+    """
+
+    ACTION_BUTTON_BACK_OR_PREVIOUS = (
+        129,
+        "actionButtonBackPrevious",
+        "Back or Previous button. Supports mouse-click and mouse-over actions",
+    )
+    """Back or Previous button. Supports mouse-click and mouse-over actions"""
+
+    ACTION_BUTTON_BEGINNING = (
+        131,
+        "actionButtonBeginning",
+        "Beginning button. Supports mouse-click and mouse-over actions",
+    )
+    """Beginning button. Supports mouse-click and mouse-over actions"""
+
+    ACTION_BUTTON_CUSTOM = (
+        125,
+        "actionButtonBlank",
+        "Button with no default picture or text. Supports mouse-click and mouse-over actions",
+    )
+    """Button with no default picture or text. Supports mouse-click and mouse-over actions"""
+
+    ACTION_BUTTON_DOCUMENT = (
+        134,
+        "actionButtonDocument",
+        "Document button. Supports mouse-click and mouse-over actions",
+    )
+    """Document button. Supports mouse-click and mouse-over actions"""
+
+    ACTION_BUTTON_END = (
+        132,
+        "actionButtonEnd",
+        "End button. Supports mouse-click and mouse-over actions",
+    )
+    """End button. Supports mouse-click and mouse-over actions"""
+
+    ACTION_BUTTON_FORWARD_OR_NEXT = (
+        130,
+        "actionButtonForwardNext",
+        "Forward or Next button. Supports mouse-click and mouse-over actions",
+    )
+    """Forward or Next button. Supports mouse-click and mouse-over actions"""
+
+    ACTION_BUTTON_HELP = (
+        127,
+        "actionButtonHelp",
+        "Help button. Supports mouse-click and mouse-over actions",
+    )
+    """Help button. Supports mouse-click and mouse-over actions"""
+
+    ACTION_BUTTON_HOME = (
+        126,
+        "actionButtonHome",
+        "Home button. Supports mouse-click and mouse-over actions",
+    )
+    """Home button. Supports mouse-click and mouse-over actions"""
+
+    ACTION_BUTTON_INFORMATION = (
+        128,
+        "actionButtonInformation",
+        "Information button. Supports mouse-click and mouse-over actions",
+    )
+    """Information button. Supports mouse-click and mouse-over actions"""
+
+    ACTION_BUTTON_MOVIE = (
+        136,
+        "actionButtonMovie",
+        "Movie button. Supports mouse-click and mouse-over actions",
+    )
+    """Movie button. Supports mouse-click and mouse-over actions"""
+
+    ACTION_BUTTON_RETURN = (
+        133,
+        "actionButtonReturn",
+        "Return button. Supports mouse-click and mouse-over actions",
+    )
+    """Return button. Supports mouse-click and mouse-over actions"""
+
+    ACTION_BUTTON_SOUND = (
+        135,
+        "actionButtonSound",
+        "Sound button. Supports mouse-click and mouse-over actions",
+    )
+    """Sound button. Supports mouse-click and mouse-over actions"""
+
+    ARC = (25, "arc", "Arc")
+    """Arc"""
+
+    BALLOON = (137, "wedgeRoundRectCallout", "Rounded Rectangular Callout")
+    """Rounded Rectangular Callout"""
+
+    BENT_ARROW = (41, "bentArrow", "Block arrow that follows a curved 90-degree angle")
+    """Block arrow that follows a curved 90-degree angle"""
+
+    BENT_UP_ARROW = (
+        44,
+        "bentUpArrow",
+        "Block arrow that follows a sharp 90-degree angle. Points up by default",
+    )
+    """Block arrow that follows a sharp 90-degree angle. Points up by default"""
+
+    BEVEL = (15, "bevel", "Bevel")
+    """Bevel"""
+
+    BLOCK_ARC = (20, "blockArc", "Block arc")
+    """Block arc"""
+
+    CAN = (13, "can", "Can")
+    """Can"""
+
+    CHART_PLUS = (182, "chartPlus", "Chart Plus")
+    """Chart Plus"""
+
+    CHART_STAR = (181, "chartStar", "Chart Star")
+    """Chart Star"""
+
+    CHART_X = (180, "chartX", "Chart X")
+    """Chart X"""
+
+    CHEVRON = (52, "chevron", "Chevron")
+    """Chevron"""
+
+    CHORD = (161, "chord", "Geometric chord shape")
+    """Geometric chord shape"""
+
+    CIRCULAR_ARROW = (60, "circularArrow", "Block arrow that follows a curved 180-degree angle")
+    """Block arrow that follows a curved 180-degree angle"""
+
+    CLOUD = (179, "cloud", "Cloud")
+    """Cloud"""
+
+    CLOUD_CALLOUT = (108, "cloudCallout", "Cloud callout")
+    """Cloud callout"""
+
+    CORNER = (162, "corner", "Corner")
+    """Corner"""
+
+    CORNER_TABS = (169, "cornerTabs", "Corner Tabs")
+    """Corner Tabs"""
+
+    CROSS = (11, "plus", "Cross")
+    """Cross"""
+
+    CUBE = (14, "cube", "Cube")
+    """Cube"""
+
+    CURVED_DOWN_ARROW = (48, "curvedDownArrow", "Block arrow that curves down")
+    """Block arrow that curves down"""
+
+    CURVED_DOWN_RIBBON = (100, "ellipseRibbon", "Ribbon banner that curves down")
+    """Ribbon banner that curves down"""
+
+    CURVED_LEFT_ARROW = (46, "curvedLeftArrow", "Block arrow that curves left")
+    """Block arrow that curves left"""
+
+    CURVED_RIGHT_ARROW = (45, "curvedRightArrow", "Block arrow that curves right")
+    """Block arrow that curves right"""
+
+    CURVED_UP_ARROW = (47, "curvedUpArrow", "Block arrow that curves up")
+    """Block arrow that curves up"""
+
+    CURVED_UP_RIBBON = (99, "ellipseRibbon2", "Ribbon banner that curves up")
+    """Ribbon banner that curves up"""
+
+    DECAGON = (144, "decagon", "Decagon")
+    """Decagon"""
+
+    DIAGONAL_STRIPE = (141, "diagStripe", "Diagonal Stripe")
+    """Diagonal Stripe"""
+
+    DIAMOND = (4, "diamond", "Diamond")
+    """Diamond"""
+
+    DODECAGON = (146, "dodecagon", "Dodecagon")
+    """Dodecagon"""
+
+    DONUT = (18, "donut", "Donut")
+    """Donut"""
+
+    DOUBLE_BRACE = (27, "bracePair", "Double brace")
+    """Double brace"""
+
+    DOUBLE_BRACKET = (26, "bracketPair", "Double bracket")
+    """Double bracket"""
+
+    DOUBLE_WAVE = (104, "doubleWave", "Double wave")
+    """Double wave"""
+
+    DOWN_ARROW = (36, "downArrow", "Block arrow that points down")
+    """Block arrow that points down"""
+
+    DOWN_ARROW_CALLOUT = (56, "downArrowCallout", "Callout with arrow that points down")
+    """Callout with arrow that points down"""
+
+    DOWN_RIBBON = (98, "ribbon", "Ribbon banner with center area below ribbon ends")
+    """Ribbon banner with center area below ribbon ends"""
+
+    EXPLOSION1 = (89, "irregularSeal1", "Explosion")
+    """Explosion"""
+
+    EXPLOSION2 = (90, "irregularSeal2", "Explosion")
+    """Explosion"""
+
+    FLOWCHART_ALTERNATE_PROCESS = (
+        62,
+        "flowChartAlternateProcess",
+        "Alternate process flowchart symbol",
+    )
+    """Alternate process flowchart symbol"""
+
+    FLOWCHART_CARD = (75, "flowChartPunchedCard", "Card flowchart symbol")
+    """Card flowchart symbol"""
+
+    FLOWCHART_COLLATE = (79, "flowChartCollate", "Collate flowchart symbol")
+    """Collate flowchart symbol"""
+
+    FLOWCHART_CONNECTOR = (73, "flowChartConnector", "Connector flowchart symbol")
+    """Connector flowchart symbol"""
+
+    FLOWCHART_DATA = (64, "flowChartInputOutput", "Data flowchart symbol")
+    """Data flowchart symbol"""
+
+    FLOWCHART_DECISION = (63, "flowChartDecision", "Decision flowchart symbol")
+    """Decision flowchart symbol"""
+
+    FLOWCHART_DELAY = (84, "flowChartDelay", "Delay flowchart symbol")
+    """Delay flowchart symbol"""
+
+    FLOWCHART_DIRECT_ACCESS_STORAGE = (
+        87,
+        "flowChartMagneticDrum",
+        "Direct access storage flowchart symbol",
+    )
+    """Direct access storage flowchart symbol"""
+
+    FLOWCHART_DISPLAY = (88, "flowChartDisplay", "Display flowchart symbol")
+    """Display flowchart symbol"""
+
+    FLOWCHART_DOCUMENT = (67, "flowChartDocument", "Document flowchart symbol")
+    """Document flowchart symbol"""
+
+    FLOWCHART_EXTRACT = (81, "flowChartExtract", "Extract flowchart symbol")
+    """Extract flowchart symbol"""
+
+    FLOWCHART_INTERNAL_STORAGE = (
+        66,
+        "flowChartInternalStorage",
+        "Internal storage flowchart symbol",
+    )
+    """Internal storage flowchart symbol"""
+
+    FLOWCHART_MAGNETIC_DISK = (86, "flowChartMagneticDisk", "Magnetic disk flowchart symbol")
+    """Magnetic disk flowchart symbol"""
+
+    FLOWCHART_MANUAL_INPUT = (71, "flowChartManualInput", "Manual input flowchart symbol")
+    """Manual input flowchart symbol"""
+
+    FLOWCHART_MANUAL_OPERATION = (
+        72,
+        "flowChartManualOperation",
+        "Manual operation flowchart symbol",
+    )
+    """Manual operation flowchart symbol"""
+
+    FLOWCHART_MERGE = (82, "flowChartMerge", "Merge flowchart symbol")
+    """Merge flowchart symbol"""
+
+    FLOWCHART_MULTIDOCUMENT = (68, "flowChartMultidocument", "Multi-document flowchart symbol")
+    """Multi-document flowchart symbol"""
+
+    FLOWCHART_OFFLINE_STORAGE = (139, "flowChartOfflineStorage", "Offline Storage")
+    """Offline Storage"""
+
+    FLOWCHART_OFFPAGE_CONNECTOR = (
+        74,
+        "flowChartOffpageConnector",
+        "Off-page connector flowchart symbol",
+    )
+    """Off-page connector flowchart symbol"""
+
+    FLOWCHART_OR = (78, "flowChartOr", '"Or" flowchart symbol')
+    """\"Or\" flowchart symbol"""
+
+    FLOWCHART_PREDEFINED_PROCESS = (
+        65,
+        "flowChartPredefinedProcess",
+        "Predefined process flowchart symbol",
+    )
+    """Predefined process flowchart symbol"""
+
+    FLOWCHART_PREPARATION = (70, "flowChartPreparation", "Preparation flowchart symbol")
+    """Preparation flowchart symbol"""
+
+    FLOWCHART_PROCESS = (61, "flowChartProcess", "Process flowchart symbol")
+    """Process flowchart symbol"""
+
+    FLOWCHART_PUNCHED_TAPE = (76, "flowChartPunchedTape", "Punched tape flowchart symbol")
+    """Punched tape flowchart symbol"""
+
+    FLOWCHART_SEQUENTIAL_ACCESS_STORAGE = (
+        85,
+        "flowChartMagneticTape",
+        "Sequential access storage flowchart symbol",
+    )
+    """Sequential access storage flowchart symbol"""
+
+    FLOWCHART_SORT = (80, "flowChartSort", "Sort flowchart symbol")
+    """Sort flowchart symbol"""
+
+    FLOWCHART_STORED_DATA = (83, "flowChartOnlineStorage", "Stored data flowchart symbol")
+    """Stored data flowchart symbol"""
+
+    FLOWCHART_SUMMING_JUNCTION = (
+        77,
+        "flowChartSummingJunction",
+        "Summing junction flowchart symbol",
+    )
+    """Summing junction flowchart symbol"""
+
+    FLOWCHART_TERMINATOR = (69, "flowChartTerminator", "Terminator flowchart symbol")
+    """Terminator flowchart symbol"""
+
+    FOLDED_CORNER = (16, "foldedCorner", "Folded corner")
+    """Folded corner"""
+
+    FRAME = (158, "frame", "Frame")
+    """Frame"""
+
+    FUNNEL = (174, "funnel", "Funnel")
+    """Funnel"""
+
+    GEAR_6 = (172, "gear6", "Gear 6")
+    """Gear 6"""
+
+    GEAR_9 = (173, "gear9", "Gear 9")
+    """Gear 9"""
+
+    HALF_FRAME = (159, "halfFrame", "Half Frame")
+    """Half Frame"""
+
+    HEART = (21, "heart", "Heart")
+    """Heart"""
+
+    HEPTAGON = (145, "heptagon", "Heptagon")
+    """Heptagon"""
+
+    HEXAGON = (10, "hexagon", "Hexagon")
+    """Hexagon"""
+
+    HORIZONTAL_SCROLL = (102, "horizontalScroll", "Horizontal scroll")
+    """Horizontal scroll"""
+
+    ISOSCELES_TRIANGLE = (7, "triangle", "Isosceles triangle")
+    """Isosceles triangle"""
+
+    LEFT_ARROW = (34, "leftArrow", "Block arrow that points left")
+    """Block arrow that points left"""
+
+    LEFT_ARROW_CALLOUT = (54, "leftArrowCallout", "Callout with arrow that points left")
+    """Callout with arrow that points left"""
+
+    LEFT_BRACE = (31, "leftBrace", "Left brace")
+    """Left brace"""
+
+    LEFT_BRACKET = (29, "leftBracket", "Left bracket")
+    """Left bracket"""
+
+    LEFT_CIRCULAR_ARROW = (176, "leftCircularArrow", "Left Circular Arrow")
+    """Left Circular Arrow"""
+
+    LEFT_RIGHT_ARROW = (
+        37,
+        "leftRightArrow",
+        "Block arrow with arrowheads that point both left and right",
+    )
+    """Block arrow with arrowheads that point both left and right"""
+
+    LEFT_RIGHT_ARROW_CALLOUT = (
+        57,
+        "leftRightArrowCallout",
+        "Callout with arrowheads that point both left and right",
+    )
+    """Callout with arrowheads that point both left and right"""
+
+    LEFT_RIGHT_CIRCULAR_ARROW = (177, "leftRightCircularArrow", "Left Right Circular Arrow")
+    """Left Right Circular Arrow"""
+
+    LEFT_RIGHT_RIBBON = (140, "leftRightRibbon", "Left Right Ribbon")
+    """Left Right Ribbon"""
+
+    LEFT_RIGHT_UP_ARROW = (
+        40,
+        "leftRightUpArrow",
+        "Block arrow with arrowheads that point left, right, and up",
+    )
+    """Block arrow with arrowheads that point left, right, and up"""
+
+    LEFT_UP_ARROW = (43, "leftUpArrow", "Block arrow with arrowheads that point left and up")
+    """Block arrow with arrowheads that point left and up"""
+
+    LIGHTNING_BOLT = (22, "lightningBolt", "Lightning bolt")
+    """Lightning bolt"""
+
+    LINE_CALLOUT_1 = (109, "borderCallout1", "Callout with border and horizontal callout line")
+    """Callout with border and horizontal callout line"""
+
+    LINE_CALLOUT_1_ACCENT_BAR = (113, "accentCallout1", "Callout with vertical accent bar")
+    """Callout with vertical accent bar"""
+
+    LINE_CALLOUT_1_BORDER_AND_ACCENT_BAR = (
+        121,
+        "accentBorderCallout1",
+        "Callout with border and vertical accent bar",
+    )
+    """Callout with border and vertical accent bar"""
+
+    LINE_CALLOUT_1_NO_BORDER = (117, "callout1", "Callout with horizontal line")
+    """Callout with horizontal line"""
+
+    LINE_CALLOUT_2 = (110, "borderCallout2", "Callout with diagonal straight line")
+    """Callout with diagonal straight line"""
+
+    LINE_CALLOUT_2_ACCENT_BAR = (
+        114,
+        "accentCallout2",
+        "Callout with diagonal callout line and accent bar",
+    )
+    """Callout with diagonal callout line and accent bar"""
+
+    LINE_CALLOUT_2_BORDER_AND_ACCENT_BAR = (
+        122,
+        "accentBorderCallout2",
+        "Callout with border, diagonal straight line, and accent bar",
+    )
+    """Callout with border, diagonal straight line, and accent bar"""
+
+    LINE_CALLOUT_2_NO_BORDER = (118, "callout2", "Callout with no border and diagonal callout line")
+    """Callout with no border and diagonal callout line"""
+
+    LINE_CALLOUT_3 = (111, "borderCallout3", "Callout with angled line")
+    """Callout with angled line"""
+
+    LINE_CALLOUT_3_ACCENT_BAR = (
+        115,
+        "accentCallout3",
+        "Callout with angled callout line and accent bar",
+    )
+    """Callout with angled callout line and accent bar"""
+
+    LINE_CALLOUT_3_BORDER_AND_ACCENT_BAR = (
+        123,
+        "accentBorderCallout3",
+        "Callout with border, angled callout line, and accent bar",
+    )
+    """Callout with border, angled callout line, and accent bar"""
+
+    LINE_CALLOUT_3_NO_BORDER = (119, "callout3", "Callout with no border and angled callout line")
+    """Callout with no border and angled callout line"""
+
+    LINE_CALLOUT_4 = (
+        112,
+        "borderCallout3",
+        "Callout with callout line segments forming a U-shape.",
+    )
+    """Callout with callout line segments forming a U-shape."""
+
+    LINE_CALLOUT_4_ACCENT_BAR = (
+        116,
+        "accentCallout3",
+        "Callout with accent bar and callout line segments forming a U-shape.",
+    )
+    """Callout with accent bar and callout line segments forming a U-shape."""
+
+    LINE_CALLOUT_4_BORDER_AND_ACCENT_BAR = (
+        124,
+        "accentBorderCallout3",
+        "Callout with border, accent bar, and callout line segments forming a U-shape.",
+    )
+    """Callout with border, accent bar, and callout line segments forming a U-shape."""
+
+    LINE_CALLOUT_4_NO_BORDER = (
+        120,
+        "callout3",
+        "Callout with no border and callout line segments forming a U-shape.",
+    )
+    """Callout with no border and callout line segments forming a U-shape."""
+
+    LINE_INVERSE = (183, "lineInv", "Straight Connector")
+    """Straight Connector"""
+
+    MATH_DIVIDE = (166, "mathDivide", "Division")
+    """Division"""
+
+    MATH_EQUAL = (167, "mathEqual", "Equal")
+    """Equal"""
+
+    MATH_MINUS = (164, "mathMinus", "Minus")
+    """Minus"""
+
+    MATH_MULTIPLY = (165, "mathMultiply", "Multiply")
+    """Multiply"""
+
+    MATH_NOT_EQUAL = (168, "mathNotEqual", "Not Equal")
+    """Not Equal"""
+
+    MATH_PLUS = (163, "mathPlus", "Plus")
+    """Plus"""
+
+    MOON = (24, "moon", "Moon")
+    """Moon"""
+
+    NON_ISOSCELES_TRAPEZOID = (143, "nonIsoscelesTrapezoid", "Non-isosceles Trapezoid")
+    """Non-isosceles Trapezoid"""
+
+    NOTCHED_RIGHT_ARROW = (50, "notchedRightArrow", "Notched block arrow that points right")
+    """Notched block arrow that points right"""
+
+    NO_SYMBOL = (19, "noSmoking", "'No' Symbol")
+    """'No' Symbol"""
+
+    OCTAGON = (6, "octagon", "Octagon")
+    """Octagon"""
+
+    OVAL = (9, "ellipse", "Oval")
+    """Oval"""
+
+    OVAL_CALLOUT = (107, "wedgeEllipseCallout", "Oval-shaped callout")
+    """Oval-shaped callout"""
+
+    PARALLELOGRAM = (2, "parallelogram", "Parallelogram")
+    """Parallelogram"""
+
+    PENTAGON = (51, "homePlate", "Pentagon")
+    """Pentagon"""
+
+    PIE = (142, "pie", "Pie")
+    """Pie"""
+
+    PIE_WEDGE = (175, "pieWedge", "Pie")
+    """Pie"""
+
+    PLAQUE = (28, "plaque", "Plaque")
+    """Plaque"""
+
+    PLAQUE_TABS = (171, "plaqueTabs", "Plaque Tabs")
+    """Plaque Tabs"""
+
+    QUAD_ARROW = (39, "quadArrow", "Block arrows that point up, down, left, and right")
+    """Block arrows that point up, down, left, and right"""
+
+    QUAD_ARROW_CALLOUT = (
+        59,
+        "quadArrowCallout",
+        "Callout with arrows that point up, down, left, and right",
+    )
+    """Callout with arrows that point up, down, left, and right"""
+
+    RECTANGLE = (1, "rect", "Rectangle")
+    """Rectangle"""
+
+    RECTANGULAR_CALLOUT = (105, "wedgeRectCallout", "Rectangular callout")
+    """Rectangular callout"""
+
+    REGULAR_PENTAGON = (12, "pentagon", "Pentagon")
+    """Pentagon"""
+
+    RIGHT_ARROW = (33, "rightArrow", "Block arrow that points right")
+    """Block arrow that points right"""
+
+    RIGHT_ARROW_CALLOUT = (53, "rightArrowCallout", "Callout with arrow that points right")
+    """Callout with arrow that points right"""
+
+    RIGHT_BRACE = (32, "rightBrace", "Right brace")
+    """Right brace"""
+
+    RIGHT_BRACKET = (30, "rightBracket", "Right bracket")
+    """Right bracket"""
+
+    RIGHT_TRIANGLE = (8, "rtTriangle", "Right triangle")
+    """Right triangle"""
+
+    ROUNDED_RECTANGLE = (5, "roundRect", "Rounded rectangle")
+    """Rounded rectangle"""
+
+    ROUNDED_RECTANGULAR_CALLOUT = (106, "wedgeRoundRectCallout", "Rounded rectangle-shaped callout")
+    """Rounded rectangle-shaped callout"""
+
+    ROUND_1_RECTANGLE = (151, "round1Rect", "Round Single Corner Rectangle")
+    """Round Single Corner Rectangle"""
+
+    ROUND_2_DIAG_RECTANGLE = (153, "round2DiagRect", "Round Diagonal Corner Rectangle")
+    """Round Diagonal Corner Rectangle"""
+
+    ROUND_2_SAME_RECTANGLE = (152, "round2SameRect", "Round Same Side Corner Rectangle")
+    """Round Same Side Corner Rectangle"""
+
+    SMILEY_FACE = (17, "smileyFace", "Smiley face")
+    """Smiley face"""
+
+    SNIP_1_RECTANGLE = (155, "snip1Rect", "Snip Single Corner Rectangle")
+    """Snip Single Corner Rectangle"""
+
+    SNIP_2_DIAG_RECTANGLE = (157, "snip2DiagRect", "Snip Diagonal Corner Rectangle")
+    """Snip Diagonal Corner Rectangle"""
+
+    SNIP_2_SAME_RECTANGLE = (156, "snip2SameRect", "Snip Same Side Corner Rectangle")
+    """Snip Same Side Corner Rectangle"""
+
+    SNIP_ROUND_RECTANGLE = (154, "snipRoundRect", "Snip and Round Single Corner Rectangle")
+    """Snip and Round Single Corner Rectangle"""
+
+    SQUARE_TABS = (170, "squareTabs", "Square Tabs")
+    """Square Tabs"""
+
+    STAR_10_POINT = (149, "star10", "10-Point Star")
+    """10-Point Star"""
+
+    STAR_12_POINT = (150, "star12", "12-Point Star")
+    """12-Point Star"""
+
+    STAR_16_POINT = (94, "star16", "16-point star")
+    """16-point star"""
+
+    STAR_24_POINT = (95, "star24", "24-point star")
+    """24-point star"""
+
+    STAR_32_POINT = (96, "star32", "32-point star")
+    """32-point star"""
+
+    STAR_4_POINT = (91, "star4", "4-point star")
+    """4-point star"""
+
+    STAR_5_POINT = (92, "star5", "5-point star")
+    """5-point star"""
+
+    STAR_6_POINT = (147, "star6", "6-Point Star")
+    """6-Point Star"""
+
+    STAR_7_POINT = (148, "star7", "7-Point Star")
+    """7-Point Star"""
+
+    STAR_8_POINT = (93, "star8", "8-point star")
+    """8-point star"""
+
+    STRIPED_RIGHT_ARROW = (
+        49,
+        "stripedRightArrow",
+        "Block arrow that points right with stripes at the tail",
+    )
+    """Block arrow that points right with stripes at the tail"""
+
+    SUN = (23, "sun", "Sun")
+    """Sun"""
+
+    SWOOSH_ARROW = (178, "swooshArrow", "Swoosh Arrow")
+    """Swoosh Arrow"""
+
+    TEAR = (160, "teardrop", "Teardrop")
+    """Teardrop"""
+
+    TRAPEZOID = (3, "trapezoid", "Trapezoid")
+    """Trapezoid"""
+
+    UP_ARROW = (35, "upArrow", "Block arrow that points up")
+    """Block arrow that points up"""
+
+    UP_ARROW_CALLOUT = (55, "upArrowCallout", "Callout with arrow that points up")
+    """Callout with arrow that points up"""
+
+    UP_DOWN_ARROW = (38, "upDownArrow", "Block arrow that points up and down")
+    """Block arrow that points up and down"""
+
+    UP_DOWN_ARROW_CALLOUT = (58, "upDownArrowCallout", "Callout with arrows that point up and down")
+    """Callout with arrows that point up and down"""
+
+    UP_RIBBON = (97, "ribbon2", "Ribbon banner with center area above ribbon ends")
+    """Ribbon banner with center area above ribbon ends"""
+
+    U_TURN_ARROW = (42, "uturnArrow", "Block arrow forming a U shape")
+    """Block arrow forming a U shape"""
+
+    VERTICAL_SCROLL = (101, "verticalScroll", "Vertical scroll")
+    """Vertical scroll"""
+
+    WAVE = (103, "wave", "Wave")
+    """Wave"""
+
+
+MSO_SHAPE = MSO_AUTO_SHAPE_TYPE
+
+
+class MSO_CONNECTOR_TYPE(BaseXmlEnum):
+    """
+    Specifies a type of connector.
+
+    Alias: ``MSO_CONNECTOR``
+
+    Example::
+
+        from pptx.enum.shapes import MSO_CONNECTOR
+        from pptx.util import Cm
+
+        shapes = prs.slides[0].shapes
+        connector = shapes.add_connector(
+            MSO_CONNECTOR.STRAIGHT, Cm(2), Cm(2), Cm(10), Cm(10)
+        )
+        assert connector.left.cm == 2
+
+    MS API Name: `MsoConnectorType`
+
+    http://msdn.microsoft.com/en-us/library/office/ff860918.aspx
+    """
+
+    CURVE = (3, "curvedConnector3", "Curved connector.")
+    """Curved connector."""
+
+    ELBOW = (2, "bentConnector3", "Elbow connector.")
+    """Elbow connector."""
+
+    STRAIGHT = (1, "line", "Straight line connector.")
+    """Straight line connector."""
+
+    MIXED = (-2, "", "Return value only; indicates a combination of other states.")
+    """Return value only; indicates a combination of other states."""
+
+
+MSO_CONNECTOR = MSO_CONNECTOR_TYPE
+
+
+class MSO_SHAPE_TYPE(BaseEnum):
+    """Specifies the type of a shape, more specifically than the five base types.
+
+    Alias: ``MSO``
+
+    Example::
+
+        from pptx.enum.shapes import MSO_SHAPE_TYPE
+
+        assert shape.type == MSO_SHAPE_TYPE.PICTURE
+
+    MS API Name: `MsoShapeType`
+
+    http://msdn.microsoft.com/en-us/library/office/ff860759(v=office.15).aspx
+    """
+
+    AUTO_SHAPE = (1, "AutoShape")
+    """AutoShape"""
+
+    CALLOUT = (2, "Callout shape")
+    """Callout shape"""
+
+    CANVAS = (20, "Drawing canvas")
+    """Drawing canvas"""
+
+    CHART = (3, "Chart, e.g. pie chart, bar chart")
+    """Chart, e.g. pie chart, bar chart"""
+
+    COMMENT = (4, "Comment")
+    """Comment"""
+
+    DIAGRAM = (21, "Diagram")
+    """Diagram"""
+
+    EMBEDDED_OLE_OBJECT = (7, "Embedded OLE object")
+    """Embedded OLE object"""
+
+    FORM_CONTROL = (8, "Form control")
+    """Form control"""
+
+    FREEFORM = (5, "Freeform")
+    """Freeform"""
+
+    GROUP = (6, "Group shape")
+    """Group shape"""
+
+    IGX_GRAPHIC = (24, "SmartArt graphic")
+    """SmartArt graphic"""
+
+    INK = (22, "Ink")
+    """Ink"""
+
+    INK_COMMENT = (23, "Ink Comment")
+    """Ink Comment"""
+
+    LINE = (9, "Line")
+    """Line"""
+
+    LINKED_OLE_OBJECT = (10, "Linked OLE object")
+    """Linked OLE object"""
+
+    LINKED_PICTURE = (11, "Linked picture")
+    """Linked picture"""
+
+    MEDIA = (16, "Media")
+    """Media"""
+
+    OLE_CONTROL_OBJECT = (12, "OLE control object")
+    """OLE control object"""
+
+    PICTURE = (13, "Picture")
+    """Picture"""
+
+    PLACEHOLDER = (14, "Placeholder")
+    """Placeholder"""
+
+    SCRIPT_ANCHOR = (18, "Script anchor")
+    """Script anchor"""
+
+    TABLE = (19, "Table")
+    """Table"""
+
+    TEXT_BOX = (17, "Text box")
+    """Text box"""
+
+    TEXT_EFFECT = (15, "Text effect")
+    """Text effect"""
+
+    WEB_VIDEO = (26, "Web video")
+    """Web video"""
+
+    MIXED = (-2, "Multiple shape types (read-only).")
+    """Multiple shape types (read-only)."""
+
+
+MSO = MSO_SHAPE_TYPE
+
+
+class PP_MEDIA_TYPE(BaseEnum):
+    """Indicates the OLE media type.
+
+    Example::
+
+        from pptx.enum.shapes import PP_MEDIA_TYPE
+
+        movie = slide.shapes[0]
+        assert movie.media_type == PP_MEDIA_TYPE.MOVIE
+
+    MS API Name: `PpMediaType`
+
+    https://msdn.microsoft.com/en-us/library/office/ff746008.aspx
+    """
+
+    MOVIE = (3, "Video media such as MP4.")
+    """Video media such as MP4."""
+
+    OTHER = (1, "Other media types")
+    """Other media types"""
+
+    SOUND = (1, "Audio media such as MP3.")
+    """Audio media such as MP3."""
+
+    MIXED = (
+        -2,
+        "Return value only; indicates multiple media types, typically for a collection of shapes."
+        " May not be applicable in python-pptx.",
+    )
+    """Return value only; indicates multiple media types.
+
+    Typically for a collection of shapes. May not be applicable in python-pptx.
+    """
+
+
+class PP_PLACEHOLDER_TYPE(BaseXmlEnum):
+    """Specifies one of the 18 distinct types of placeholder.
+
+    Alias: ``PP_PLACEHOLDER``
+
+    Example::
+
+        from pptx.enum.shapes import PP_PLACEHOLDER
+
+        placeholder = slide.placeholders[0]
+        assert placeholder.type == PP_PLACEHOLDER.TITLE
+
+    MS API name: `PpPlaceholderType`
+
+    http://msdn.microsoft.com/en-us/library/office/ff860759(v=office.15 ").aspx"
+    """
+
+    BITMAP = (9, "clipArt", "Clip art placeholder")
+    """Clip art placeholder"""
+
+    BODY = (2, "body", "Body")
+    """Body"""
+
+    CENTER_TITLE = (3, "ctrTitle", "Center Title")
+    """Center Title"""
+
+    CHART = (8, "chart", "Chart")
+    """Chart"""
+
+    DATE = (16, "dt", "Date")
+    """Date"""
+
+    FOOTER = (15, "ftr", "Footer")
+    """Footer"""
+
+    HEADER = (14, "hdr", "Header")
+    """Header"""
+
+    MEDIA_CLIP = (10, "media", "Media Clip")
+    """Media Clip"""
+
+    OBJECT = (7, "obj", "Object")
+    """Object"""
+
+    ORG_CHART = (11, "dgm", "SmartArt placeholder. Organization chart is a legacy name.")
+    """SmartArt placeholder. Organization chart is a legacy name."""
+
+    PICTURE = (18, "pic", "Picture")
+    """Picture"""
+
+    SLIDE_IMAGE = (101, "sldImg", "Slide Image")
+    """Slide Image"""
+
+    SLIDE_NUMBER = (13, "sldNum", "Slide Number")
+    """Slide Number"""
+
+    SUBTITLE = (4, "subTitle", "Subtitle")
+    """Subtitle"""
+
+    TABLE = (12, "tbl", "Table")
+    """Table"""
+
+    TITLE = (1, "title", "Title")
+    """Title"""
+
+    VERTICAL_BODY = (6, "", "Vertical Body (read-only).")
+    """Vertical Body (read-only)."""
+
+    VERTICAL_OBJECT = (17, "", "Vertical Object (read-only).")
+    """Vertical Object (read-only)."""
+
+    VERTICAL_TITLE = (5, "", "Vertical Title (read-only).")
+    """Vertical Title (read-only)."""
+
+    MIXED = (-2, "", "Return value only; multiple placeholders of differing types.")
+    """Return value only; multiple placeholders of differing types."""
+
+
+PP_PLACEHOLDER = PP_PLACEHOLDER_TYPE
+
+
+class PROG_ID(enum.Enum):
+    """One-off Enum-like object for progId values.
+
+    Indicates the type of an OLE object in terms of the program used to open it.
+
+    A member of this enumeration can be used in a `SlideShapes.add_ole_object()` call to
+    specify a Microsoft Office file-type (Excel, PowerPoint, or Word), which will
+    then not require several of the arguments required to embed other object types.
+
+    Example::
+
+        from pptx.enum.shapes import PROG_ID
+        from pptx.util import Inches
+
+        embedded_xlsx_shape = slide.shapes.add_ole_object(
+            "workbook.xlsx", PROG_ID.XLSX, left=Inches(1), top=Inches(1)
+        )
+        assert embedded_xlsx_shape.ole_format.prog_id == "Excel.Sheet.12"
+    """
+
+    _progId: str
+    _icon_filename: str
+    _width: int
+    _height: int
+
+    def __new__(cls, value: str, progId: str, icon_filename: str, width: int, height: int):
+        self = object.__new__(cls)
+        self._value_ = value
+        self._progId = progId
+        self._icon_filename = icon_filename
+        self._width = width
+        self._height = height
+        return self
+
+    @property
+    def height(self):
+        return self._height
+
+    @property
+    def icon_filename(self):
+        return self._icon_filename
+
+    @property
+    def progId(self):
+        return self._progId
+
+    @property
+    def width(self):
+        return self._width
+
+    DOCX = ("DOCX", "Word.Document.12", "docx-icon.emf", 965200, 609600)
+    """`progId` for an embedded Word 2007+ (.docx) document."""
+
+    PPTX = ("PPTX", "PowerPoint.Show.12", "pptx-icon.emf", 965200, 609600)
+    """`progId` for an embedded PowerPoint 2007+ (.pptx) document."""
+
+    XLSX = ("XLSX", "Excel.Sheet.12", "xlsx-icon.emf", 965200, 609600)
+    """`progId` for an embedded Excel 2007+ (.xlsx) document."""
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/text.py b/.venv/lib/python3.12/site-packages/pptx/enum/text.py
new file mode 100644
index 00000000..db266a3c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/text.py
@@ -0,0 +1,230 @@
+"""Enumerations used by text and related objects."""
+
+from __future__ import annotations
+
+from pptx.enum.base import BaseEnum, BaseXmlEnum
+
+
+class MSO_AUTO_SIZE(BaseEnum):
+    """Determines the type of automatic sizing allowed.
+
+    The following names can be used to specify the automatic sizing behavior used to fit a shape's
+    text within the shape bounding box, for example::
+
+        from pptx.enum.text import MSO_AUTO_SIZE
+
+        shape.text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
+
+    The word-wrap setting of the text frame interacts with the auto-size setting to determine the
+    specific auto-sizing behavior.
+
+    Note that `TextFrame.auto_size` can also be set to |None|, which removes the auto size setting
+    altogether. This causes the setting to be inherited, either from the layout placeholder, in the
+    case of a placeholder shape, or from the theme.
+
+    MS API Name: `MsoAutoSize`
+
+    http://msdn.microsoft.com/en-us/library/office/ff865367(v=office.15).aspx
+    """
+
+    NONE = (
+        0,
+        "No automatic sizing of the shape or text will be done.\n\nText can freely extend beyond"
+        " the horizontal and vertical edges of the shape bounding box.",
+    )
+    """No automatic sizing of the shape or text will be done.
+
+    Text can freely extend beyond the horizontal and vertical edges of the shape bounding box.
+    """
+
+    SHAPE_TO_FIT_TEXT = (
+        1,
+        "The shape height and possibly width are adjusted to fit the text.\n\nNote this setting"
+        " interacts with the TextFrame.word_wrap property setting. If word wrap is turned on,"
+        " only the height of the shape will be adjusted; soft line breaks will be used to fit the"
+        " text horizontally.",
+    )
+    """The shape height and possibly width are adjusted to fit the text.
+
+    Note this setting interacts with the TextFrame.word_wrap property setting. If word wrap is
+    turned on, only the height of the shape will be adjusted; soft line breaks will be used to fit
+    the text horizontally.
+    """
+
+    TEXT_TO_FIT_SHAPE = (
+        2,
+        "The font size is reduced as necessary to fit the text within the shape.",
+    )
+    """The font size is reduced as necessary to fit the text within the shape."""
+
+    MIXED = (-2, "Return value only; indicates a combination of automatic sizing schemes are used.")
+    """Return value only; indicates a combination of automatic sizing schemes are used."""
+
+
+class MSO_TEXT_UNDERLINE_TYPE(BaseXmlEnum):
+    """
+    Indicates the type of underline for text. Used with
+    :attr:`.Font.underline` to specify the style of text underlining.
+
+    Alias: ``MSO_UNDERLINE``
+
+    Example::
+
+        from pptx.enum.text import MSO_UNDERLINE
+
+        run.font.underline = MSO_UNDERLINE.DOUBLE_LINE
+
+    MS API Name: `MsoTextUnderlineType`
+
+    http://msdn.microsoft.com/en-us/library/aa432699.aspx
+    """
+
+    NONE = (0, "none", "Specifies no underline.")
+    """Specifies no underline."""
+
+    DASH_HEAVY_LINE = (8, "dashHeavy", "Specifies a dash underline.")
+    """Specifies a dash underline."""
+
+    DASH_LINE = (7, "dash", "Specifies a dash line underline.")
+    """Specifies a dash line underline."""
+
+    DASH_LONG_HEAVY_LINE = (10, "dashLongHeavy", "Specifies a long heavy line underline.")
+    """Specifies a long heavy line underline."""
+
+    DASH_LONG_LINE = (9, "dashLong", "Specifies a dashed long line underline.")
+    """Specifies a dashed long line underline."""
+
+    DOT_DASH_HEAVY_LINE = (12, "dotDashHeavy", "Specifies a dot dash heavy line underline.")
+    """Specifies a dot dash heavy line underline."""
+
+    DOT_DASH_LINE = (11, "dotDash", "Specifies a dot dash line underline.")
+    """Specifies a dot dash line underline."""
+
+    DOT_DOT_DASH_HEAVY_LINE = (
+        14,
+        "dotDotDashHeavy",
+        "Specifies a dot dot dash heavy line underline.",
+    )
+    """Specifies a dot dot dash heavy line underline."""
+
+    DOT_DOT_DASH_LINE = (13, "dotDotDash", "Specifies a dot dot dash line underline.")
+    """Specifies a dot dot dash line underline."""
+
+    DOTTED_HEAVY_LINE = (6, "dottedHeavy", "Specifies a dotted heavy line underline.")
+    """Specifies a dotted heavy line underline."""
+
+    DOTTED_LINE = (5, "dotted", "Specifies a dotted line underline.")
+    """Specifies a dotted line underline."""
+
+    DOUBLE_LINE = (3, "dbl", "Specifies a double line underline.")
+    """Specifies a double line underline."""
+
+    HEAVY_LINE = (4, "heavy", "Specifies a heavy line underline.")
+    """Specifies a heavy line underline."""
+
+    SINGLE_LINE = (2, "sng", "Specifies a single line underline.")
+    """Specifies a single line underline."""
+
+    WAVY_DOUBLE_LINE = (17, "wavyDbl", "Specifies a wavy double line underline.")
+    """Specifies a wavy double line underline."""
+
+    WAVY_HEAVY_LINE = (16, "wavyHeavy", "Specifies a wavy heavy line underline.")
+    """Specifies a wavy heavy line underline."""
+
+    WAVY_LINE = (15, "wavy", "Specifies a wavy line underline.")
+    """Specifies a wavy line underline."""
+
+    WORDS = (1, "words", "Specifies underlining words.")
+    """Specifies underlining words."""
+
+    MIXED = (-2, "", "Specifies a mix of underline types (read-only).")
+    """Specifies a mix of underline types (read-only)."""
+
+
+MSO_UNDERLINE = MSO_TEXT_UNDERLINE_TYPE
+
+
+class MSO_VERTICAL_ANCHOR(BaseXmlEnum):
+    """Specifies the vertical alignment of text in a text frame.
+
+    Used with the `.vertical_anchor` property of the |TextFrame| object. Note that the
+    `vertical_anchor` property can also have the value None, indicating there is no directly
+    specified vertical anchor setting and its effective value is inherited from its placeholder if
+    it has one or from the theme. |None| may also be assigned to remove an explicitly specified
+    vertical anchor setting.
+
+    MS API Name: `MsoVerticalAnchor`
+
+    http://msdn.microsoft.com/en-us/library/office/ff865255.aspx
+    """
+
+    TOP = (1, "t", "Aligns text to top of text frame")
+    """Aligns text to top of text frame"""
+
+    MIDDLE = (3, "ctr", "Centers text vertically")
+    """Centers text vertically"""
+
+    BOTTOM = (4, "b", "Aligns text to bottom of text frame")
+    """Aligns text to bottom of text frame"""
+
+    MIXED = (-2, "", "Return value only; indicates a combination of the other states.")
+    """Return value only; indicates a combination of the other states."""
+
+
+MSO_ANCHOR = MSO_VERTICAL_ANCHOR
+
+
+class PP_PARAGRAPH_ALIGNMENT(BaseXmlEnum):
+    """Specifies the horizontal alignment for one or more paragraphs.
+
+    Alias: `PP_ALIGN`
+
+    Example::
+
+        from pptx.enum.text import PP_ALIGN
+
+        shape.paragraphs[0].alignment = PP_ALIGN.CENTER
+
+    MS API Name: `PpParagraphAlignment`
+
+    http://msdn.microsoft.com/en-us/library/office/ff745375(v=office.15).aspx
+    """
+
+    CENTER = (2, "ctr", "Center align")
+    """Center align"""
+
+    DISTRIBUTE = (
+        5,
+        "dist",
+        "Evenly distributes e.g. Japanese characters from left to right within a line",
+    )
+    """Evenly distributes e.g. Japanese characters from left to right within a line"""
+
+    JUSTIFY = (
+        4,
+        "just",
+        "Justified, i.e. each line both begins and ends at the margin.\n\nSpacing between words"
+        " is adjusted such that the line exactly fills the width of the paragraph.",
+    )
+    """Justified, i.e. each line both begins and ends at the margin.
+
+    Spacing between words is adjusted such that the line exactly fills the width of the paragraph.
+    """
+
+    JUSTIFY_LOW = (7, "justLow", "Justify using a small amount of space between words.")
+    """Justify using a small amount of space between words."""
+
+    LEFT = (1, "l", "Left aligned")
+    """Left aligned"""
+
+    RIGHT = (3, "r", "Right aligned")
+    """Right aligned"""
+
+    THAI_DISTRIBUTE = (6, "thaiDist", "Thai distributed")
+    """Thai distributed"""
+
+    MIXED = (-2, "", "Multiple alignments are present in a set of paragraphs (read-only).")
+    """Multiple alignments are present in a set of paragraphs (read-only)."""
+
+
+PP_ALIGN = PP_PARAGRAPH_ALIGNMENT
diff --git a/.venv/lib/python3.12/site-packages/pptx/exc.py b/.venv/lib/python3.12/site-packages/pptx/exc.py
new file mode 100644
index 00000000..0a1e03b8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/exc.py
@@ -0,0 +1,23 @@
+"""Exceptions used with python-pptx.
+
+The base exception class is PythonPptxError.
+"""
+
+from __future__ import annotations
+
+
+class PythonPptxError(Exception):
+    """Generic error class."""
+
+
+class PackageNotFoundError(PythonPptxError):
+    """
+    Raised when a package cannot be found at the specified path.
+    """
+
+
+class InvalidXmlError(PythonPptxError):
+    """
+    Raised when a value is encountered in the XML that is not valid according
+    to the schema.
+    """
diff --git a/.venv/lib/python3.12/site-packages/pptx/media.py b/.venv/lib/python3.12/site-packages/pptx/media.py
new file mode 100644
index 00000000..7aaf47ca
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/media.py
@@ -0,0 +1,197 @@
+"""Objects related to images, audio, and video."""
+
+from __future__ import annotations
+
+import base64
+import hashlib
+import os
+from typing import IO
+
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.util import lazyproperty
+
+
+class Video(object):
+    """Immutable value object representing a video such as MP4."""
+
+    def __init__(self, blob: bytes, mime_type: str | None, filename: str | None):
+        super(Video, self).__init__()
+        self._blob = blob
+        self._mime_type = mime_type
+        self._filename = filename
+
+    @classmethod
+    def from_blob(cls, blob: bytes, mime_type: str | None, filename: str | None = None):
+        """Return a new |Video| object loaded from image binary in *blob*."""
+        return cls(blob, mime_type, filename)
+
+    @classmethod
+    def from_path_or_file_like(cls, movie_file: str | IO[bytes], mime_type: str | None) -> Video:
+        """Return a new |Video| object containing video in *movie_file*.
+
+        *movie_file* can be either a path (string) or a file-like
+        (e.g. StringIO) object.
+        """
+        if isinstance(movie_file, str):
+            # treat movie_file as a path
+            with open(movie_file, "rb") as f:
+                blob = f.read()
+            filename = os.path.basename(movie_file)
+        else:
+            # assume movie_file is a file-like object
+            blob = movie_file.read()
+            filename = None
+
+        return cls.from_blob(blob, mime_type, filename)
+
+    @property
+    def blob(self):
+        """The bytestream of the media "file"."""
+        return self._blob
+
+    @property
+    def content_type(self):
+        """MIME-type of this media, e.g. `'video/mp4'`."""
+        return self._mime_type
+
+    @property
+    def ext(self):
+        """Return the file extension for this video, e.g. 'mp4'.
+
+        The extension is that from the actual filename if known. Otherwise
+        it is the lowercase canonical extension for the video's MIME type.
+        'vid' is used if the MIME type is 'video/unknown'.
+        """
+        if self._filename:
+            return os.path.splitext(self._filename)[1].lstrip(".")
+        return {
+            CT.ASF: "asf",
+            CT.AVI: "avi",
+            CT.MOV: "mov",
+            CT.MP4: "mp4",
+            CT.MPG: "mpg",
+            CT.MS_VIDEO: "avi",
+            CT.SWF: "swf",
+            CT.WMV: "wmv",
+            CT.X_MS_VIDEO: "avi",
+        }.get(self._mime_type, "vid")
+
+    @property
+    def filename(self) -> str:
+        """Return a filename.ext string appropriate to this video.
+
+        The base filename from the original path is used if this image was
+        loaded from the filesystem. If no filename is available, such as when
+        the video object is created from an in-memory stream, the string
+        'movie.{ext}' is used where 'ext' is suitable to the video format,
+        such as 'mp4'.
+        """
+        if self._filename is not None:
+            return self._filename
+        return "movie.%s" % self.ext
+
+    @lazyproperty
+    def sha1(self):
+        """The SHA1 hash digest for the binary "file" of this video.
+
+        Example: `'1be010ea47803b00e140b852765cdf84f491da47'`
+        """
+        return hashlib.sha1(self._blob).hexdigest()
+
+
+SPEAKER_IMAGE_BYTES = base64.b64decode(
+    "iVBORw0KGgoAAAANSUhEUgAAAHgAAAA3CAYAAADHao5rAAAACXBIWXMAAAsTAAALEwEAmpw"
+    "YAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUh"
+    "UIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74"
+    "Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz"
+    "/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEB"
+    "GAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVo"
+    "pFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8"
+    "lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wA"
+    "AKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qI"
+    "l7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X"
+    "48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5Em"
+    "ozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgD"
+    "gGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/x"
+    "gNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKL"
+    "yBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h"
+    "1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP"
+    "2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0I"
+    "gYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iE"
+    "PENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG"
+    "+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1"
+    "mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAc"
+    "YZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81"
+    "XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgs"
+    "V/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx"
+    "+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5"
+    "Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+h"
+    "x9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGj"
+    "UYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb"
+    "15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZ"
+    "nw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFD"
+    "pWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbx"
+    "t3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvf"
+    "rH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+"
+    "F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrS"
+    "FoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6R"
+    "JZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3i"
+    "C+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtG"
+    "I2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQq"
+    "ohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKO"
+    "ZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2"
+    "Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhT"
+    "bF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319k"
+    "XbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/"
+    "T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr"
+    "60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRpt"
+    "TmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752"
+    "PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca"
+    "7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf"
+    "9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L"
+    "96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV"
+    "70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAe"
+    "iUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAACJ5JREFUeNrsm19oW9cdx7/n"
+    "/tO9uleS9ceWZMdO68Su0jiEmNCGLVsDg+J16xhkYx0UM1ayvS19SQZ7WfK6PWR7Gax0S9n"
+    "24JGFlbGNPIRgEkKWZC1u6rizHMXxv1iWJdnWvbqS7p9z9uDokqzs1bWd84GDQEigez465/"
+    "x+v3MOYYyBs3shXDAXzOGCOVwwhwvmcMEcLpjDBXPBHC6YwwVzuOAt4dGjRy/MzMwMmqZpS"
+    "JLkZbPZYn9//8NkMlnmgncwt27d+tKlS5e+W6lUEolEYjQUCoExBsuy4Lru+319fXMjIyNX"
+    "jh49+m8ueAfRarXUCxcuvHvv3r3DR44ceavRaMCyLDDGIAgCFEVBOBwGIQSFQuH9PXv2PD5"
+    "16tRvu7u7H3PB2xzXdZXz58//vFKp/Gzfvn1YXFyEYRgwDAOSJEEQBDDG4LouPM+DpmnwPA"
+    "/5fP73o6Ojf3zttdfGueBtzAcffPCD8fHxi0NDQ1hZWUE6nYYoiiCEQBCEZ14JIfA8D77vA"
+    "wAmJib+cPLkyctvvvnm33ZLf0i7Se7s7Gz/tWvXvpbL5bC+vo5sNgtFUTb/yU/EthshBAAg"
+    "yzIAoNls4tChQ6OXL1+GKIreG2+88U8ueJsxPj5+QlXVt0OhEGRZhqIoEEXxGbFPC6aUBuJ"
+    "VVYVlWThw4MDo2NiYkMlkisPDwx/v9D4Rdotc0zSj9+7dO5RMJgEA4XAYkiRBkqRA9tNNlu"
+    "VArKIokCQJ8Xgc8Xgc+/fvf/u999778fr6egcXvE0ol8spx3HeNQwDoihClmXIsgxRFCFJ0"
+    "ucEE0KgqipCodAz0pPJJFKpFGKx2I8uXrz4Qy54m1CpVBK+70MUxUDW0yO1Lbz9XnuUPz26"
+    "25/JZDLo7OzExMTE4cnJySG+Bm8DarVa1Pd9EEIgy3IwPYuiCFEANFWEIGw2x6PQNA2qqqK"
+    "dRTDGwBgDpTSQXKvVRj/88MOZoaGhSS74C8a27XA75ZEk6YloBYoswHQNfPy4Fx4JoTdmYj"
+    "BRhSozsP+ZwCil8DwPlFIkk0kkEglMTEwM5PP5wcHBwTwX/AXhOI5i23bY930wxqAoymbVi"
+    "jA0/DD+mj8C048iqjMUHYbFRg0n+uaQ0FzQJ5IdxwGlNFi/AaCzsxPpdHr0+vXrN3aq4F2x"
+    "Bs/Pz/eFQqE/t0egIAjQdR2SQDFVzmKlHoEme1BEH3uyDJFsHFdnX4SLECRRhOM4EAQBhmF"
+    "A1/UgvUomk4jH4/j0008PeZ4nccFbTKPRCH/00UdHi8Viph1gNZtNUEqhKAp0XUfRNMAYQC"
+    "ng+0DNAhp1H4s1A3fmkwhrMmKxGGKxGFRVDdZvWZYRiUQQjUZRq9V+Mjc39wIXvIWsra0lx"
+    "sfHTzx48OAuY+yGKIpQVRX1eh2O4wQBFmMUjsvQ8oCWC5RWGWZmKVyX4e58DLarIBzWnomy"
+    "23mxoiiIRqPQdR2FQqGfr8FbhOu6SrFYzJim+Y9mswnHcSCKImKxGFZWVmBZFgRBgCQTpPU"
+    "6LNOCquhgjMERAUIAnzJUWgrmqzJeTclouRSUUrQj8XbQZRgGwuEwFhYW+vgI3iJarZbS2d"
+    "l5v1Qqwfd9uK4LAEgmk2g0GlhfX99cV0UFL2dNCLSGmtlEvUFg2oBpA5YNmHWCikmgKNLnc"
+    "uH2VN2udFWr1QQXvEV4nicxxlCv19FsNoOtv0QiAUVRsLq6ikqlAtel2JvycOxFCyulFZh1"
+    "CqtBNuXaBPV6EyHRhyAIaLVaQST99MZEWzQPsrZ2BKu2bYMQgmq1Cs/z4DgOwuEwMpkMSqU"
+    "SlpeXwQBoegTfe9XEgayDhaUHWNuoo1YHKus1hEgVuR4GLRxFPB6HrusQRRGU0uBwQLsAsl"
+    "O3VXdskNVsNtHR0YHl5WU0Gg24rgvGGLLZLCilWFpawuLiIggR0d1l4KffMPH6yz5a5iNUV"
+    "qcR8ot4+9UKcv0pCOLmlBwKhaBpGjRNC1Kl9p9H07QmD7K2CFVVm2tra0in07AsCysrK0il"
+    "UnBdF5FIBD09PVhYWMDDhw+RyWTQ07MH/X0qznyripNFhnKNYU8SeOVwL6Idm9+jdFNm+yB"
+    "AO9CybRumaeLw4cOPueAtIhKJ1Hzf/4phGDe6urowOTmJY8eOBbXk9jRdKBQQiUTQ0dGBeD"
+    "yOSCyB4z0aZEmErKhoNDfX7v83BXueh2q1ilKpBF7J2sofLQi0p6dn0bZtDAwMYGNjA1NTU"
+    "/B9H61WC4qioK+vDxsbG5iZmcHdu3dh2zZEUUTL8QFBAYgIQSAolUrBsR1KadCe5NpYXl6G"
+    "qqp/yuVy/+GCt5C9e/fOy7L8dVmWkcvlMD09jc8++yxYM3VdR29vL+bn53H//n3cunULtm0"
+    "HGwqu6wY169XVVTiOE4hmjKHRaGB2dhaFQgHHjx+/EQ6H7Z3YT+K5c+d2pGBCCOvu7n48NT"
+    "X1gFL6bcMwUCgUUCqVgkpUu/Q4NzeHZrOJVquFSCQCwzCCIzuSJMH3fViWBUII2ulXoVDAn"
+    "Tt3UC6X/3L27NlfaJrW4IK3GFmW3ZdeeilvWda/HMe5u2/fvhFFUWCa5m/279///vDw8K8y"
+    "mczfCSH56enpr5bL5UC0IAhPSpksCKparRbK5TJmZmZw+/ZtfPLJJ1fPnDnzy506PQO76Nh"
+    "stVpNFIvFDGNMSKfTxVQqVX66tHnp0qXvjI2NfZ8Q8s3e3l709/ejq6sL0WgUoVAIruuiVq"
+    "thaWkJ09PTWFpaunL69Olfj4yMXNnJ/fJcXT7L5/ODY2Njb928efPLoii+3t5IkGUZnufBN"
+    "E2sra1dHR4e/vidd9753cDAQH6nP/Nzebtwdna2//bt269MTk4eKpVKXZ7nSbquW7lcbvrE"
+    "iRPjBw8enNwtz/rcXx9ljAlPypJ0Nz4fvx+8y+GCuWAOF8zhgjlcMIcL5nDBHC6YC+ZwwRw"
+    "umLMN+O8AX65uqCMleo4AAAAASUVORK5CYII="
+)
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/__init__.py b/.venv/lib/python3.12/site-packages/pptx/opc/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/constants.py b/.venv/lib/python3.12/site-packages/pptx/opc/constants.py
new file mode 100644
index 00000000..e1b08a93
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/constants.py
@@ -0,0 +1,331 @@
+"""Constant values related to the Open Packaging Convention.
+
+In particular, this includes content (MIME) types and relationship types.
+"""
+
+from __future__ import annotations
+
+
+class CONTENT_TYPE:
+    """Content type URIs (like MIME-types) that specify a part's format."""
+
+    ASF = "video/x-ms-asf"
+    AVI = "video/avi"
+    BMP = "image/bmp"
+    DML_CHART = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
+    DML_CHARTSHAPES = "application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml"
+    DML_DIAGRAM_COLORS = "application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml"
+    DML_DIAGRAM_DATA = "application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml"
+    DML_DIAGRAM_DRAWING = "application/vnd.ms-office.drawingml.diagramDrawing+xml"
+    DML_DIAGRAM_LAYOUT = "application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml"
+    DML_DIAGRAM_STYLE = "application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml"
+    GIF = "image/gif"
+    INK = "application/inkml+xml"
+    JPEG = "image/jpeg"
+    MOV = "video/quicktime"
+    MP4 = "video/mp4"
+    MPG = "video/mpeg"
+    MS_PHOTO = "image/vnd.ms-photo"
+    MS_VIDEO = "video/msvideo"
+    OFC_CHART_COLORS = "application/vnd.ms-office.chartcolorstyle+xml"
+    OFC_CHART_EX = "application/vnd.ms-office.chartex+xml"
+    OFC_CHART_STYLE = "application/vnd.ms-office.chartstyle+xml"
+    OFC_CUSTOM_PROPERTIES = "application/vnd.openxmlformats-officedocument.custom-properties+xml"
+    OFC_CUSTOM_XML_PROPERTIES = (
+        "application/vnd.openxmlformats-officedocument.customXmlProperties+xml"
+    )
+    OFC_DRAWING = "application/vnd.openxmlformats-officedocument.drawing+xml"
+    OFC_EXTENDED_PROPERTIES = (
+        "application/vnd.openxmlformats-officedocument.extended-properties+xml"
+    )
+    OFC_OLE_OBJECT = "application/vnd.openxmlformats-officedocument.oleObject"
+    OFC_PACKAGE = "application/vnd.openxmlformats-officedocument.package"
+    OFC_THEME = "application/vnd.openxmlformats-officedocument.theme+xml"
+    OFC_THEME_OVERRIDE = "application/vnd.openxmlformats-officedocument.themeOverride+xml"
+    OFC_VML_DRAWING = "application/vnd.openxmlformats-officedocument.vmlDrawing"
+    OPC_CORE_PROPERTIES = "application/vnd.openxmlformats-package.core-properties+xml"
+    OPC_DIGITAL_SIGNATURE_CERTIFICATE = (
+        "application/vnd.openxmlformats-package.digital-signature-certificate"
+    )
+    OPC_DIGITAL_SIGNATURE_ORIGIN = "application/vnd.openxmlformats-package.digital-signature-origin"
+    OPC_DIGITAL_SIGNATURE_XMLSIGNATURE = (
+        "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml"
+    )
+    OPC_RELATIONSHIPS = "application/vnd.openxmlformats-package.relationships+xml"
+    PML_COMMENTS = "application/vnd.openxmlformats-officedocument.presentationml.comments+xml"
+    PML_COMMENT_AUTHORS = (
+        "application/vnd.openxmlformats-officedocument.presentationml.commentAuthors+xml"
+    )
+    PML_HANDOUT_MASTER = (
+        "application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml"
+    )
+    PML_NOTES_MASTER = (
+        "application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml"
+    )
+    PML_NOTES_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml"
+    PML_PRESENTATION = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
+    PML_PRESENTATION_MAIN = (
+        "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"
+    )
+    PML_PRES_MACRO_MAIN = "application/vnd.ms-powerpoint.presentation.macroEnabled.main+xml"
+    PML_PRES_PROPS = "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml"
+    PML_PRINTER_SETTINGS = (
+        "application/vnd.openxmlformats-officedocument.presentationml.printerSettings"
+    )
+    PML_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml"
+    PML_SLIDESHOW_MAIN = (
+        "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml"
+    )
+    PML_SLIDE_LAYOUT = (
+        "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"
+    )
+    PML_SLIDE_MASTER = (
+        "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"
+    )
+    PML_SLIDE_UPDATE_INFO = (
+        "application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo+xml"
+    )
+    PML_TABLE_STYLES = (
+        "application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml"
+    )
+    PML_TAGS = "application/vnd.openxmlformats-officedocument.presentationml.tags+xml"
+    PML_TEMPLATE_MAIN = (
+        "application/vnd.openxmlformats-officedocument.presentationml.template.main+xml"
+    )
+    PML_VIEW_PROPS = "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml"
+    PNG = "image/png"
+    SML_CALC_CHAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml"
+    SML_CHARTSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"
+    SML_COMMENTS = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"
+    SML_CONNECTIONS = "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml"
+    SML_CUSTOM_PROPERTY = (
+        "application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty"
+    )
+    SML_DIALOGSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml"
+    SML_EXTERNAL_LINK = (
+        "application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml"
+    )
+    SML_PIVOT_CACHE_DEFINITION = (
+        "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"
+    )
+    SML_PIVOT_CACHE_RECORDS = (
+        "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"
+    )
+    SML_PIVOT_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
+    SML_PRINTER_SETTINGS = (
+        "application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings"
+    )
+    SML_QUERY_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml"
+    SML_REVISION_HEADERS = (
+        "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+xml"
+    )
+    SML_REVISION_LOG = "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml"
+    SML_SHARED_STRINGS = (
+        "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
+    )
+    SML_SHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+    SML_SHEET_MAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
+    SML_SHEET_METADATA = (
+        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml"
+    )
+    SML_STYLES = "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"
+    SML_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"
+    SML_TABLE_SINGLE_CELLS = (
+        "application/vnd.openxmlformats-officedocument.spreadsheetml.tableSingleCells+xml"
+    )
+    SML_TEMPLATE_MAIN = (
+        "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml"
+    )
+    SML_USER_NAMES = "application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml"
+    SML_VOLATILE_DEPENDENCIES = (
+        "application/vnd.openxmlformats-officedocument.spreadsheetml.volatileDependencies+xml"
+    )
+    SML_WORKSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
+    SWF = "application/x-shockwave-flash"
+    TIFF = "image/tiff"
+    VIDEO = "video/unknown"
+    WML_COMMENTS = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml"
+    WML_DOCUMENT = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+    WML_DOCUMENT_GLOSSARY = (
+        "application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml"
+    )
+    WML_DOCUMENT_MAIN = (
+        "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"
+    )
+    WML_ENDNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml"
+    WML_FONT_TABLE = "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"
+    WML_FOOTER = "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"
+    WML_FOOTNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml"
+    WML_HEADER = "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"
+    WML_NUMBERING = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"
+    WML_PRINTER_SETTINGS = (
+        "application/vnd.openxmlformats-officedocument.wordprocessingml.printerSettings"
+    )
+    WML_SETTINGS = "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"
+    WML_STYLES = "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"
+    WML_WEB_SETTINGS = (
+        "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml"
+    )
+    WMV = "video/x-ms-wmv"
+    XML = "application/xml"
+    X_EMF = "image/x-emf"
+    X_FONTDATA = "application/x-fontdata"
+    X_FONT_TTF = "application/x-font-ttf"
+    X_MS_VIDEO = "video/x-msvideo"
+    X_WMF = "image/x-wmf"
+
+
+class NAMESPACE:
+    """Constant values for OPC XML namespaces"""
+
+    DML_WORDPROCESSING_DRAWING = (
+        "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
+    )
+    OFC_RELATIONSHIPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
+    OPC_RELATIONSHIPS = "http://schemas.openxmlformats.org/package/2006/relationships"
+    OPC_CONTENT_TYPES = "http://schemas.openxmlformats.org/package/2006/content-types"
+    WML_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
+
+
+class RELATIONSHIP_TARGET_MODE:
+    """Open XML relationship target modes"""
+
+    EXTERNAL = "External"
+    INTERNAL = "Internal"
+
+
+class RELATIONSHIP_TYPE:
+    AUDIO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio"
+    A_F_CHUNK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk"
+    CALC_CHAIN = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain"
+    CERTIFICATE = (
+        "http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu"
+        "re/certificate"
+    )
+    CHART = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
+    CHARTSHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"
+    CHART_COLOR_STYLE = "http://schemas.microsoft.com/office/2011/relationships/chartColorStyle"
+    CHART_USER_SHAPES = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartUserShapes"
+    )
+    COMMENTS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
+    COMMENT_AUTHORS = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentAuthors"
+    )
+    CONNECTIONS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/connections"
+    CONTROL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/control"
+    CORE_PROPERTIES = (
+        "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties"
+    )
+    CUSTOM_PROPERTIES = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties"
+    )
+    CUSTOM_PROPERTY = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customProperty"
+    )
+    CUSTOM_XML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml"
+    CUSTOM_XML_PROPS = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps"
+    )
+    DIAGRAM_COLORS = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors"
+    )
+    DIAGRAM_DATA = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData"
+    DIAGRAM_LAYOUT = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout"
+    )
+    DIAGRAM_QUICK_STYLE = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle"
+    )
+    DIALOGSHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet"
+    DRAWING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
+    ENDNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes"
+    EXTENDED_PROPERTIES = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"
+    )
+    EXTERNAL_LINK = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink"
+    )
+    FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font"
+    FONT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable"
+    FOOTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
+    FOOTNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes"
+    GLOSSARY_DOCUMENT = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/glossaryDocument"
+    )
+    HANDOUT_MASTER = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/handoutMaster"
+    )
+    HEADER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
+    HYPERLINK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
+    IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
+    MEDIA = "http://schemas.microsoft.com/office/2007/relationships/media"
+    NOTES_MASTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster"
+    NOTES_SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide"
+    NUMBERING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering"
+    OFFICE_DOCUMENT = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
+    )
+    OLE_OBJECT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject"
+    ORIGIN = "http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin"
+    PACKAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/package"
+    PIVOT_CACHE_DEFINITION = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCac"
+        "heDefinition"
+    )
+    PIVOT_CACHE_RECORDS = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/spreadsh"
+        "eetml/pivotCacheRecords"
+    )
+    PIVOT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"
+    PRES_PROPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps"
+    PRINTER_SETTINGS = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings"
+    )
+    QUERY_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/queryTable"
+    REVISION_HEADERS = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionHeaders"
+    )
+    REVISION_LOG = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionLog"
+    SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings"
+    SHARED_STRINGS = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
+    )
+    SHEET_METADATA = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata"
+    )
+    SIGNATURE = (
+        "http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu"
+        "re/signature"
+    )
+    SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"
+    SLIDE_LAYOUT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout"
+    SLIDE_MASTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster"
+    SLIDE_UPDATE_INFO = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideUpdateInfo"
+    )
+    STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"
+    TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table"
+    TABLE_SINGLE_CELLS = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableSingleCells"
+    )
+    TABLE_STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles"
+    TAGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags"
+    THEME = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"
+    THEME_OVERRIDE = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/themeOverride"
+    )
+    THUMBNAIL = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail"
+    USERNAMES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/usernames"
+    VIDEO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/video"
+    VIEW_PROPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps"
+    VML_DRAWING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"
+    VOLATILE_DEPENDENCIES = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/volatile"
+        "Dependencies"
+    )
+    WEB_SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings"
+    WORKSHEET_SOURCE = (
+        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheetSource"
+    )
+    XML_MAPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps"
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/oxml.py b/.venv/lib/python3.12/site-packages/pptx/opc/oxml.py
new file mode 100644
index 00000000..5dd902a5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/oxml.py
@@ -0,0 +1,188 @@
+"""OPC-local oxml module to handle OPC-local concerns like relationship parsing."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, cast
+
+from lxml import etree
+
+from pptx.opc.constants import NAMESPACE as NS
+from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM
+from pptx.oxml import parse_xml, register_element_cls
+from pptx.oxml.simpletypes import (
+    ST_ContentType,
+    ST_Extension,
+    ST_TargetMode,
+    XsdAnyUri,
+    XsdId,
+)
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrMore,
+)
+
+if TYPE_CHECKING:
+    from pptx.opc.packuri import PackURI
+
+nsmap = {
+    "ct": NS.OPC_CONTENT_TYPES,
+    "pr": NS.OPC_RELATIONSHIPS,
+    "r": NS.OFC_RELATIONSHIPS,
+}
+
+
+def oxml_to_encoded_bytes(
+    element: BaseOxmlElement,
+    encoding: str = "utf-8",
+    pretty_print: bool = False,
+    standalone: bool | None = None,
+) -> bytes:
+    return etree.tostring(
+        element, encoding=encoding, pretty_print=pretty_print, standalone=standalone
+    )
+
+
+def oxml_tostring(
+    elm: BaseOxmlElement,
+    encoding: str | None = None,
+    pretty_print: bool = False,
+    standalone: bool | None = None,
+):
+    return etree.tostring(elm, encoding=encoding, pretty_print=pretty_print, standalone=standalone)
+
+
+def serialize_part_xml(part_elm: BaseOxmlElement) -> bytes:
+    """Produce XML-file bytes for `part_elm`, suitable for writing directly to a `.xml` file.
+
+    Includes XML-declaration header.
+    """
+    return etree.tostring(part_elm, encoding="UTF-8", standalone=True)
+
+
+class CT_Default(BaseOxmlElement):
+    """`<Default>` element.
+
+    Specifies the default content type to be applied to a part with the specified extension.
+    """
+
+    extension: str = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "Extension", ST_Extension
+    )
+    contentType: str = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "ContentType", ST_ContentType
+    )
+
+
+class CT_Override(BaseOxmlElement):
+    """`<Override>` element.
+
+    Specifies the content type to be applied for a part with the specified partname.
+    """
+
+    partName: str = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "PartName", XsdAnyUri
+    )
+    contentType: str = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "ContentType", ST_ContentType
+    )
+
+
+class CT_Relationship(BaseOxmlElement):
+    """`<Relationship>` element.
+
+    Represents a single relationship from a source to a target part.
+    """
+
+    rId: str = RequiredAttribute("Id", XsdId)  # pyright: ignore[reportAssignmentType]
+    reltype: str = RequiredAttribute("Type", XsdAnyUri)  # pyright: ignore[reportAssignmentType]
+    target_ref: str = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "Target", XsdAnyUri
+    )
+    targetMode: str = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "TargetMode", ST_TargetMode, default=RTM.INTERNAL
+    )
+
+    @classmethod
+    def new(
+        cls, rId: str, reltype: str, target_ref: str, target_mode: str = RTM.INTERNAL
+    ) -> CT_Relationship:
+        """Return a new `<Relationship>` element.
+
+        `target_ref` is either a partname or a URI.
+        """
+        relationship = cast(CT_Relationship, parse_xml(f'<Relationship xmlns="{nsmap["pr"]}"/>'))
+        relationship.rId = rId
+        relationship.reltype = reltype
+        relationship.target_ref = target_ref
+        relationship.targetMode = target_mode
+        return relationship
+
+
+class CT_Relationships(BaseOxmlElement):
+    """`<Relationships>` element, the root element in a .rels file."""
+
+    relationship_lst: list[CT_Relationship]
+    _insert_relationship: Callable[[CT_Relationship], CT_Relationship]
+
+    relationship = ZeroOrMore("pr:Relationship")
+
+    def add_rel(
+        self, rId: str, reltype: str, target: str, is_external: bool = False
+    ) -> CT_Relationship:
+        """Add a child `<Relationship>` element with attributes set as specified."""
+        target_mode = RTM.EXTERNAL if is_external else RTM.INTERNAL
+        relationship = CT_Relationship.new(rId, reltype, target, target_mode)
+        return self._insert_relationship(relationship)
+
+    @classmethod
+    def new(cls) -> CT_Relationships:
+        """Return a new `<Relationships>` element."""
+        return cast(CT_Relationships, parse_xml(f'<Relationships xmlns="{nsmap["pr"]}"/>'))
+
+    @property
+    def xml_file_bytes(self) -> bytes:
+        """Return XML bytes, with XML-declaration, for this `<Relationships>` element.
+
+        Suitable for saving in a .rels stream, not pretty printed and with an XML declaration at
+        the top.
+        """
+        return oxml_to_encoded_bytes(self, encoding="UTF-8", standalone=True)
+
+
+class CT_Types(BaseOxmlElement):
+    """`<Types>` element.
+
+    The container element for Default and Override elements in [Content_Types].xml.
+    """
+
+    default_lst: list[CT_Default]
+    override_lst: list[CT_Override]
+
+    _add_default: Callable[..., CT_Default]
+    _add_override: Callable[..., CT_Override]
+
+    default = ZeroOrMore("ct:Default")
+    override = ZeroOrMore("ct:Override")
+
+    def add_default(self, ext: str, content_type: str) -> CT_Default:
+        """Add a child `<Default>` element with attributes set to parameter values."""
+        return self._add_default(extension=ext, contentType=content_type)
+
+    def add_override(self, partname: PackURI, content_type: str) -> CT_Override:
+        """Add a child `<Override>` element with attributes set to parameter values."""
+        return self._add_override(partName=partname, contentType=content_type)
+
+    @classmethod
+    def new(cls) -> CT_Types:
+        """Return a new `<Types>` element."""
+        return cast(CT_Types, parse_xml(f'<Types xmlns="{nsmap["ct"]}"/>'))
+
+
+register_element_cls("ct:Default", CT_Default)
+register_element_cls("ct:Override", CT_Override)
+register_element_cls("ct:Types", CT_Types)
+
+register_element_cls("pr:Relationship", CT_Relationship)
+register_element_cls("pr:Relationships", CT_Relationships)
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/package.py b/.venv/lib/python3.12/site-packages/pptx/opc/package.py
new file mode 100644
index 00000000..713759c5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/package.py
@@ -0,0 +1,762 @@
+"""Fundamental Open Packaging Convention (OPC) objects.
+
+The :mod:`pptx.packaging` module coheres around the concerns of reading and writing
+presentations to and from a .pptx file.
+"""
+
+from __future__ import annotations
+
+import collections
+from typing import IO, TYPE_CHECKING, DefaultDict, Iterator, Mapping, Set, cast
+
+from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.opc.oxml import CT_Relationships, serialize_part_xml
+from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI
+from pptx.opc.serialized import PackageReader, PackageWriter
+from pptx.opc.shared import CaseInsensitiveDict
+from pptx.oxml import parse_xml
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from typing_extensions import Self
+
+    from pptx.opc.oxml import CT_Relationship, CT_Types
+    from pptx.oxml.xmlchemy import BaseOxmlElement
+    from pptx.package import Package
+    from pptx.parts.presentation import PresentationPart
+
+
+class _RelatableMixin:
+    """Provide relationship methods required by both the package and each part."""
+
+    def part_related_by(self, reltype: str) -> Part:
+        """Return (single) part having relationship to this package of `reltype`.
+
+        Raises |KeyError| if no such relationship is found and |ValueError| if more than one such
+        relationship is found.
+        """
+        return self._rels.part_with_reltype(reltype)
+
+    def relate_to(self, target: Part | str, reltype: str, is_external: bool = False) -> str:
+        """Return rId key of relationship of `reltype` to `target`.
+
+        If such a relationship already exists, its rId is returned. Otherwise the relationship is
+        added and its new rId returned.
+        """
+        if isinstance(target, str):
+            assert is_external
+            return self._rels.get_or_add_ext_rel(reltype, target)
+
+        return self._rels.get_or_add(reltype, target)
+
+    def related_part(self, rId: str) -> Part:
+        """Return related |Part| subtype identified by `rId`."""
+        return self._rels[rId].target_part
+
+    def target_ref(self, rId: str) -> str:
+        """Return URL contained in target ref of relationship identified by `rId`."""
+        return self._rels[rId].target_ref
+
+    @lazyproperty
+    def _rels(self) -> _Relationships:
+        """|_Relationships| object containing relationships from this part to others."""
+        raise NotImplementedError(  # pragma: no cover
+            "`%s` must implement `.rels`" % type(self).__name__
+        )
+
+
+class OpcPackage(_RelatableMixin):
+    """Main API class for |python-opc|.
+
+    A new instance is constructed by calling the :meth:`open` classmethod with a path to a package
+    file or file-like object containing a package (.pptx file).
+    """
+
+    def __init__(self, pkg_file: str | IO[bytes]):
+        self._pkg_file = pkg_file
+
+    @classmethod
+    def open(cls, pkg_file: str | IO[bytes]) -> Self:
+        """Return an |OpcPackage| instance loaded with the contents of `pkg_file`."""
+        return cls(pkg_file)._load()
+
+    def drop_rel(self, rId: str) -> None:
+        """Remove relationship identified by `rId`."""
+        self._rels.pop(rId)
+
+    def iter_parts(self) -> Iterator[Part]:
+        """Generate exactly one reference to each part in the package."""
+        visited: Set[Part] = set()
+        for rel in self.iter_rels():
+            if rel.is_external:
+                continue
+            part = rel.target_part
+            if part in visited:
+                continue
+            yield part
+            visited.add(part)
+
+    def iter_rels(self) -> Iterator[_Relationship]:
+        """Generate exactly one reference to each relationship in package.
+
+        Performs a depth-first traversal of the rels graph.
+        """
+        visited: Set[Part] = set()
+
+        def walk_rels(rels: _Relationships) -> Iterator[_Relationship]:
+            for rel in rels.values():
+                yield rel
+                # --- external items can have no relationships ---
+                if rel.is_external:
+                    continue
+                # -- all relationships other than those for the package belong to a part. Once
+                # -- that part has been processed, processing it again would lead to the same
+                # -- relationships appearing more than once.
+                part = rel.target_part
+                if part in visited:
+                    continue
+                visited.add(part)
+                # --- recurse into relationships of each unvisited target-part ---
+                yield from walk_rels(part.rels)
+
+        yield from walk_rels(self._rels)
+
+    @property
+    def main_document_part(self) -> PresentationPart:
+        """Return |Part| subtype serving as the main document part for this package.
+
+        In this case it will be a |Presentation| part.
+        """
+        return cast("PresentationPart", self.part_related_by(RT.OFFICE_DOCUMENT))
+
+    def next_partname(self, tmpl: str) -> PackURI:
+        """Return |PackURI| next available partname matching `tmpl`.
+
+        `tmpl` is a printf (%)-style template string containing a single replacement item, a '%d'
+        to be used to insert the integer portion of the partname. Example:
+        '/ppt/slides/slide%d.xml'
+        """
+        # --- expected next partname is tmpl % n where n is one greater than the number
+        # --- of existing partnames that match tmpl. Speed up finding the next one
+        # --- (maybe) by searching from the end downward rather than from 1 upward.
+        prefix = tmpl[: (tmpl % 42).find("42")]
+        partnames = {p.partname for p in self.iter_parts() if p.partname.startswith(prefix)}
+        for n in range(len(partnames) + 1, 0, -1):
+            candidate_partname = tmpl % n
+            if candidate_partname not in partnames:
+                return PackURI(candidate_partname)
+        raise Exception("ProgrammingError: ran out of candidate_partnames")  # pragma: no cover
+
+    def save(self, pkg_file: str | IO[bytes]) -> None:
+        """Save this package to `pkg_file`.
+
+        `file` can be either a path to a file (a string) or a file-like object.
+        """
+        PackageWriter.write(pkg_file, self._rels, tuple(self.iter_parts()))
+
+    def _load(self) -> Self:
+        """Return the package after loading all parts and relationships."""
+        pkg_xml_rels, parts = _PackageLoader.load(self._pkg_file, cast("Package", self))
+        self._rels.load_from_xml(PACKAGE_URI, pkg_xml_rels, parts)
+        return self
+
+    @lazyproperty
+    def _rels(self) -> _Relationships:
+        """|Relationships| object containing relationships of this package."""
+        return _Relationships(PACKAGE_URI.baseURI)
+
+
+class _PackageLoader:
+    """Function-object that loads a package from disk (or other store)."""
+
+    def __init__(self, pkg_file: str | IO[bytes], package: Package):
+        self._pkg_file = pkg_file
+        self._package = package
+
+    @classmethod
+    def load(
+        cls, pkg_file: str | IO[bytes], package: Package
+    ) -> tuple[CT_Relationships, dict[PackURI, Part]]:
+        """Return (pkg_xml_rels, parts) pair resulting from loading `pkg_file`.
+
+        The returned `parts` value is a {partname: part} mapping with each part in the package
+        included and constructed complete with its relationships to other parts in the package.
+
+        The returned `pkg_xml_rels` value is a `CT_Relationships` object containing the parsed
+        package relationships. It is the caller's responsibility (the package object) to load
+        those relationships into its |_Relationships| object.
+        """
+        return cls(pkg_file, package)._load()
+
+    def _load(self) -> tuple[CT_Relationships, dict[PackURI, Part]]:
+        """Return (pkg_xml_rels, parts) pair resulting from loading pkg_file."""
+        parts, xml_rels = self._parts, self._xml_rels
+
+        for partname, part in parts.items():
+            part.load_rels_from_xml(xml_rels[partname], parts)
+
+        return xml_rels[PACKAGE_URI], parts
+
+    @lazyproperty
+    def _content_types(self) -> _ContentTypeMap:
+        """|_ContentTypeMap| object providing content-types for items of this package.
+
+        Provides a content-type (MIME-type) for any given partname.
+        """
+        return _ContentTypeMap.from_xml(self._package_reader[CONTENT_TYPES_URI])
+
+    @lazyproperty
+    def _package_reader(self) -> PackageReader:
+        """|PackageReader| object providing access to package-items in pkg_file."""
+        return PackageReader(self._pkg_file)
+
+    @lazyproperty
+    def _parts(self) -> dict[PackURI, Part]:
+        """dict {partname: Part} populated with parts loading from package.
+
+        Among other duties, this collection is passed to each relationships collection so each
+        relationship can resolve a reference to its target part when required. This reference can
+        only be reliably carried out once the all parts have been loaded.
+        """
+        content_types = self._content_types
+        package = self._package
+        package_reader = self._package_reader
+
+        return {
+            partname: PartFactory(
+                partname,
+                content_types[partname],
+                package,
+                blob=package_reader[partname],
+            )
+            for partname in (p for p in self._xml_rels if p != "/")
+            # -- invalid partnames can arise in some packages; ignore those rather than raise an
+            # -- exception.
+            if partname in package_reader
+        }
+
+    @lazyproperty
+    def _xml_rels(self) -> dict[PackURI, CT_Relationships]:
+        """dict {partname: xml_rels} for package and all package parts.
+
+        This is used as the basis for other loading operations such as loading parts and
+        populating their relationships.
+        """
+        xml_rels: dict[PackURI, CT_Relationships] = {}
+        visited_partnames: Set[PackURI] = set()
+
+        def load_rels(source_partname: PackURI, rels: CT_Relationships):
+            """Populate `xml_rels` dict by traversing relationships depth-first."""
+            xml_rels[source_partname] = rels
+            visited_partnames.add(source_partname)
+            base_uri = source_partname.baseURI
+
+            # --- recursion stops when there are no unvisited partnames in rels ---
+            for rel in rels.relationship_lst:
+                if rel.targetMode == RTM.EXTERNAL:
+                    continue
+                target_partname = PackURI.from_rel_ref(base_uri, rel.target_ref)
+                if target_partname in visited_partnames:
+                    continue
+                load_rels(target_partname, self._xml_rels_for(target_partname))
+
+        load_rels(PACKAGE_URI, self._xml_rels_for(PACKAGE_URI))
+        return xml_rels
+
+    def _xml_rels_for(self, partname: PackURI) -> CT_Relationships:
+        """Return CT_Relationships object formed by parsing rels XML for `partname`.
+
+        A CT_Relationships object is returned in all cases. A part that has no relationships
+        receives an "empty" CT_Relationships object, i.e. containing no `CT_Relationship` objects.
+        """
+        rels_xml = self._package_reader.rels_xml_for(partname)
+        return (
+            CT_Relationships.new()
+            if rels_xml is None
+            else cast(CT_Relationships, parse_xml(rels_xml))
+        )
+
+
+class Part(_RelatableMixin):
+    """Base class for package parts.
+
+    Provides common properties and methods, but intended to be subclassed in client code to
+    implement specific part behaviors. Also serves as the default class for parts that are not yet
+    given specific behaviors.
+    """
+
+    def __init__(
+        self, partname: PackURI, content_type: str, package: Package, blob: bytes | None = None
+    ):
+        # --- XmlPart subtypes, don't store a blob (the original XML) ---
+        self._partname = partname
+        self._content_type = content_type
+        self._package = package
+        self._blob = blob
+
+    @classmethod
+    def load(cls, partname: PackURI, content_type: str, package: Package, blob: bytes) -> Self:
+        """Return `cls` instance loaded from arguments.
+
+        This one is a straight pass-through, but subtypes may do some pre-processing, see XmlPart
+        for an example.
+        """
+        return cls(partname, content_type, package, blob)
+
+    @property
+    def blob(self) -> bytes:
+        """Contents of this package part as a sequence of bytes.
+
+        Intended to be overridden by subclasses. Default behavior is to return the blob initial
+        loaded during `Package.open()` operation.
+        """
+        return self._blob or b""
+
+    @blob.setter
+    def blob(self, blob: bytes):
+        """Note that not all subclasses use the part blob as their blob source.
+
+        In particular, the |XmlPart| subclass uses its `self._element` to serialize a blob on
+        demand. This works fine for binary parts though.
+        """
+        self._blob = blob
+
+    @lazyproperty
+    def content_type(self) -> str:
+        """Content-type (MIME-type) of this part."""
+        return self._content_type
+
+    def load_rels_from_xml(self, xml_rels: CT_Relationships, parts: dict[PackURI, Part]) -> None:
+        """load _Relationships for this part from `xml_rels`.
+
+        Part references are resolved using the `parts` dict that maps each partname to the loaded
+        part with that partname. These relationships are loaded from a serialized package and so
+        already have assigned rIds. This method is only used during package loading.
+        """
+        self._rels.load_from_xml(self._partname.baseURI, xml_rels, parts)
+
+    @lazyproperty
+    def package(self) -> Package:
+        """Package this part belongs to."""
+        return self._package
+
+    @property
+    def partname(self) -> PackURI:
+        """|PackURI| partname for this part, e.g. "/ppt/slides/slide1.xml"."""
+        return self._partname
+
+    @partname.setter
+    def partname(self, partname: PackURI):
+        if not isinstance(partname, PackURI):  # pyright: ignore[reportUnnecessaryIsInstance]
+            raise TypeError(  # pragma: no cover
+                "partname must be instance of PackURI, got '%s'" % type(partname).__name__
+            )
+        self._partname = partname
+
+    @lazyproperty
+    def rels(self) -> _Relationships:
+        """Collection of relationships from this part to other parts."""
+        # --- this must be public to allow the part graph to be traversed ---
+        return self._rels
+
+    def _blob_from_file(self, file: str | IO[bytes]) -> bytes:
+        """Return bytes of `file`, which is either a str path or a file-like object."""
+        # --- a str `file` is assumed to be a path ---
+        if isinstance(file, str):
+            with open(file, "rb") as f:
+                return f.read()
+
+        # --- otherwise, assume `file` is a file-like object
+        # --- reposition file cursor if it has one
+        if callable(getattr(file, "seek")):
+            file.seek(0)
+        return file.read()
+
+    @lazyproperty
+    def _rels(self) -> _Relationships:
+        """Relationships from this part to others."""
+        return _Relationships(self._partname.baseURI)
+
+
+class XmlPart(Part):
+    """Base class for package parts containing an XML payload, which is most of them.
+
+    Provides additional methods to the |Part| base class that take care of parsing and
+    reserializing the XML payload and managing relationships to other parts.
+    """
+
+    def __init__(
+        self, partname: PackURI, content_type: str, package: Package, element: BaseOxmlElement
+    ):
+        super(XmlPart, self).__init__(partname, content_type, package)
+        self._element = element
+
+    @classmethod
+    def load(cls, partname: PackURI, content_type: str, package: Package, blob: bytes):
+        """Return instance of `cls` loaded with parsed XML from `blob`."""
+        return cls(
+            partname, content_type, package, element=cast("BaseOxmlElement", parse_xml(blob))
+        )
+
+    @property
+    def blob(self) -> bytes:  # pyright: ignore[reportIncompatibleMethodOverride]
+        """bytes XML serialization of this part."""
+        return serialize_part_xml(self._element)
+
+    # -- XmlPart cannot set its blob, which is why pyright complains --
+
+    def drop_rel(self, rId: str) -> None:
+        """Remove relationship identified by `rId` if its reference count is under 2.
+
+        Relationships with a reference count of 0 are implicit relationships. Note that only XML
+        parts can drop relationships.
+        """
+        if self._rel_ref_count(rId) < 2:
+            self._rels.pop(rId)
+
+    @property
+    def part(self):
+        """This part.
+
+        This is part of the parent protocol, "children" of the document will not know the part
+        that contains them so must ask their parent object. That chain of delegation ends here for
+        child objects.
+        """
+        return self
+
+    def _rel_ref_count(self, rId: str) -> int:
+        """Return int count of references in this part's XML to `rId`."""
+        return len([r for r in cast("list[str]", self._element.xpath("//@r:id")) if r == rId])
+
+
+class PartFactory:
+    """Constructs a registered subtype of |Part|.
+
+    Client code can register a subclass of |Part| to be used for a package blob based on its
+    content type.
+    """
+
+    part_type_for: dict[str, type[Part]] = {}
+
+    def __new__(cls, partname: PackURI, content_type: str, package: Package, blob: bytes) -> Part:
+        PartClass = cls._part_cls_for(content_type)
+        return PartClass.load(partname, content_type, package, blob)
+
+    @classmethod
+    def _part_cls_for(cls, content_type: str) -> type[Part]:
+        """Return the custom part class registered for `content_type`.
+
+        Returns |Part| if no custom class is registered for `content_type`.
+        """
+        if content_type in cls.part_type_for:
+            return cls.part_type_for[content_type]
+        return Part
+
+
+class _ContentTypeMap:
+    """Value type providing dict semantics for looking up content type by partname."""
+
+    def __init__(self, overrides: dict[str, str], defaults: dict[str, str]):
+        self._overrides = overrides
+        self._defaults = defaults
+
+    def __getitem__(self, partname: PackURI) -> str:
+        """Return content-type (MIME-type) for part identified by *partname*."""
+        if not isinstance(partname, PackURI):  # pyright: ignore[reportUnnecessaryIsInstance]
+            raise TypeError(
+                "_ContentTypeMap key must be <type 'PackURI'>, got %s" % type(partname).__name__
+            )
+
+        if partname in self._overrides:
+            return self._overrides[partname]
+
+        if partname.ext in self._defaults:
+            return self._defaults[partname.ext]
+
+        raise KeyError("no content-type for partname '%s' in [Content_Types].xml" % partname)
+
+    @classmethod
+    def from_xml(cls, content_types_xml: bytes) -> _ContentTypeMap:
+        """Return |_ContentTypeMap| instance populated from `content_types_xml`."""
+        types_elm = cast("CT_Types", parse_xml(content_types_xml))
+        # -- note all partnames in [Content_Types].xml are absolute --
+        overrides = CaseInsensitiveDict(
+            (o.partName.lower(), o.contentType) for o in types_elm.override_lst
+        )
+        defaults = CaseInsensitiveDict(
+            (d.extension.lower(), d.contentType) for d in types_elm.default_lst
+        )
+        return cls(overrides, defaults)
+
+
+class _Relationships(Mapping[str, "_Relationship"]):
+    """Collection of |_Relationship| instances having `dict` semantics.
+
+    Relationships are keyed by their rId, but may also be found in other ways, such as by their
+    relationship type. |Relationship| objects are keyed by their rId.
+
+    Iterating this collection has normal mapping semantics, generating the keys (rIds) of the
+    mapping. `rels.keys()`, `rels.values()`, and `rels.items() can be used as they would be for a
+    `dict`.
+    """
+
+    def __init__(self, base_uri: str):
+        self._base_uri = base_uri
+
+    def __contains__(self, rId: object) -> bool:
+        """Implement 'in' operation, like `"rId7" in relationships`."""
+        return rId in self._rels
+
+    def __getitem__(self, rId: str) -> _Relationship:
+        """Implement relationship lookup by rId using indexed access, like rels[rId]."""
+        try:
+            return self._rels[rId]
+        except KeyError:
+            raise KeyError("no relationship with key '%s'" % rId)
+
+    def __iter__(self) -> Iterator[str]:
+        """Implement iteration of rIds (iterating a mapping produces its keys)."""
+        return iter(self._rels)
+
+    def __len__(self) -> int:
+        """Return count of relationships in collection."""
+        return len(self._rels)
+
+    def get_or_add(self, reltype: str, target_part: Part) -> str:
+        """Return str rId of `reltype` to `target_part`.
+
+        The rId of an existing matching relationship is used if present. Otherwise, a new
+        relationship is added and that rId is returned.
+        """
+        existing_rId = self._get_matching(reltype, target_part)
+        return (
+            self._add_relationship(reltype, target_part) if existing_rId is None else existing_rId
+        )
+
+    def get_or_add_ext_rel(self, reltype: str, target_ref: str) -> str:
+        """Return str rId of external relationship of `reltype` to `target_ref`.
+
+        The rId of an existing matching relationship is used if present. Otherwise, a new
+        relationship is added and that rId is returned.
+        """
+        existing_rId = self._get_matching(reltype, target_ref, is_external=True)
+        return (
+            self._add_relationship(reltype, target_ref, is_external=True)
+            if existing_rId is None
+            else existing_rId
+        )
+
+    def load_from_xml(
+        self, base_uri: str, xml_rels: CT_Relationships, parts: dict[PackURI, Part]
+    ) -> None:
+        """Replace any relationships in this collection with those from `xml_rels`."""
+
+        def iter_valid_rels():
+            """Filter out broken relationships such as those pointing to NULL."""
+            for rel_elm in xml_rels.relationship_lst:
+                # --- Occasionally a PowerPoint plugin or other client will "remove"
+                # --- a relationship simply by "voiding" its Target value, like making
+                # --- it "/ppt/slides/NULL". Skip any relationships linking to a
+                # --- partname that is not present in the package.
+                if rel_elm.targetMode == RTM.INTERNAL:
+                    partname = PackURI.from_rel_ref(base_uri, rel_elm.target_ref)
+                    if partname not in parts:
+                        continue
+                yield _Relationship.from_xml(base_uri, rel_elm, parts)
+
+        self._rels.clear()
+        self._rels.update((rel.rId, rel) for rel in iter_valid_rels())
+
+    def part_with_reltype(self, reltype: str) -> Part:
+        """Return target part of relationship with matching `reltype`.
+
+        Raises |KeyError| if not found and |ValueError| if more than one matching relationship is
+        found.
+        """
+        rels_of_reltype = self._rels_by_reltype[reltype]
+
+        if len(rels_of_reltype) == 0:
+            raise KeyError("no relationship of type '%s' in collection" % reltype)
+
+        if len(rels_of_reltype) > 1:
+            raise ValueError("multiple relationships of type '%s' in collection" % reltype)
+
+        return rels_of_reltype[0].target_part
+
+    def pop(self, rId: str) -> _Relationship:
+        """Return |_Relationship| identified by `rId` after removing it from collection.
+
+        The caller is responsible for ensuring it is no longer required.
+        """
+        return self._rels.pop(rId)
+
+    @property
+    def xml(self):
+        """bytes XML serialization of this relationship collection.
+
+        This value is suitable for storage as a .rels file in an OPC package. Includes a `<?xml..`
+        declaration header with encoding as UTF-8.
+        """
+        rels_elm = CT_Relationships.new()
+
+        # -- Sequence <Relationship> elements deterministically (in numerical order) to
+        # -- simplify testing and manual inspection.
+        def iter_rels_in_numerical_order():
+            sorted_num_rId_pairs = sorted(
+                (
+                    int(rId[3:]) if rId.startswith("rId") and rId[3:].isdigit() else 0,
+                    rId,
+                )
+                for rId in self.keys()
+            )
+            return (self[rId] for _, rId in sorted_num_rId_pairs)
+
+        for rel in iter_rels_in_numerical_order():
+            rels_elm.add_rel(rel.rId, rel.reltype, rel.target_ref, rel.is_external)
+
+        return rels_elm.xml_file_bytes
+
+    def _add_relationship(self, reltype: str, target: Part | str, is_external: bool = False) -> str:
+        """Return str rId of |_Relationship| newly added to spec."""
+        rId = self._next_rId
+        self._rels[rId] = _Relationship(
+            self._base_uri,
+            rId,
+            reltype,
+            target_mode=RTM.EXTERNAL if is_external else RTM.INTERNAL,
+            target=target,
+        )
+        return rId
+
+    def _get_matching(
+        self, reltype: str, target: Part | str, is_external: bool = False
+    ) -> str | None:
+        """Return optional str rId of rel of `reltype`, `target`, and `is_external`.
+
+        Returns `None` on no matching relationship
+        """
+        for rel in self._rels_by_reltype[reltype]:
+            if rel.is_external != is_external:
+                continue
+            rel_target = rel.target_ref if rel.is_external else rel.target_part
+            if rel_target == target:
+                return rel.rId
+
+        return None
+
+    @property
+    def _next_rId(self) -> str:
+        """Next str rId available in collection.
+
+        The next rId is the first unused key starting from "rId1" and making use of any gaps in
+        numbering, e.g. 'rId2' for rIds ['rId1', 'rId3'].
+        """
+        # --- The common case is where all sequential numbers starting at "rId1" are
+        # --- used and the next available rId is "rId%d" % (len(rels)+1). So we start
+        # --- there and count down to produce the best performance.
+        for n in range(len(self) + 1, 0, -1):
+            rId_candidate = "rId%d" % n  # like 'rId19'
+            if rId_candidate not in self._rels:
+                return rId_candidate
+        raise Exception(
+            "ProgrammingError: Impossible to have more distinct rIds than relationships"
+        )
+
+    @lazyproperty
+    def _rels(self) -> dict[str, _Relationship]:
+        """dict {rId: _Relationship} containing relationships of this collection."""
+        return {}
+
+    @property
+    def _rels_by_reltype(self) -> dict[str, list[_Relationship]]:
+        """defaultdict {reltype: [rels]} for all relationships in collection."""
+        D: DefaultDict[str, list[_Relationship]] = collections.defaultdict(list)
+        for rel in self.values():
+            D[rel.reltype].append(rel)
+        return D
+
+
+class _Relationship:
+    """Value object describing link from a part or package to another part."""
+
+    def __init__(self, base_uri: str, rId: str, reltype: str, target_mode: str, target: Part | str):
+        self._base_uri = base_uri
+        self._rId = rId
+        self._reltype = reltype
+        self._target_mode = target_mode
+        self._target = target
+
+    @classmethod
+    def from_xml(
+        cls, base_uri: str, rel: CT_Relationship, parts: dict[PackURI, Part]
+    ) -> _Relationship:
+        """Return |_Relationship| object based on CT_Relationship element `rel`."""
+        target = (
+            rel.target_ref
+            if rel.targetMode == RTM.EXTERNAL
+            else parts[PackURI.from_rel_ref(base_uri, rel.target_ref)]
+        )
+        return cls(base_uri, rel.rId, rel.reltype, rel.targetMode, target)
+
+    @lazyproperty
+    def is_external(self) -> bool:
+        """True if target_mode is `RTM.EXTERNAL`.
+
+        An external relationship is a link to a resource outside the package, such as a
+        web-resource (URL).
+        """
+        return self._target_mode == RTM.EXTERNAL
+
+    @lazyproperty
+    def reltype(self) -> str:
+        """Member of RELATIONSHIP_TYPE describing relationship of target to source."""
+        return self._reltype
+
+    @lazyproperty
+    def rId(self) -> str:
+        """str relationship-id, like 'rId9'.
+
+        Corresponds to the `Id` attribute on the `CT_Relationship` element and uniquely identifies
+        this relationship within its peers for the source-part or package.
+        """
+        return self._rId
+
+    @lazyproperty
+    def target_part(self) -> Part:
+        """|Part| or subtype referred to by this relationship."""
+        if self.is_external:
+            raise ValueError(
+                "`.target_part` property on _Relationship is undefined when "
+                "target-mode is external"
+            )
+        assert isinstance(self._target, Part)
+        return self._target
+
+    @lazyproperty
+    def target_partname(self) -> PackURI:
+        """|PackURI| instance containing partname targeted by this relationship.
+
+        Raises `ValueError` on reference if target_mode is external. Use :attr:`target_mode` to
+        check before referencing.
+        """
+        if self.is_external:
+            raise ValueError(
+                "`.target_partname` property on _Relationship is undefined when "
+                "target-mode is external"
+            )
+        assert isinstance(self._target, Part)
+        return self._target.partname
+
+    @lazyproperty
+    def target_ref(self) -> str:
+        """str reference to relationship target.
+
+        For internal relationships this is the relative partname, suitable for serialization
+        purposes. For an external relationship it is typically a URL.
+        """
+        if self.is_external:
+            assert isinstance(self._target, str)
+            return self._target
+
+        return self.target_partname.relative_ref(self._base_uri)
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/packuri.py b/.venv/lib/python3.12/site-packages/pptx/opc/packuri.py
new file mode 100644
index 00000000..74ddd333
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/packuri.py
@@ -0,0 +1,109 @@
+"""Provides the PackURI value type and known pack-URI strings such as PACKAGE_URI."""
+
+from __future__ import annotations
+
+import posixpath
+import re
+
+
+class PackURI(str):
+    """Proxy for a pack URI (partname).
+
+    Provides utility properties the baseURI and the filename slice. Behaves as |str| otherwise.
+    """
+
+    _filename_re = re.compile("([a-zA-Z]+)([0-9][0-9]*)?")
+
+    def __new__(cls, pack_uri_str: str):
+        if not pack_uri_str[0] == "/":
+            raise ValueError(f"PackURI must begin with slash, got {repr(pack_uri_str)}")
+        return str.__new__(cls, pack_uri_str)
+
+    @staticmethod
+    def from_rel_ref(baseURI: str, relative_ref: str) -> PackURI:
+        """Construct an absolute pack URI formed by translating `relative_ref` onto `baseURI`."""
+        joined_uri = posixpath.join(baseURI, relative_ref)
+        abs_uri = posixpath.abspath(joined_uri)
+        return PackURI(abs_uri)
+
+    @property
+    def baseURI(self) -> str:
+        """The base URI of this pack URI; the directory portion, roughly speaking.
+
+        E.g. `"/ppt/slides"` for `"/ppt/slides/slide1.xml"`.
+
+        For the package pseudo-partname "/", the baseURI is "/".
+        """
+        return posixpath.split(self)[0]
+
+    @property
+    def ext(self) -> str:
+        """The extension portion of this pack URI.
+
+        E.g. `"xml"` for `"/ppt/slides/slide1.xml"`. Note the leading period is not included.
+        """
+        # -- raw_ext is either empty string or starts with period, e.g. ".xml" --
+        raw_ext = posixpath.splitext(self)[1]
+        return raw_ext[1:] if raw_ext.startswith(".") else raw_ext
+
+    @property
+    def filename(self) -> str:
+        """The "filename" portion of this pack URI.
+
+        E.g. `"slide1.xml"` for `"/ppt/slides/slide1.xml"`.
+
+        For the package pseudo-partname "/", `filename` is ''.
+        """
+        return posixpath.split(self)[1]
+
+    @property
+    def idx(self) -> int | None:
+        """Optional int partname index.
+
+        Value is an integer for an "array" partname or None for singleton partname, e.g. `21` for
+        `"/ppt/slides/slide21.xml"` and |None| for `"/ppt/presentation.xml"`.
+        """
+        filename = self.filename
+        if not filename:
+            return None
+        name_part = posixpath.splitext(filename)[0]  # filename w/ext removed
+        match = self._filename_re.match(name_part)
+        if match is None:
+            return None
+        if match.group(2):
+            return int(match.group(2))
+        return None
+
+    @property
+    def membername(self) -> str:
+        """The pack URI with the leading slash stripped off.
+
+        This is the form used as the Zip file membername for the package item. Returns "" for the
+        package pseudo-partname "/".
+        """
+        return self[1:]
+
+    def relative_ref(self, baseURI: str) -> str:
+        """Return string containing relative reference to package item from `baseURI`.
+
+        E.g. PackURI("/ppt/slideLayouts/slideLayout1.xml") would return
+        "../slideLayouts/slideLayout1.xml" for baseURI "/ppt/slides".
+        """
+        # workaround for posixpath bug in 2.6, doesn't generate correct
+        # relative path when `start` (second) parameter is root ("/")
+        return self[1:] if baseURI == "/" else posixpath.relpath(self, baseURI)
+
+    @property
+    def rels_uri(self) -> PackURI:
+        """The pack URI of the .rels part corresponding to the current pack URI.
+
+        Only produces sensible output if the pack URI is a partname or the package pseudo-partname
+        "/".
+        """
+        rels_filename = "%s.rels" % self.filename
+        rels_uri_str = posixpath.join(self.baseURI, "_rels", rels_filename)
+        return PackURI(rels_uri_str)
+
+
+PACKAGE_URI = PackURI("/")
+CONTENT_TYPES_URI = PackURI("/[Content_Types].xml")
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/serialized.py b/.venv/lib/python3.12/site-packages/pptx/opc/serialized.py
new file mode 100644
index 00000000..92366708
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/serialized.py
@@ -0,0 +1,296 @@
+"""API for reading/writing serialized Open Packaging Convention (OPC) package."""
+
+from __future__ import annotations
+
+import os
+import posixpath
+import zipfile
+from typing import IO, TYPE_CHECKING, Any, Container, Sequence
+
+from pptx.exc import PackageNotFoundError
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.oxml import CT_Types, serialize_part_xml
+from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI
+from pptx.opc.shared import CaseInsensitiveDict
+from pptx.opc.spec import default_content_types
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.opc.package import Part, _Relationships  # pyright: ignore[reportPrivateUsage]
+
+
+class PackageReader(Container[bytes]):
+    """Provides access to package-parts of an OPC package with dict semantics.
+
+    The package may be in zip-format (a .pptx file) or expanded into a directory structure,
+    perhaps by unzipping a .pptx file.
+    """
+
+    def __init__(self, pkg_file: str | IO[bytes]):
+        self._pkg_file = pkg_file
+
+    def __contains__(self, pack_uri: object) -> bool:
+        """Return True when part identified by `pack_uri` is present in package."""
+        return pack_uri in self._blob_reader
+
+    def __getitem__(self, pack_uri: PackURI) -> bytes:
+        """Return bytes for part corresponding to `pack_uri`."""
+        return self._blob_reader[pack_uri]
+
+    def rels_xml_for(self, partname: PackURI) -> bytes | None:
+        """Return optional rels item XML for `partname`.
+
+        Returns `None` if no rels item is present for `partname`. `partname` is a |PackURI|
+        instance.
+        """
+        blob_reader, uri = self._blob_reader, partname.rels_uri
+        return blob_reader[uri] if uri in blob_reader else None
+
+    @lazyproperty
+    def _blob_reader(self) -> _PhysPkgReader:
+        """|_PhysPkgReader| subtype providing read access to the package file."""
+        return _PhysPkgReader.factory(self._pkg_file)
+
+
+class PackageWriter:
+    """Writes a zip-format OPC package to `pkg_file`.
+
+    `pkg_file` can be either a path to a zip file (a string) or a file-like object. `pkg_rels` is
+    the |_Relationships| object containing relationships for the package. `parts` is a sequence of
+    |Part| subtype instance to be written to the package.
+
+    Its single API classmethod is :meth:`write`. This class is not intended to be instantiated.
+    """
+
+    def __init__(self, pkg_file: str | IO[bytes], pkg_rels: _Relationships, parts: Sequence[Part]):
+        self._pkg_file = pkg_file
+        self._pkg_rels = pkg_rels
+        self._parts = parts
+
+    @classmethod
+    def write(
+        cls, pkg_file: str | IO[bytes], pkg_rels: _Relationships, parts: Sequence[Part]
+    ) -> None:
+        """Write a physical package (.pptx file) to `pkg_file`.
+
+        The serialized package contains `pkg_rels` and `parts`, a content-types stream based on
+        the content type of each part, and a .rels file for each part that has relationships.
+        """
+        cls(pkg_file, pkg_rels, parts)._write()
+
+    def _write(self) -> None:
+        """Write physical package (.pptx file)."""
+        with _PhysPkgWriter.factory(self._pkg_file) as phys_writer:
+            self._write_content_types_stream(phys_writer)
+            self._write_pkg_rels(phys_writer)
+            self._write_parts(phys_writer)
+
+    def _write_content_types_stream(self, phys_writer: _PhysPkgWriter) -> None:
+        """Write `[Content_Types].xml` part to the physical package.
+
+        This part must contain an appropriate content type lookup target for each part in the
+        package.
+        """
+        phys_writer.write(
+            CONTENT_TYPES_URI,
+            serialize_part_xml(_ContentTypesItem.xml_for(self._parts)),
+        )
+
+    def _write_parts(self, phys_writer: _PhysPkgWriter) -> None:
+        """Write blob of each part in `parts` to the package.
+
+        A rels item for each part is also written when the part has relationships.
+        """
+        for part in self._parts:
+            phys_writer.write(part.partname, part.blob)
+            if part._rels:  # pyright: ignore[reportPrivateUsage]
+                phys_writer.write(part.partname.rels_uri, part.rels.xml)
+
+    def _write_pkg_rels(self, phys_writer: _PhysPkgWriter) -> None:
+        """Write the XML rels item for `pkg_rels` ('/_rels/.rels') to the package."""
+        phys_writer.write(PACKAGE_URI.rels_uri, self._pkg_rels.xml)
+
+
+class _PhysPkgReader(Container[PackURI]):
+    """Base class for physical package reader objects."""
+
+    def __contains__(self, item: object) -> bool:
+        """Must be implemented by each subclass."""
+        raise NotImplementedError(  # pragma: no cover
+            "`%s` must implement `.__contains__()`" % type(self).__name__
+        )
+
+    def __getitem__(self, pack_uri: PackURI) -> bytes:
+        """Blob for part corresponding to `pack_uri`."""
+        raise NotImplementedError(  # pragma: no cover
+            f"`{type(self).__name__}` must implement `.__contains__()`"
+        )
+
+    @classmethod
+    def factory(cls, pkg_file: str | IO[bytes]) -> _PhysPkgReader:
+        """Return |_PhysPkgReader| subtype instance appropriage for `pkg_file`."""
+        # --- for pkg_file other than str, assume it's a stream and pass it to Zip
+        # --- reader to sort out
+        if not isinstance(pkg_file, str):
+            return _ZipPkgReader(pkg_file)
+
+        # --- otherwise we treat `pkg_file` as a path ---
+        if os.path.isdir(pkg_file):
+            return _DirPkgReader(pkg_file)
+
+        if zipfile.is_zipfile(pkg_file):
+            return _ZipPkgReader(pkg_file)
+
+        raise PackageNotFoundError("Package not found at '%s'" % pkg_file)
+
+
+class _DirPkgReader(_PhysPkgReader):
+    """Implements |PhysPkgReader| interface for OPC package extracted into directory.
+
+    `path` is the path to a directory containing an expanded package.
+    """
+
+    def __init__(self, path: str):
+        self._path = os.path.abspath(path)
+
+    def __contains__(self, pack_uri: object) -> bool:
+        """Return True when part identified by `pack_uri` is present in zip archive."""
+        if not isinstance(pack_uri, PackURI):
+            return False
+        return os.path.exists(posixpath.join(self._path, pack_uri.membername))
+
+    def __getitem__(self, pack_uri: PackURI) -> bytes:
+        """Return bytes of file corresponding to `pack_uri` in package directory."""
+        path = os.path.join(self._path, pack_uri.membername)
+        try:
+            with open(path, "rb") as f:
+                return f.read()
+        except IOError:
+            raise KeyError("no member '%s' in package" % pack_uri)
+
+
+class _ZipPkgReader(_PhysPkgReader):
+    """Implements |PhysPkgReader| interface for a zip-file OPC package."""
+
+    def __init__(self, pkg_file: str | IO[bytes]):
+        self._pkg_file = pkg_file
+
+    def __contains__(self, pack_uri: object) -> bool:
+        """Return True when part identified by `pack_uri` is present in zip archive."""
+        return pack_uri in self._blobs
+
+    def __getitem__(self, pack_uri: PackURI) -> bytes:
+        """Return bytes for part corresponding to `pack_uri`.
+
+        Raises |KeyError| if no matching member is present in zip archive.
+        """
+        if pack_uri not in self._blobs:
+            raise KeyError("no member '%s' in package" % pack_uri)
+        return self._blobs[pack_uri]
+
+    @lazyproperty
+    def _blobs(self) -> dict[PackURI, bytes]:
+        """dict mapping partname to package part binaries."""
+        with zipfile.ZipFile(self._pkg_file, "r") as z:
+            return {PackURI("/%s" % name): z.read(name) for name in z.namelist()}
+
+
+class _PhysPkgWriter:
+    """Base class for physical package writer objects."""
+
+    @classmethod
+    def factory(cls, pkg_file: str | IO[bytes]) -> _ZipPkgWriter:
+        """Return |_PhysPkgWriter| subtype instance appropriage for `pkg_file`.
+
+        Currently the only subtype is `_ZipPkgWriter`, but a `_DirPkgWriter` could be implemented
+        or even a `_StreamPkgWriter`.
+        """
+        return _ZipPkgWriter(pkg_file)
+
+    def write(self, pack_uri: PackURI, blob: bytes) -> None:
+        """Write `blob` to package with membername corresponding to `pack_uri`."""
+        raise NotImplementedError(  # pragma: no cover
+            f"`{type(self).__name__}` must implement `.write()`"
+        )
+
+
+class _ZipPkgWriter(_PhysPkgWriter):
+    """Implements |PhysPkgWriter| interface for a zip-file (.pptx file) OPC package."""
+
+    def __init__(self, pkg_file: str | IO[bytes]):
+        self._pkg_file = pkg_file
+
+    def __enter__(self) -> _ZipPkgWriter:
+        """Enable use as a context-manager. Opening zip for writing happens here."""
+        return self
+
+    def __exit__(self, *exc: list[Any]) -> None:
+        """Close the zip archive on exit from context.
+
+        Closing flushes any pending physical writes and releasing any resources it's using.
+        """
+        self._zipf.close()
+
+    def write(self, pack_uri: PackURI, blob: bytes) -> None:
+        """Write `blob` to zip package with membername corresponding to `pack_uri`."""
+        self._zipf.writestr(pack_uri.membername, blob)
+
+    @lazyproperty
+    def _zipf(self) -> zipfile.ZipFile:
+        """`ZipFile` instance open for writing."""
+        return zipfile.ZipFile(
+            self._pkg_file, "w", compression=zipfile.ZIP_DEFLATED, strict_timestamps=False
+        )
+
+
+class _ContentTypesItem:
+    """Composes content-types "part" ([Content_Types].xml) for a collection of parts."""
+
+    def __init__(self, parts: Sequence[Part]):
+        self._parts = parts
+
+    @classmethod
+    def xml_for(cls, parts: Sequence[Part]) -> CT_Types:
+        """Return content-types XML mapping each part in `parts` to a content-type.
+
+        The resulting XML is suitable for storage as `[Content_Types].xml` in an OPC package.
+        """
+        return cls(parts)._xml
+
+    @lazyproperty
+    def _xml(self) -> CT_Types:
+        """lxml.etree._Element containing the content-types item.
+
+        This XML object is suitable for serialization to the `[Content_Types].xml` item for an OPC
+        package. Although the sequence of elements is not strictly significant, as an aid to
+        testing and readability Default elements are sorted by extension and Override elements are
+        sorted by partname.
+        """
+        defaults, overrides = self._defaults_and_overrides
+        _types_elm = CT_Types.new()
+
+        for ext, content_type in sorted(defaults.items()):
+            _types_elm.add_default(ext, content_type)
+        for partname, content_type in sorted(overrides.items()):
+            _types_elm.add_override(partname, content_type)
+
+        return _types_elm
+
+    @lazyproperty
+    def _defaults_and_overrides(self) -> tuple[dict[str, str], dict[PackURI, str]]:
+        """pair of dict (defaults, overrides) accounting for all parts.
+
+        `defaults` is {ext: content_type} and overrides is {partname: content_type}.
+        """
+        defaults = CaseInsensitiveDict(rels=CT.OPC_RELATIONSHIPS, xml=CT.XML)
+        overrides: dict[PackURI, str] = {}
+
+        for part in self._parts:
+            partname, content_type = part.partname, part.content_type
+            ext = partname.ext
+            if (ext.lower(), content_type) in default_content_types:
+                defaults[ext] = content_type
+            else:
+                overrides[partname] = content_type
+
+        return defaults, overrides
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/shared.py b/.venv/lib/python3.12/site-packages/pptx/opc/shared.py
new file mode 100644
index 00000000..cc7fce8c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/shared.py
@@ -0,0 +1,20 @@
+"""Objects shared by modules in the pptx.opc sub-package."""
+
+from __future__ import annotations
+
+
+class CaseInsensitiveDict(dict):
+    """Mapping type like dict except it matches key without respect to case.
+
+    For example, D['A'] == D['a']. Note this is not general-purpose, just complete
+    enough to satisfy opc package needs. It assumes str keys for example.
+    """
+
+    def __contains__(self, key):
+        return super(CaseInsensitiveDict, self).__contains__(key.lower())
+
+    def __getitem__(self, key):
+        return super(CaseInsensitiveDict, self).__getitem__(key.lower())
+
+    def __setitem__(self, key, value):
+        return super(CaseInsensitiveDict, self).__setitem__(key.lower(), value)
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/spec.py b/.venv/lib/python3.12/site-packages/pptx/opc/spec.py
new file mode 100644
index 00000000..a83caf8b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/spec.py
@@ -0,0 +1,44 @@
+"""Provides mappings that embody aspects of the Open XML spec ISO/IEC 29500."""
+
+from pptx.opc.constants import CONTENT_TYPE as CT
+
+default_content_types = (
+    ("bin", CT.PML_PRINTER_SETTINGS),
+    ("bin", CT.SML_PRINTER_SETTINGS),
+    ("bin", CT.WML_PRINTER_SETTINGS),
+    ("bmp", CT.BMP),
+    ("emf", CT.X_EMF),
+    ("fntdata", CT.X_FONTDATA),
+    ("gif", CT.GIF),
+    ("jpe", CT.JPEG),
+    ("jpeg", CT.JPEG),
+    ("jpg", CT.JPEG),
+    ("mov", CT.MOV),
+    ("mp4", CT.MP4),
+    ("mpg", CT.MPG),
+    ("png", CT.PNG),
+    ("rels", CT.OPC_RELATIONSHIPS),
+    ("tif", CT.TIFF),
+    ("tiff", CT.TIFF),
+    ("vid", CT.VIDEO),
+    ("wdp", CT.MS_PHOTO),
+    ("wmf", CT.X_WMF),
+    ("wmv", CT.WMV),
+    ("xlsx", CT.SML_SHEET),
+    ("xml", CT.XML),
+)
+
+
+image_content_types = {
+    "bmp": CT.BMP,
+    "emf": CT.X_EMF,
+    "gif": CT.GIF,
+    "jpe": CT.JPEG,
+    "jpeg": CT.JPEG,
+    "jpg": CT.JPEG,
+    "png": CT.PNG,
+    "tif": CT.TIFF,
+    "tiff": CT.TIFF,
+    "wdp": CT.MS_PHOTO,
+    "wmf": CT.X_WMF,
+}
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/__init__.py b/.venv/lib/python3.12/site-packages/pptx/oxml/__init__.py
new file mode 100644
index 00000000..21afaa92
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/__init__.py
@@ -0,0 +1,486 @@
+"""Initializes lxml parser, particularly the custom element classes.
+
+Also makes available a handful of functions that wrap its typical uses.
+"""
+
+from __future__ import annotations
+
+import os
+from typing import TYPE_CHECKING, Type
+
+from lxml import etree
+
+from pptx.oxml.ns import NamespacePrefixedTag
+
+if TYPE_CHECKING:
+    from pptx.oxml.xmlchemy import BaseOxmlElement
+
+
+# -- configure etree 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_from_template(template_file_name: str):
+    """Return an element loaded from the XML in the template file identified by `template_name`."""
+    thisdir = os.path.split(__file__)[0]
+    filename = os.path.join(thisdir, "..", "templates", "%s.xml" % template_file_name)
+    with open(filename, "rb") as f:
+        xml = f.read()
+    return parse_xml(xml)
+
+
+def parse_xml(xml: str | bytes):
+    """Return root lxml element obtained by parsing XML character string in `xml`."""
+    return etree.fromstring(xml, oxml_parser)
+
+
+def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]):
+    """Register `cls` to be constructed when oxml parser encounters element having `nsptag_name`.
+
+    `nsptag_name` is a string of the form `nspfx:tagroot`, e.g. `"w:document"`.
+    """
+    nsptag = NamespacePrefixedTag(nsptagname)
+    namespace = element_class_lookup.get_namespace(nsptag.nsuri)
+    namespace[nsptag.local_part] = cls
+
+
+from pptx.oxml.action import CT_Hyperlink  # noqa: E402
+
+register_element_cls("a:hlinkClick", CT_Hyperlink)
+register_element_cls("a:hlinkHover", CT_Hyperlink)
+
+
+from pptx.oxml.chart.axis import (  # noqa: E402
+    CT_AxisUnit,
+    CT_CatAx,
+    CT_ChartLines,
+    CT_Crosses,
+    CT_DateAx,
+    CT_LblOffset,
+    CT_Orientation,
+    CT_Scaling,
+    CT_TickLblPos,
+    CT_TickMark,
+    CT_ValAx,
+)
+
+register_element_cls("c:catAx", CT_CatAx)
+register_element_cls("c:crosses", CT_Crosses)
+register_element_cls("c:dateAx", CT_DateAx)
+register_element_cls("c:lblOffset", CT_LblOffset)
+register_element_cls("c:majorGridlines", CT_ChartLines)
+register_element_cls("c:majorTickMark", CT_TickMark)
+register_element_cls("c:majorUnit", CT_AxisUnit)
+register_element_cls("c:minorTickMark", CT_TickMark)
+register_element_cls("c:minorUnit", CT_AxisUnit)
+register_element_cls("c:orientation", CT_Orientation)
+register_element_cls("c:scaling", CT_Scaling)
+register_element_cls("c:tickLblPos", CT_TickLblPos)
+register_element_cls("c:valAx", CT_ValAx)
+
+
+from pptx.oxml.chart.chart import (  # noqa: E402
+    CT_Chart,
+    CT_ChartSpace,
+    CT_ExternalData,
+    CT_PlotArea,
+    CT_Style,
+)
+
+register_element_cls("c:chart", CT_Chart)
+register_element_cls("c:chartSpace", CT_ChartSpace)
+register_element_cls("c:externalData", CT_ExternalData)
+register_element_cls("c:plotArea", CT_PlotArea)
+register_element_cls("c:style", CT_Style)
+
+
+from pptx.oxml.chart.datalabel import CT_DLbl, CT_DLblPos, CT_DLbls  # noqa: E402
+
+register_element_cls("c:dLbl", CT_DLbl)
+register_element_cls("c:dLblPos", CT_DLblPos)
+register_element_cls("c:dLbls", CT_DLbls)
+
+
+from pptx.oxml.chart.legend import CT_Legend, CT_LegendPos  # noqa: E402
+
+register_element_cls("c:legend", CT_Legend)
+register_element_cls("c:legendPos", CT_LegendPos)
+
+
+from pptx.oxml.chart.marker import CT_Marker, CT_MarkerSize, CT_MarkerStyle  # noqa: E402
+
+register_element_cls("c:marker", CT_Marker)
+register_element_cls("c:size", CT_MarkerSize)
+register_element_cls("c:symbol", CT_MarkerStyle)
+
+
+from pptx.oxml.chart.plot import (  # noqa: E402
+    CT_Area3DChart,
+    CT_AreaChart,
+    CT_BarChart,
+    CT_BarDir,
+    CT_BubbleChart,
+    CT_BubbleScale,
+    CT_DoughnutChart,
+    CT_GapAmount,
+    CT_Grouping,
+    CT_LineChart,
+    CT_Overlap,
+    CT_PieChart,
+    CT_RadarChart,
+    CT_ScatterChart,
+)
+
+register_element_cls("c:area3DChart", CT_Area3DChart)
+register_element_cls("c:areaChart", CT_AreaChart)
+register_element_cls("c:barChart", CT_BarChart)
+register_element_cls("c:barDir", CT_BarDir)
+register_element_cls("c:bubbleChart", CT_BubbleChart)
+register_element_cls("c:bubbleScale", CT_BubbleScale)
+register_element_cls("c:doughnutChart", CT_DoughnutChart)
+register_element_cls("c:gapWidth", CT_GapAmount)
+register_element_cls("c:grouping", CT_Grouping)
+register_element_cls("c:lineChart", CT_LineChart)
+register_element_cls("c:overlap", CT_Overlap)
+register_element_cls("c:pieChart", CT_PieChart)
+register_element_cls("c:radarChart", CT_RadarChart)
+register_element_cls("c:scatterChart", CT_ScatterChart)
+
+
+from pptx.oxml.chart.series import (  # noqa: E402
+    CT_AxDataSource,
+    CT_DPt,
+    CT_Lvl,
+    CT_NumDataSource,
+    CT_SeriesComposite,
+    CT_StrVal_NumVal_Composite,
+)
+
+register_element_cls("c:bubbleSize", CT_NumDataSource)
+register_element_cls("c:cat", CT_AxDataSource)
+register_element_cls("c:dPt", CT_DPt)
+register_element_cls("c:lvl", CT_Lvl)
+register_element_cls("c:pt", CT_StrVal_NumVal_Composite)
+register_element_cls("c:ser", CT_SeriesComposite)
+register_element_cls("c:val", CT_NumDataSource)
+register_element_cls("c:xVal", CT_NumDataSource)
+register_element_cls("c:yVal", CT_NumDataSource)
+
+
+from pptx.oxml.chart.shared import (  # noqa: E402
+    CT_Boolean,
+    CT_Boolean_Explicit,
+    CT_Double,
+    CT_Layout,
+    CT_LayoutMode,
+    CT_ManualLayout,
+    CT_NumFmt,
+    CT_Title,
+    CT_Tx,
+    CT_UnsignedInt,
+)
+
+register_element_cls("c:autoTitleDeleted", CT_Boolean_Explicit)
+register_element_cls("c:autoUpdate", CT_Boolean)
+register_element_cls("c:bubble3D", CT_Boolean)
+register_element_cls("c:crossAx", CT_UnsignedInt)
+register_element_cls("c:crossesAt", CT_Double)
+register_element_cls("c:date1904", CT_Boolean)
+register_element_cls("c:delete", CT_Boolean)
+register_element_cls("c:idx", CT_UnsignedInt)
+register_element_cls("c:invertIfNegative", CT_Boolean_Explicit)
+register_element_cls("c:layout", CT_Layout)
+register_element_cls("c:manualLayout", CT_ManualLayout)
+register_element_cls("c:max", CT_Double)
+register_element_cls("c:min", CT_Double)
+register_element_cls("c:numFmt", CT_NumFmt)
+register_element_cls("c:order", CT_UnsignedInt)
+register_element_cls("c:overlay", CT_Boolean_Explicit)
+register_element_cls("c:ptCount", CT_UnsignedInt)
+register_element_cls("c:showCatName", CT_Boolean_Explicit)
+register_element_cls("c:showLegendKey", CT_Boolean_Explicit)
+register_element_cls("c:showPercent", CT_Boolean_Explicit)
+register_element_cls("c:showSerName", CT_Boolean_Explicit)
+register_element_cls("c:showVal", CT_Boolean_Explicit)
+register_element_cls("c:smooth", CT_Boolean)
+register_element_cls("c:title", CT_Title)
+register_element_cls("c:tx", CT_Tx)
+register_element_cls("c:varyColors", CT_Boolean)
+register_element_cls("c:x", CT_Double)
+register_element_cls("c:xMode", CT_LayoutMode)
+
+
+from pptx.oxml.coreprops import CT_CoreProperties  # noqa: E402
+
+register_element_cls("cp:coreProperties", CT_CoreProperties)
+
+
+from pptx.oxml.dml.color import (  # noqa: E402
+    CT_Color,
+    CT_HslColor,
+    CT_Percentage,
+    CT_PresetColor,
+    CT_SchemeColor,
+    CT_ScRgbColor,
+    CT_SRgbColor,
+    CT_SystemColor,
+)
+
+register_element_cls("a:bgClr", CT_Color)
+register_element_cls("a:fgClr", CT_Color)
+register_element_cls("a:hslClr", CT_HslColor)
+register_element_cls("a:lumMod", CT_Percentage)
+register_element_cls("a:lumOff", CT_Percentage)
+register_element_cls("a:prstClr", CT_PresetColor)
+register_element_cls("a:schemeClr", CT_SchemeColor)
+register_element_cls("a:scrgbClr", CT_ScRgbColor)
+register_element_cls("a:srgbClr", CT_SRgbColor)
+register_element_cls("a:sysClr", CT_SystemColor)
+
+
+from pptx.oxml.dml.fill import (  # noqa: E402
+    CT_Blip,
+    CT_BlipFillProperties,
+    CT_GradientFillProperties,
+    CT_GradientStop,
+    CT_GradientStopList,
+    CT_GroupFillProperties,
+    CT_LinearShadeProperties,
+    CT_NoFillProperties,
+    CT_PatternFillProperties,
+    CT_RelativeRect,
+    CT_SolidColorFillProperties,
+)
+
+register_element_cls("a:blip", CT_Blip)
+register_element_cls("a:blipFill", CT_BlipFillProperties)
+register_element_cls("a:gradFill", CT_GradientFillProperties)
+register_element_cls("a:grpFill", CT_GroupFillProperties)
+register_element_cls("a:gs", CT_GradientStop)
+register_element_cls("a:gsLst", CT_GradientStopList)
+register_element_cls("a:lin", CT_LinearShadeProperties)
+register_element_cls("a:noFill", CT_NoFillProperties)
+register_element_cls("a:pattFill", CT_PatternFillProperties)
+register_element_cls("a:solidFill", CT_SolidColorFillProperties)
+register_element_cls("a:srcRect", CT_RelativeRect)
+
+
+from pptx.oxml.dml.line import CT_PresetLineDashProperties  # noqa: E402
+
+register_element_cls("a:prstDash", CT_PresetLineDashProperties)
+
+
+from pptx.oxml.presentation import (  # noqa: E402
+    CT_Presentation,
+    CT_SlideId,
+    CT_SlideIdList,
+    CT_SlideMasterIdList,
+    CT_SlideMasterIdListEntry,
+    CT_SlideSize,
+)
+
+register_element_cls("p:presentation", CT_Presentation)
+register_element_cls("p:sldId", CT_SlideId)
+register_element_cls("p:sldIdLst", CT_SlideIdList)
+register_element_cls("p:sldMasterId", CT_SlideMasterIdListEntry)
+register_element_cls("p:sldMasterIdLst", CT_SlideMasterIdList)
+register_element_cls("p:sldSz", CT_SlideSize)
+
+
+from pptx.oxml.shapes.autoshape import (  # noqa: E402
+    CT_AdjPoint2D,
+    CT_CustomGeometry2D,
+    CT_GeomGuide,
+    CT_GeomGuideList,
+    CT_NonVisualDrawingShapeProps,
+    CT_Path2D,
+    CT_Path2DClose,
+    CT_Path2DLineTo,
+    CT_Path2DList,
+    CT_Path2DMoveTo,
+    CT_PresetGeometry2D,
+    CT_Shape,
+    CT_ShapeNonVisual,
+)
+
+register_element_cls("a:avLst", CT_GeomGuideList)
+register_element_cls("a:custGeom", CT_CustomGeometry2D)
+register_element_cls("a:gd", CT_GeomGuide)
+register_element_cls("a:close", CT_Path2DClose)
+register_element_cls("a:lnTo", CT_Path2DLineTo)
+register_element_cls("a:moveTo", CT_Path2DMoveTo)
+register_element_cls("a:path", CT_Path2D)
+register_element_cls("a:pathLst", CT_Path2DList)
+register_element_cls("a:prstGeom", CT_PresetGeometry2D)
+register_element_cls("a:pt", CT_AdjPoint2D)
+register_element_cls("p:cNvSpPr", CT_NonVisualDrawingShapeProps)
+register_element_cls("p:nvSpPr", CT_ShapeNonVisual)
+register_element_cls("p:sp", CT_Shape)
+
+
+from pptx.oxml.shapes.connector import (  # noqa: E402
+    CT_Connection,
+    CT_Connector,
+    CT_ConnectorNonVisual,
+    CT_NonVisualConnectorProperties,
+)
+
+register_element_cls("a:endCxn", CT_Connection)
+register_element_cls("a:stCxn", CT_Connection)
+register_element_cls("p:cNvCxnSpPr", CT_NonVisualConnectorProperties)
+register_element_cls("p:cxnSp", CT_Connector)
+register_element_cls("p:nvCxnSpPr", CT_ConnectorNonVisual)
+
+
+from pptx.oxml.shapes.graphfrm import (  # noqa: E402
+    CT_GraphicalObject,
+    CT_GraphicalObjectData,
+    CT_GraphicalObjectFrame,
+    CT_GraphicalObjectFrameNonVisual,
+    CT_OleObject,
+)
+
+register_element_cls("a:graphic", CT_GraphicalObject)
+register_element_cls("a:graphicData", CT_GraphicalObjectData)
+register_element_cls("p:graphicFrame", CT_GraphicalObjectFrame)
+register_element_cls("p:nvGraphicFramePr", CT_GraphicalObjectFrameNonVisual)
+register_element_cls("p:oleObj", CT_OleObject)
+
+
+from pptx.oxml.shapes.groupshape import (  # noqa: E402
+    CT_GroupShape,
+    CT_GroupShapeNonVisual,
+    CT_GroupShapeProperties,
+)
+
+register_element_cls("p:grpSp", CT_GroupShape)
+register_element_cls("p:grpSpPr", CT_GroupShapeProperties)
+register_element_cls("p:nvGrpSpPr", CT_GroupShapeNonVisual)
+register_element_cls("p:spTree", CT_GroupShape)
+
+
+from pptx.oxml.shapes.picture import CT_Picture, CT_PictureNonVisual  # noqa: E402
+
+register_element_cls("p:blipFill", CT_BlipFillProperties)
+register_element_cls("p:nvPicPr", CT_PictureNonVisual)
+register_element_cls("p:pic", CT_Picture)
+
+
+from pptx.oxml.shapes.shared import (  # noqa: E402
+    CT_ApplicationNonVisualDrawingProps,
+    CT_LineProperties,
+    CT_NonVisualDrawingProps,
+    CT_Placeholder,
+    CT_Point2D,
+    CT_PositiveSize2D,
+    CT_ShapeProperties,
+    CT_Transform2D,
+)
+
+register_element_cls("a:chExt", CT_PositiveSize2D)
+register_element_cls("a:chOff", CT_Point2D)
+register_element_cls("a:ext", CT_PositiveSize2D)
+register_element_cls("a:ln", CT_LineProperties)
+register_element_cls("a:off", CT_Point2D)
+register_element_cls("a:xfrm", CT_Transform2D)
+register_element_cls("c:spPr", CT_ShapeProperties)
+register_element_cls("p:cNvPr", CT_NonVisualDrawingProps)
+register_element_cls("p:nvPr", CT_ApplicationNonVisualDrawingProps)
+register_element_cls("p:ph", CT_Placeholder)
+register_element_cls("p:spPr", CT_ShapeProperties)
+register_element_cls("p:xfrm", CT_Transform2D)
+
+
+from pptx.oxml.slide import (  # noqa: E402
+    CT_Background,
+    CT_BackgroundProperties,
+    CT_CommonSlideData,
+    CT_NotesMaster,
+    CT_NotesSlide,
+    CT_Slide,
+    CT_SlideLayout,
+    CT_SlideLayoutIdList,
+    CT_SlideLayoutIdListEntry,
+    CT_SlideMaster,
+    CT_SlideTiming,
+    CT_TimeNodeList,
+    CT_TLMediaNodeVideo,
+)
+
+register_element_cls("p:bg", CT_Background)
+register_element_cls("p:bgPr", CT_BackgroundProperties)
+register_element_cls("p:childTnLst", CT_TimeNodeList)
+register_element_cls("p:cSld", CT_CommonSlideData)
+register_element_cls("p:notes", CT_NotesSlide)
+register_element_cls("p:notesMaster", CT_NotesMaster)
+register_element_cls("p:sld", CT_Slide)
+register_element_cls("p:sldLayout", CT_SlideLayout)
+register_element_cls("p:sldLayoutId", CT_SlideLayoutIdListEntry)
+register_element_cls("p:sldLayoutIdLst", CT_SlideLayoutIdList)
+register_element_cls("p:sldMaster", CT_SlideMaster)
+register_element_cls("p:timing", CT_SlideTiming)
+register_element_cls("p:video", CT_TLMediaNodeVideo)
+
+
+from pptx.oxml.table import (  # noqa: E402
+    CT_Table,
+    CT_TableCell,
+    CT_TableCellProperties,
+    CT_TableCol,
+    CT_TableGrid,
+    CT_TableProperties,
+    CT_TableRow,
+)
+
+register_element_cls("a:gridCol", CT_TableCol)
+register_element_cls("a:tbl", CT_Table)
+register_element_cls("a:tblGrid", CT_TableGrid)
+register_element_cls("a:tblPr", CT_TableProperties)
+register_element_cls("a:tc", CT_TableCell)
+register_element_cls("a:tcPr", CT_TableCellProperties)
+register_element_cls("a:tr", CT_TableRow)
+
+
+from pptx.oxml.text import (  # noqa: E402
+    CT_RegularTextRun,
+    CT_TextBody,
+    CT_TextBodyProperties,
+    CT_TextCharacterProperties,
+    CT_TextField,
+    CT_TextFont,
+    CT_TextLineBreak,
+    CT_TextNormalAutofit,
+    CT_TextParagraph,
+    CT_TextParagraphProperties,
+    CT_TextSpacing,
+    CT_TextSpacingPercent,
+    CT_TextSpacingPoint,
+)
+
+register_element_cls("a:bodyPr", CT_TextBodyProperties)
+register_element_cls("a:br", CT_TextLineBreak)
+register_element_cls("a:defRPr", CT_TextCharacterProperties)
+register_element_cls("a:endParaRPr", CT_TextCharacterProperties)
+register_element_cls("a:fld", CT_TextField)
+register_element_cls("a:latin", CT_TextFont)
+register_element_cls("a:lnSpc", CT_TextSpacing)
+register_element_cls("a:normAutofit", CT_TextNormalAutofit)
+register_element_cls("a:r", CT_RegularTextRun)
+register_element_cls("a:p", CT_TextParagraph)
+register_element_cls("a:pPr", CT_TextParagraphProperties)
+register_element_cls("c:rich", CT_TextBody)
+register_element_cls("a:rPr", CT_TextCharacterProperties)
+register_element_cls("a:spcAft", CT_TextSpacing)
+register_element_cls("a:spcBef", CT_TextSpacing)
+register_element_cls("a:spcPct", CT_TextSpacingPercent)
+register_element_cls("a:spcPts", CT_TextSpacingPoint)
+register_element_cls("a:txBody", CT_TextBody)
+register_element_cls("c:txPr", CT_TextBody)
+register_element_cls("p:txBody", CT_TextBody)
+
+
+from pptx.oxml.theme import CT_OfficeStyleSheet  # noqa: E402
+
+register_element_cls("a:theme", CT_OfficeStyleSheet)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/action.py b/.venv/lib/python3.12/site-packages/pptx/oxml/action.py
new file mode 100644
index 00000000..9b31a9e1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/action.py
@@ -0,0 +1,53 @@
+"""lxml custom element classes for text-related XML elements."""
+
+from __future__ import annotations
+
+from pptx.oxml.simpletypes import XsdString
+from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute
+
+
+class CT_Hyperlink(BaseOxmlElement):
+    """Custom element class for <a:hlinkClick> elements."""
+
+    rId: str = OptionalAttribute("r:id", XsdString)  # pyright: ignore[reportAssignmentType]
+    action: str | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "action", XsdString
+    )
+
+    @property
+    def action_fields(self) -> dict[str, str]:
+        """Query portion of the `ppaction://` URL as dict.
+
+        For example `{'id':'0', 'return':'true'}` in 'ppaction://customshow?id=0&return=true'.
+
+        Returns an empty dict if the URL contains no query string or if no action attribute is
+        present.
+        """
+        url = self.action
+
+        if url is None:
+            return {}
+
+        halves = url.split("?")
+        if len(halves) == 1:
+            return {}
+
+        key_value_pairs = halves[1].split("&")
+        return dict([pair.split("=") for pair in key_value_pairs])
+
+    @property
+    def action_verb(self) -> str | None:
+        """The host portion of the `ppaction://` URL contained in the action attribute.
+
+        For example 'customshow' in 'ppaction://customshow?id=0&return=true'. Returns |None| if no
+        action attribute is present.
+        """
+        url = self.action
+
+        if url is None:
+            return None
+
+        protocol_and_host = url.split("?")[0]
+        host = protocol_and_host[11:]
+
+        return host
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/__init__.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/axis.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/axis.py
new file mode 100644
index 00000000..7129810c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/axis.py
@@ -0,0 +1,297 @@
+"""Axis-related oxml objects."""
+
+from __future__ import annotations
+
+from pptx.enum.chart import XL_AXIS_CROSSES, XL_TICK_LABEL_POSITION, XL_TICK_MARK
+from pptx.oxml.chart.shared import CT_Title
+from pptx.oxml.simpletypes import ST_AxisUnit, ST_LblOffset, ST_Orientation
+from pptx.oxml.text import CT_TextBody
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OneAndOnlyOne,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrOne,
+)
+
+
+class BaseAxisElement(BaseOxmlElement):
+    """Base class for catAx, dateAx, valAx, and perhaps other axis elements."""
+
+    @property
+    def defRPr(self):
+        """
+        ``<a:defRPr>`` great-great-grandchild element, added with its
+        ancestors if not present.
+        """
+        txPr = self.get_or_add_txPr()
+        defRPr = txPr.defRPr
+        return defRPr
+
+    @property
+    def orientation(self):
+        """Value of `val` attribute of `c:scaling/c:orientation` grandchild element.
+
+        Defaults to `ST_Orientation.MIN_MAX` if attribute or any ancestors are not
+        present.
+        """
+        orientation = self.scaling.orientation
+        if orientation is None:
+            return ST_Orientation.MIN_MAX
+        return orientation.val
+
+    @orientation.setter
+    def orientation(self, value):
+        """`value` is a member of `ST_Orientation`."""
+        self.scaling._remove_orientation()
+        if value == ST_Orientation.MAX_MIN:
+            self.scaling.get_or_add_orientation().val = value
+
+    def _new_title(self):
+        return CT_Title.new_title()
+
+    def _new_txPr(self):
+        return CT_TextBody.new_txPr()
+
+
+class CT_AxisUnit(BaseOxmlElement):
+    """Used for `c:majorUnit` and `c:minorUnit` elements, and others."""
+
+    val = RequiredAttribute("val", ST_AxisUnit)
+
+
+class CT_CatAx(BaseAxisElement):
+    """`c:catAx` element, defining a category axis."""
+
+    _tag_seq = (
+        "c:axId",
+        "c:scaling",
+        "c:delete",
+        "c:axPos",
+        "c:majorGridlines",
+        "c:minorGridlines",
+        "c:title",
+        "c:numFmt",
+        "c:majorTickMark",
+        "c:minorTickMark",
+        "c:tickLblPos",
+        "c:spPr",
+        "c:txPr",
+        "c:crossAx",
+        "c:crosses",
+        "c:crossesAt",
+        "c:auto",
+        "c:lblAlgn",
+        "c:lblOffset",
+        "c:tickLblSkip",
+        "c:tickMarkSkip",
+        "c:noMultiLvlLbl",
+        "c:extLst",
+    )
+    scaling = OneAndOnlyOne("c:scaling")
+    delete_ = ZeroOrOne("c:delete", successors=_tag_seq[3:])
+    majorGridlines = ZeroOrOne("c:majorGridlines", successors=_tag_seq[5:])
+    minorGridlines = ZeroOrOne("c:minorGridlines", successors=_tag_seq[6:])
+    title = ZeroOrOne("c:title", successors=_tag_seq[7:])
+    numFmt = ZeroOrOne("c:numFmt", successors=_tag_seq[8:])
+    majorTickMark = ZeroOrOne("c:majorTickMark", successors=_tag_seq[9:])
+    minorTickMark = ZeroOrOne("c:minorTickMark", successors=_tag_seq[10:])
+    tickLblPos = ZeroOrOne("c:tickLblPos", successors=_tag_seq[11:])
+    spPr = ZeroOrOne("c:spPr", successors=_tag_seq[12:])
+    txPr = ZeroOrOne("c:txPr", successors=_tag_seq[13:])
+    crosses = ZeroOrOne("c:crosses", successors=_tag_seq[15:])
+    crossesAt = ZeroOrOne("c:crossesAt", successors=_tag_seq[16:])
+    lblOffset = ZeroOrOne("c:lblOffset", successors=_tag_seq[19:])
+    del _tag_seq
+
+
+class CT_ChartLines(BaseOxmlElement):
+    """Used for `c:majorGridlines` and `c:minorGridlines`.
+
+    Specifies gridlines visual properties such as color and width.
+    """
+
+    spPr = ZeroOrOne("c:spPr", successors=())
+
+
+class CT_Crosses(BaseOxmlElement):
+    """`c:crosses` element, specifying where the other axis crosses this one."""
+
+    val = RequiredAttribute("val", XL_AXIS_CROSSES)
+
+
+class CT_DateAx(BaseAxisElement):
+    """`c:dateAx` element, defining a date (category) axis."""
+
+    _tag_seq = (
+        "c:axId",
+        "c:scaling",
+        "c:delete",
+        "c:axPos",
+        "c:majorGridlines",
+        "c:minorGridlines",
+        "c:title",
+        "c:numFmt",
+        "c:majorTickMark",
+        "c:minorTickMark",
+        "c:tickLblPos",
+        "c:spPr",
+        "c:txPr",
+        "c:crossAx",
+        "c:crosses",
+        "c:crossesAt",
+        "c:auto",
+        "c:lblOffset",
+        "c:baseTimeUnit",
+        "c:majorUnit",
+        "c:majorTimeUnit",
+        "c:minorUnit",
+        "c:minorTimeUnit",
+        "c:extLst",
+    )
+    scaling = OneAndOnlyOne("c:scaling")
+    delete_ = ZeroOrOne("c:delete", successors=_tag_seq[3:])
+    majorGridlines = ZeroOrOne("c:majorGridlines", successors=_tag_seq[5:])
+    minorGridlines = ZeroOrOne("c:minorGridlines", successors=_tag_seq[6:])
+    title = ZeroOrOne("c:title", successors=_tag_seq[7:])
+    numFmt = ZeroOrOne("c:numFmt", successors=_tag_seq[8:])
+    majorTickMark = ZeroOrOne("c:majorTickMark", successors=_tag_seq[9:])
+    minorTickMark = ZeroOrOne("c:minorTickMark", successors=_tag_seq[10:])
+    tickLblPos = ZeroOrOne("c:tickLblPos", successors=_tag_seq[11:])
+    spPr = ZeroOrOne("c:spPr", successors=_tag_seq[12:])
+    txPr = ZeroOrOne("c:txPr", successors=_tag_seq[13:])
+    crosses = ZeroOrOne("c:crosses", successors=_tag_seq[15:])
+    crossesAt = ZeroOrOne("c:crossesAt", successors=_tag_seq[16:])
+    lblOffset = ZeroOrOne("c:lblOffset", successors=_tag_seq[18:])
+    del _tag_seq
+
+
+class CT_LblOffset(BaseOxmlElement):
+    """`c:lblOffset` custom element class."""
+
+    val = OptionalAttribute("val", ST_LblOffset, default=100)
+
+
+class CT_Orientation(BaseOxmlElement):
+    """`c:xAx/c:scaling/c:orientation` element, defining category order.
+
+    Used to reverse the order categories appear in on a bar chart so they start at the
+    top rather than the bottom. Because we read top-to-bottom, the default way looks odd
+    to many and perhaps most folks. Also applicable to value and date axes.
+    """
+
+    val = OptionalAttribute("val", ST_Orientation, default=ST_Orientation.MIN_MAX)
+
+
+class CT_Scaling(BaseOxmlElement):
+    """`c:scaling` element.
+
+    Defines axis scale characteristics such as maximum value, log vs. linear, etc.
+    """
+
+    _tag_seq = ("c:logBase", "c:orientation", "c:max", "c:min", "c:extLst")
+    orientation = ZeroOrOne("c:orientation", successors=_tag_seq[2:])
+    max = ZeroOrOne("c:max", successors=_tag_seq[3:])
+    min = ZeroOrOne("c:min", successors=_tag_seq[4:])
+    del _tag_seq
+
+    @property
+    def maximum(self):
+        """
+        The float value of the ``<c:max>`` child element, or |None| if no max
+        element is present.
+        """
+        max = self.max
+        if max is None:
+            return None
+        return max.val
+
+    @maximum.setter
+    def maximum(self, value):
+        """
+        Set the value of the ``<c:max>`` child element to the float *value*,
+        or remove the max element if *value* is |None|.
+        """
+        self._remove_max()
+        if value is None:
+            return
+        self._add_max(val=value)
+
+    @property
+    def minimum(self):
+        """
+        The float value of the ``<c:min>`` child element, or |None| if no min
+        element is present.
+        """
+        min = self.min
+        if min is None:
+            return None
+        return min.val
+
+    @minimum.setter
+    def minimum(self, value):
+        """
+        Set the value of the ``<c:min>`` child element to the float *value*,
+        or remove the min element if *value* is |None|.
+        """
+        self._remove_min()
+        if value is None:
+            return
+        self._add_min(val=value)
+
+
+class CT_TickLblPos(BaseOxmlElement):
+    """`c:tickLblPos` element."""
+
+    val = OptionalAttribute("val", XL_TICK_LABEL_POSITION)
+
+
+class CT_TickMark(BaseOxmlElement):
+    """Used for `c:minorTickMark` and `c:majorTickMark`."""
+
+    val = OptionalAttribute("val", XL_TICK_MARK, default=XL_TICK_MARK.CROSS)
+
+
+class CT_ValAx(BaseAxisElement):
+    """`c:valAx` element, defining a value axis."""
+
+    _tag_seq = (
+        "c:axId",
+        "c:scaling",
+        "c:delete",
+        "c:axPos",
+        "c:majorGridlines",
+        "c:minorGridlines",
+        "c:title",
+        "c:numFmt",
+        "c:majorTickMark",
+        "c:minorTickMark",
+        "c:tickLblPos",
+        "c:spPr",
+        "c:txPr",
+        "c:crossAx",
+        "c:crosses",
+        "c:crossesAt",
+        "c:crossBetween",
+        "c:majorUnit",
+        "c:minorUnit",
+        "c:dispUnits",
+        "c:extLst",
+    )
+    scaling = OneAndOnlyOne("c:scaling")
+    delete_ = ZeroOrOne("c:delete", successors=_tag_seq[3:])
+    majorGridlines = ZeroOrOne("c:majorGridlines", successors=_tag_seq[5:])
+    minorGridlines = ZeroOrOne("c:minorGridlines", successors=_tag_seq[6:])
+    title = ZeroOrOne("c:title", successors=_tag_seq[7:])
+    numFmt = ZeroOrOne("c:numFmt", successors=_tag_seq[8:])
+    majorTickMark = ZeroOrOne("c:majorTickMark", successors=_tag_seq[9:])
+    minorTickMark = ZeroOrOne("c:minorTickMark", successors=_tag_seq[10:])
+    tickLblPos = ZeroOrOne("c:tickLblPos", successors=_tag_seq[11:])
+    spPr = ZeroOrOne("c:spPr", successors=_tag_seq[12:])
+    txPr = ZeroOrOne("c:txPr", successors=_tag_seq[13:])
+    crossAx = ZeroOrOne("c:crossAx", successors=_tag_seq[14:])
+    crosses = ZeroOrOne("c:crosses", successors=_tag_seq[15:])
+    crossesAt = ZeroOrOne("c:crossesAt", successors=_tag_seq[16:])
+    majorUnit = ZeroOrOne("c:majorUnit", successors=_tag_seq[18:])
+    minorUnit = ZeroOrOne("c:minorUnit", successors=_tag_seq[19:])
+    del _tag_seq
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/chart.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/chart.py
new file mode 100644
index 00000000..f4cd0dc7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/chart.py
@@ -0,0 +1,282 @@
+"""Custom element classes for top-level chart-related XML elements."""
+
+from __future__ import annotations
+
+from typing import cast
+
+from pptx.oxml import parse_xml
+from pptx.oxml.chart.shared import CT_Title
+from pptx.oxml.ns import nsdecls, qn
+from pptx.oxml.simpletypes import ST_Style, XsdString
+from pptx.oxml.text import CT_TextBody
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OneAndOnlyOne,
+    RequiredAttribute,
+    ZeroOrMore,
+    ZeroOrOne,
+)
+
+
+class CT_Chart(BaseOxmlElement):
+    """`c:chart` custom element class."""
+
+    _tag_seq = (
+        "c:title",
+        "c:autoTitleDeleted",
+        "c:pivotFmts",
+        "c:view3D",
+        "c:floor",
+        "c:sideWall",
+        "c:backWall",
+        "c:plotArea",
+        "c:legend",
+        "c:plotVisOnly",
+        "c:dispBlanksAs",
+        "c:showDLblsOverMax",
+        "c:extLst",
+    )
+    title = ZeroOrOne("c:title", successors=_tag_seq[1:])
+    autoTitleDeleted = ZeroOrOne("c:autoTitleDeleted", successors=_tag_seq[2:])
+    plotArea = OneAndOnlyOne("c:plotArea")
+    legend = ZeroOrOne("c:legend", successors=_tag_seq[9:])
+    rId: str = RequiredAttribute("r:id", XsdString)  # pyright: ignore[reportAssignmentType]
+
+    @property
+    def has_legend(self):
+        """
+        True if this chart has a legend defined, False otherwise.
+        """
+        legend = self.legend
+        if legend is None:
+            return False
+        return True
+
+    @has_legend.setter
+    def has_legend(self, bool_value):
+        """
+        Add, remove, or leave alone the ``<c:legend>`` child element depending
+        on current state and *bool_value*. If *bool_value* is |True| and no
+        ``<c:legend>`` element is present, a new default element is added.
+        When |False|, any existing legend element is removed.
+        """
+        if bool(bool_value) is False:
+            self._remove_legend()
+        else:
+            if self.legend is None:
+                self._add_legend()
+
+    @staticmethod
+    def new_chart(rId: str) -> CT_Chart:
+        """Return a new `c:chart` element."""
+        return cast(CT_Chart, parse_xml(f'<c:chart {nsdecls("c")} {nsdecls("r")} r:id="{rId}"/>'))
+
+    def _new_title(self):
+        return CT_Title.new_title()
+
+
+class CT_ChartSpace(BaseOxmlElement):
+    """`c:chartSpace` root element of a chart part."""
+
+    _tag_seq = (
+        "c:date1904",
+        "c:lang",
+        "c:roundedCorners",
+        "c:style",
+        "c:clrMapOvr",
+        "c:pivotSource",
+        "c:protection",
+        "c:chart",
+        "c:spPr",
+        "c:txPr",
+        "c:externalData",
+        "c:printSettings",
+        "c:userShapes",
+        "c:extLst",
+    )
+    date1904 = ZeroOrOne("c:date1904", successors=_tag_seq[1:])
+    style = ZeroOrOne("c:style", successors=_tag_seq[4:])
+    chart = OneAndOnlyOne("c:chart")
+    txPr = ZeroOrOne("c:txPr", successors=_tag_seq[10:])
+    externalData = ZeroOrOne("c:externalData", successors=_tag_seq[11:])
+    del _tag_seq
+
+    @property
+    def catAx_lst(self):
+        return self.chart.plotArea.catAx_lst
+
+    @property
+    def date_1904(self):
+        """
+        Return |True| if the `c:date1904` child element resolves truthy,
+        |False| otherwise. This value indicates whether date number values
+        are based on the 1900 or 1904 epoch.
+        """
+        date1904 = self.date1904
+        if date1904 is None:
+            return False
+        return date1904.val
+
+    @property
+    def dateAx_lst(self):
+        return self.xpath("c:chart/c:plotArea/c:dateAx")
+
+    def get_or_add_title(self):
+        """Return the `c:title` grandchild, newly created if not present."""
+        return self.chart.get_or_add_title()
+
+    @property
+    def plotArea(self):
+        """
+        Return the required `c:chartSpace/c:chart/c:plotArea` grandchild
+        element.
+        """
+        return self.chart.plotArea
+
+    @property
+    def valAx_lst(self):
+        return self.chart.plotArea.valAx_lst
+
+    @property
+    def xlsx_part_rId(self):
+        """
+        The string in the required ``r:id`` attribute of the
+        `<c:externalData>` child, or |None| if no externalData element is
+        present.
+        """
+        externalData = self.externalData
+        if externalData is None:
+            return None
+        return externalData.rId
+
+    def _add_externalData(self):
+        """
+        Always add a ``<c:autoUpdate val="0"/>`` child so auto-updating
+        behavior is off by default.
+        """
+        externalData = self._new_externalData()
+        externalData._add_autoUpdate(val=False)
+        self._insert_externalData(externalData)
+        return externalData
+
+    def _new_txPr(self):
+        return CT_TextBody.new_txPr()
+
+
+class CT_ExternalData(BaseOxmlElement):
+    """
+    `<c:externalData>` element, defining link to embedded Excel package part
+    containing the chart data.
+    """
+
+    autoUpdate = ZeroOrOne("c:autoUpdate")
+    rId = RequiredAttribute("r:id", XsdString)
+
+
+class CT_PlotArea(BaseOxmlElement):
+    """
+    ``<c:plotArea>`` element.
+    """
+
+    catAx = ZeroOrMore("c:catAx")
+    valAx = ZeroOrMore("c:valAx")
+
+    def iter_sers(self):
+        """
+        Generate each of the `c:ser` elements in this chart, ordered first by
+        the document order of the containing xChart element, then by their
+        ordering within the xChart element (not necessarily document order).
+        """
+        for xChart in self.iter_xCharts():
+            for ser in xChart.iter_sers():
+                yield ser
+
+    def iter_xCharts(self):
+        """
+        Generate each xChart child element in document.
+        """
+        plot_tags = (
+            qn("c:area3DChart"),
+            qn("c:areaChart"),
+            qn("c:bar3DChart"),
+            qn("c:barChart"),
+            qn("c:bubbleChart"),
+            qn("c:doughnutChart"),
+            qn("c:line3DChart"),
+            qn("c:lineChart"),
+            qn("c:ofPieChart"),
+            qn("c:pie3DChart"),
+            qn("c:pieChart"),
+            qn("c:radarChart"),
+            qn("c:scatterChart"),
+            qn("c:stockChart"),
+            qn("c:surface3DChart"),
+            qn("c:surfaceChart"),
+        )
+
+        for child in self.iterchildren():
+            if child.tag not in plot_tags:
+                continue
+            yield child
+
+    @property
+    def last_ser(self):
+        """
+        Return the last `<c:ser>` element in the last xChart element, based
+        on series order (not necessarily the same element as document order).
+        """
+        last_xChart = self.xCharts[-1]
+        sers = last_xChart.sers
+        if not sers:
+            return None
+        return sers[-1]
+
+    @property
+    def next_idx(self):
+        """
+        Return the next available `c:ser/c:idx` value within the scope of
+        this chart, the maximum idx value found on existing series,
+        incremented by one.
+        """
+        idx_vals = [s.idx.val for s in self.sers]
+        if not idx_vals:
+            return 0
+        return max(idx_vals) + 1
+
+    @property
+    def next_order(self):
+        """
+        Return the next available `c:ser/c:order` value within the scope of
+        this chart, the maximum order value found on existing series,
+        incremented by one.
+        """
+        order_vals = [s.order.val for s in self.sers]
+        if not order_vals:
+            return 0
+        return max(order_vals) + 1
+
+    @property
+    def sers(self):
+        """
+        Return a sequence containing all the `c:ser` elements in this chart,
+        ordered first by the document order of the containing xChart element,
+        then by their ordering within the xChart element (not necessarily
+        document order).
+        """
+        return tuple(self.iter_sers())
+
+    @property
+    def xCharts(self):
+        """
+        Return a sequence containing all the `c:{x}Chart` elements in this
+        chart, in document order.
+        """
+        return tuple(self.iter_xCharts())
+
+
+class CT_Style(BaseOxmlElement):
+    """
+    ``<c:style>`` element; defines the chart style.
+    """
+
+    val = RequiredAttribute("val", ST_Style)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/datalabel.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/datalabel.py
new file mode 100644
index 00000000..b6aac2fd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/datalabel.py
@@ -0,0 +1,252 @@
+"""Chart data-label related oxml objects."""
+
+from __future__ import annotations
+
+from pptx.enum.chart import XL_DATA_LABEL_POSITION
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.text import CT_TextBody
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OneAndOnlyOne,
+    RequiredAttribute,
+    ZeroOrMore,
+    ZeroOrOne,
+)
+
+
+class CT_DLbl(BaseOxmlElement):
+    """
+    ``<c:dLbl>`` element specifying the properties of the data label for an
+    individual data point.
+    """
+
+    _tag_seq = (
+        "c:idx",
+        "c:layout",
+        "c:tx",
+        "c:numFmt",
+        "c:spPr",
+        "c:txPr",
+        "c:dLblPos",
+        "c:showLegendKey",
+        "c:showVal",
+        "c:showCatName",
+        "c:showSerName",
+        "c:showPercent",
+        "c:showBubbleSize",
+        "c:separator",
+        "c:extLst",
+    )
+    idx = OneAndOnlyOne("c:idx")
+    tx = ZeroOrOne("c:tx", successors=_tag_seq[3:])
+    spPr = ZeroOrOne("c:spPr", successors=_tag_seq[5:])
+    txPr = ZeroOrOne("c:txPr", successors=_tag_seq[6:])
+    dLblPos = ZeroOrOne("c:dLblPos", successors=_tag_seq[7:])
+    del _tag_seq
+
+    def get_or_add_rich(self):
+        """
+        Return the `c:rich` descendant representing the text frame of the
+        data label, newly created if not present. Any existing `c:strRef`
+        element is removed along with its contents.
+        """
+        tx = self.get_or_add_tx()
+        tx._remove_strRef()
+        return tx.get_or_add_rich()
+
+    def get_or_add_tx_rich(self):
+        """
+        Return the `c:tx[c:rich]` subtree, newly created if not present.
+        """
+        tx = self.get_or_add_tx()
+        tx._remove_strRef()
+        tx.get_or_add_rich()
+        return tx
+
+    @property
+    def idx_val(self):
+        """
+        The integer value of the `val` attribute on the required `c:idx`
+        child.
+        """
+        return self.idx.val
+
+    @classmethod
+    def new_dLbl(cls):
+        """Return a newly created "loose" `c:dLbl` element.
+
+        The `c:dLbl` element contains the same (fairly extensive) default
+        subtree added by PowerPoint when an individual data label is
+        customized in the UI. Note that the idx value must be set by the
+        client. Failure to set the idx value will likely result in any
+        changes not being visible and may result in a repair error on open.
+        """
+        return parse_xml(
+            "<c:dLbl %s>\n"
+            '  <c:idx val="666"/>\n'
+            "  <c:spPr/>\n"
+            "  <c:txPr>\n"
+            "    <a:bodyPr/>\n"
+            "    <a:lstStyle/>\n"
+            "    <a:p>\n"
+            "      <a:pPr>\n"
+            "        <a:defRPr/>\n"
+            "      </a:pPr>\n"
+            "    </a:p>\n"
+            "  </c:txPr>\n"
+            '  <c:showLegendKey val="0"/>\n'
+            '  <c:showVal val="1"/>\n'
+            '  <c:showCatName val="0"/>\n'
+            '  <c:showSerName val="0"/>\n'
+            '  <c:showPercent val="0"/>\n'
+            '  <c:showBubbleSize val="0"/>\n'
+            "</c:dLbl>" % nsdecls("c", "a")
+        )
+
+    def remove_tx_rich(self):
+        """
+        Remove any `c:tx[c:rich]` child, or do nothing if not present.
+        """
+        matches = self.xpath("c:tx[c:rich]")
+        if not matches:
+            return
+        tx = matches[0]
+        self.remove(tx)
+
+    def _new_txPr(self):
+        return CT_TextBody.new_txPr()
+
+
+class CT_DLblPos(BaseOxmlElement):
+    """
+    ``<c:dLblPos>`` element specifying the positioning of a data label with
+    respect to its data point.
+    """
+
+    val = RequiredAttribute("val", XL_DATA_LABEL_POSITION)
+
+
+class CT_DLbls(BaseOxmlElement):
+    """`c:dLbls` element specifying properties for a set of data labels."""
+
+    _tag_seq = (
+        "c:dLbl",
+        "c:numFmt",
+        "c:spPr",
+        "c:txPr",
+        "c:dLblPos",
+        "c:showLegendKey",
+        "c:showVal",
+        "c:showCatName",
+        "c:showSerName",
+        "c:showPercent",
+        "c:showBubbleSize",
+        "c:separator",
+        "c:showLeaderLines",
+        "c:leaderLines",
+        "c:extLst",
+    )
+    dLbl = ZeroOrMore("c:dLbl", successors=_tag_seq[1:])
+    numFmt = ZeroOrOne("c:numFmt", successors=_tag_seq[2:])
+    txPr = ZeroOrOne("c:txPr", successors=_tag_seq[4:])
+    dLblPos = ZeroOrOne("c:dLblPos", successors=_tag_seq[5:])
+    showLegendKey = ZeroOrOne("c:showLegendKey", successors=_tag_seq[6:])
+    showVal = ZeroOrOne("c:showVal", successors=_tag_seq[7:])
+    showCatName = ZeroOrOne("c:showCatName", successors=_tag_seq[8:])
+    showSerName = ZeroOrOne("c:showSerName", successors=_tag_seq[9:])
+    showPercent = ZeroOrOne("c:showPercent", successors=_tag_seq[10:])
+    del _tag_seq
+
+    @property
+    def defRPr(self):
+        """
+        ``<a:defRPr>`` great-great-grandchild element, added with its
+        ancestors if not present.
+        """
+        txPr = self.get_or_add_txPr()
+        defRPr = txPr.defRPr
+        return defRPr
+
+    def get_dLbl_for_point(self, idx):
+        """
+        Return the `c:dLbl` child representing the label for the data point
+        at index *idx*.
+        """
+        matches = self.xpath('c:dLbl[c:idx[@val="%d"]]' % idx)
+        if matches:
+            return matches[0]
+        return None
+
+    def get_or_add_dLbl_for_point(self, idx):
+        """
+        Return the `c:dLbl` element representing the label of the point at
+        index *idx*.
+        """
+        matches = self.xpath('c:dLbl[c:idx[@val="%d"]]' % idx)
+        if matches:
+            return matches[0]
+        return self._insert_dLbl_in_sequence(idx)
+
+    @classmethod
+    def new_dLbls(cls):
+        """Return a newly created "loose" `c:dLbls` element."""
+        return parse_xml(
+            "<c:dLbls %s>\n"
+            '  <c:showLegendKey val="0"/>\n'
+            '  <c:showVal val="0"/>\n'
+            '  <c:showCatName val="0"/>\n'
+            '  <c:showSerName val="0"/>\n'
+            '  <c:showPercent val="0"/>\n'
+            '  <c:showBubbleSize val="0"/>\n'
+            '  <c:showLeaderLines val="1"/>\n'
+            "</c:dLbls>" % nsdecls("c")
+        )
+
+    def _insert_dLbl_in_sequence(self, idx):
+        """
+        Return a newly created `c:dLbl` element having `c:idx` child of *idx*
+        and inserted in numeric sequence among the `c:dLbl` children of this
+        element.
+        """
+        new_dLbl = self._new_dLbl()
+        new_dLbl.idx.val = idx
+
+        dLbl = None
+        for dLbl in self.dLbl_lst:
+            if dLbl.idx_val > idx:
+                dLbl.addprevious(new_dLbl)
+                return new_dLbl
+        if dLbl is not None:
+            dLbl.addnext(new_dLbl)
+        else:
+            self.insert(0, new_dLbl)
+        return new_dLbl
+
+    def _new_dLbl(self):
+        return CT_DLbl.new_dLbl()
+
+    def _new_showCatName(self):
+        """Return a new `c:showCatName` with value initialized.
+
+        This method is called by the metaclass-generated code whenever a new
+        `c:showCatName` element is required. In this case, it defaults to
+        `val=true`, which is not what we need so we override to make val
+        explicitly False.
+        """
+        return parse_xml('<c:showCatName %s val="0"/>' % nsdecls("c"))
+
+    def _new_showLegendKey(self):
+        return parse_xml('<c:showLegendKey %s val="0"/>' % nsdecls("c"))
+
+    def _new_showPercent(self):
+        return parse_xml('<c:showPercent %s val="0"/>' % nsdecls("c"))
+
+    def _new_showSerName(self):
+        return parse_xml('<c:showSerName %s val="0"/>' % nsdecls("c"))
+
+    def _new_showVal(self):
+        return parse_xml('<c:showVal %s val="0"/>' % nsdecls("c"))
+
+    def _new_txPr(self):
+        return CT_TextBody.new_txPr()
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/legend.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/legend.py
new file mode 100644
index 00000000..196ca15d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/legend.py
@@ -0,0 +1,72 @@
+"""lxml custom element classes for legend-related XML elements."""
+
+from __future__ import annotations
+
+from pptx.enum.chart import XL_LEGEND_POSITION
+from pptx.oxml.text import CT_TextBody
+from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, ZeroOrOne
+
+
+class CT_Legend(BaseOxmlElement):
+    """
+    ``<c:legend>`` custom element class
+    """
+
+    _tag_seq = (
+        "c:legendPos",
+        "c:legendEntry",
+        "c:layout",
+        "c:overlay",
+        "c:spPr",
+        "c:txPr",
+        "c:extLst",
+    )
+    legendPos = ZeroOrOne("c:legendPos", successors=_tag_seq[1:])
+    layout = ZeroOrOne("c:layout", successors=_tag_seq[3:])
+    overlay = ZeroOrOne("c:overlay", successors=_tag_seq[4:])
+    txPr = ZeroOrOne("c:txPr", successors=_tag_seq[6:])
+    del _tag_seq
+
+    @property
+    def defRPr(self):
+        """
+        `./c:txPr/a:p/a:pPr/a:defRPr` great-great-grandchild element, added
+        with its ancestors if not present.
+        """
+        txPr = self.get_or_add_txPr()
+        defRPr = txPr.defRPr
+        return defRPr
+
+    @property
+    def horz_offset(self):
+        """
+        The float value in ./c:layout/c:manualLayout/c:x when
+        ./c:layout/c:manualLayout/c:xMode@val == "factor". 0.0 if that
+        XPath expression has no match.
+        """
+        layout = self.layout
+        if layout is None:
+            return 0.0
+        return layout.horz_offset
+
+    @horz_offset.setter
+    def horz_offset(self, offset):
+        """
+        Set the value of ./c:layout/c:manualLayout/c:x@val to *offset* and
+        ./c:layout/c:manualLayout/c:xMode@val to "factor". Remove
+        ./c:layout/c:manualLayout if *offset* == 0.
+        """
+        layout = self.get_or_add_layout()
+        layout.horz_offset = offset
+
+    def _new_txPr(self):
+        return CT_TextBody.new_txPr()
+
+
+class CT_LegendPos(BaseOxmlElement):
+    """
+    ``<c:legendPos>`` element specifying position of legend with respect to
+    chart as a member of ST_LegendPos.
+    """
+
+    val = OptionalAttribute("val", XL_LEGEND_POSITION, default=XL_LEGEND_POSITION.RIGHT)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/marker.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/marker.py
new file mode 100644
index 00000000..34afd13d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/marker.py
@@ -0,0 +1,61 @@
+"""Series-related oxml objects."""
+
+from __future__ import annotations
+
+from pptx.enum.chart import XL_MARKER_STYLE
+from pptx.oxml.simpletypes import ST_MarkerSize
+from pptx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrOne
+
+
+class CT_Marker(BaseOxmlElement):
+    """
+    `c:marker` custom element class, containing visual properties for a data
+    point marker on line-type charts.
+    """
+
+    _tag_seq = ("c:symbol", "c:size", "c:spPr", "c:extLst")
+    symbol = ZeroOrOne("c:symbol", successors=_tag_seq[1:])
+    size = ZeroOrOne("c:size", successors=_tag_seq[2:])
+    spPr = ZeroOrOne("c:spPr", successors=_tag_seq[3:])
+    del _tag_seq
+
+    @property
+    def size_val(self):
+        """
+        Return the value of `./c:size/@val`, specifying the size of this
+        marker in points. Returns |None| if no `c:size` element is present or
+        its val attribute is not present.
+        """
+        size = self.size
+        if size is None:
+            return None
+        return size.val
+
+    @property
+    def symbol_val(self):
+        """
+        Return the value of `./c:symbol/@val`, specifying the shape of this
+        marker. Returns |None| if no `c:symbol` element is present.
+        """
+        symbol = self.symbol
+        if symbol is None:
+            return None
+        return symbol.val
+
+
+class CT_MarkerSize(BaseOxmlElement):
+    """
+    `c:size` custom element class, specifying the size (in points) of a data
+    point marker for a line, XY, or radar chart.
+    """
+
+    val = RequiredAttribute("val", ST_MarkerSize)
+
+
+class CT_MarkerStyle(BaseOxmlElement):
+    """
+    `c:symbol` custom element class, specifying the shape of a data point
+    marker for a line, XY, or radar chart.
+    """
+
+    val = RequiredAttribute("val", XL_MARKER_STYLE)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/plot.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/plot.py
new file mode 100644
index 00000000..9c695a43
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/plot.py
@@ -0,0 +1,345 @@
+"""Plot-related oxml objects."""
+
+from __future__ import annotations
+
+from pptx.oxml.chart.datalabel import CT_DLbls
+from pptx.oxml.simpletypes import (
+    ST_BarDir,
+    ST_BubbleScale,
+    ST_GapAmount,
+    ST_Grouping,
+    ST_Overlap,
+)
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OneAndOnlyOne,
+    OptionalAttribute,
+    ZeroOrMore,
+    ZeroOrOne,
+)
+
+
+class BaseChartElement(BaseOxmlElement):
+    """
+    Base class for barChart, lineChart, and other plot elements.
+    """
+
+    @property
+    def cat(self):
+        """
+        Return the `c:cat` element of the first series in this xChart, or
+        |None| if not present.
+        """
+        cats = self.xpath("./c:ser[1]/c:cat")
+        return cats[0] if cats else None
+
+    @property
+    def cat_pt_count(self):
+        """
+        Return the value of the `c:ptCount` descendent of this xChart
+        element. Its parent can be one of three element types. This value
+        represents the true number of (leaf) categories, although they might
+        not all have a corresponding `c:pt` sibling; a category with no label
+        does not get a `c:pt` element. Returns 0 if there is no `c:ptCount`
+        descendent.
+        """
+        cat_ptCounts = self.xpath("./c:ser//c:cat//c:ptCount")
+        if not cat_ptCounts:
+            return 0
+        return cat_ptCounts[0].val
+
+    @property
+    def cat_pts(self):
+        """
+        Return a sequence representing the `c:pt` elements under the `c:cat`
+        element of the first series in this xChart element. A category having
+        no value will have no corresponding `c:pt` element; |None| will
+        appear in that position in such cases. Items appear in `idx` order.
+        Only those in the first ``<c:lvl>`` element are included in the case
+        of multi-level categories.
+        """
+        cat_pts = self.xpath("./c:ser[1]/c:cat//c:lvl[1]/c:pt")
+        if not cat_pts:
+            cat_pts = self.xpath("./c:ser[1]/c:cat//c:pt")
+
+        cat_pt_dict = dict((pt.idx, pt) for pt in cat_pts)
+
+        return [cat_pt_dict.get(idx, None) for idx in range(self.cat_pt_count)]
+
+    @property
+    def grouping_val(self):
+        """
+        Return the value of the ``./c:grouping{val=?}`` attribute, taking
+        defaults into account when items are not present.
+        """
+        grouping = self.grouping
+        if grouping is None:
+            return ST_Grouping.STANDARD
+        val = grouping.val
+        if val is None:
+            return ST_Grouping.STANDARD
+        return val
+
+    def iter_sers(self):
+        """
+        Generate each ``<c:ser>`` child element in this xChart in
+        c:order/@val sequence (not document or c:idx order).
+        """
+
+        def ser_order(ser):
+            return ser.order.val
+
+        return (ser for ser in sorted(self.xpath("./c:ser"), key=ser_order))
+
+    @property
+    def sers(self):
+        """
+        Sequence of ``<c:ser>`` child elements in this xChart in c:order/@val
+        sequence (not document or c:idx order).
+        """
+        return tuple(self.iter_sers())
+
+    def _new_dLbls(self):
+        return CT_DLbls.new_dLbls()
+
+
+class CT_Area3DChart(BaseChartElement):
+    """
+    ``<c:area3DChart>`` element.
+    """
+
+    grouping = ZeroOrOne(
+        "c:grouping",
+        successors=(
+            "c:varyColors",
+            "c:ser",
+            "c:dLbls",
+            "c:dropLines",
+            "c:gapDepth",
+            "c:axId",
+        ),
+    )
+
+
+class CT_AreaChart(BaseChartElement):
+    """
+    ``<c:areaChart>`` element.
+    """
+
+    _tag_seq = (
+        "c:grouping",
+        "c:varyColors",
+        "c:ser",
+        "c:dLbls",
+        "c:dropLines",
+        "c:axId",
+        "c:extLst",
+    )
+    grouping = ZeroOrOne("c:grouping", successors=_tag_seq[1:])
+    varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[2:])
+    ser = ZeroOrMore("c:ser", successors=_tag_seq[3:])
+    dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[4:])
+    del _tag_seq
+
+
+class CT_BarChart(BaseChartElement):
+    """
+    ``<c:barChart>`` element.
+    """
+
+    _tag_seq = (
+        "c:barDir",
+        "c:grouping",
+        "c:varyColors",
+        "c:ser",
+        "c:dLbls",
+        "c:gapWidth",
+        "c:overlap",
+        "c:serLines",
+        "c:axId",
+        "c:extLst",
+    )
+    barDir = OneAndOnlyOne("c:barDir")
+    grouping = ZeroOrOne("c:grouping", successors=_tag_seq[2:])
+    varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[3:])
+    ser = ZeroOrMore("c:ser", successors=_tag_seq[4:])
+    dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[5:])
+    gapWidth = ZeroOrOne("c:gapWidth", successors=_tag_seq[6:])
+    overlap = ZeroOrOne("c:overlap", successors=_tag_seq[7:])
+    del _tag_seq
+
+    @property
+    def grouping_val(self):
+        """
+        Return the value of the ``./c:grouping{val=?}`` attribute, taking
+        defaults into account when items are not present.
+        """
+        grouping = self.grouping
+        if grouping is None:
+            return ST_Grouping.CLUSTERED
+        val = grouping.val
+        if val is None:
+            return ST_Grouping.CLUSTERED
+        return val
+
+
+class CT_BarDir(BaseOxmlElement):
+    """
+    ``<c:barDir>`` child of a barChart element, specifying the orientation of
+    the bars, 'bar' if they are horizontal and 'col' if they are vertical.
+    """
+
+    val = OptionalAttribute("val", ST_BarDir, default=ST_BarDir.COL)
+
+
+class CT_BubbleChart(BaseChartElement):
+    """
+    ``<c:bubbleChart>`` custom element class
+    """
+
+    _tag_seq = (
+        "c:varyColors",
+        "c:ser",
+        "c:dLbls",
+        "c:axId",
+        "c:bubble3D",
+        "c:bubbleScale",
+        "c:showNegBubbles",
+        "c:sizeRepresents",
+        "c:axId",
+        "c:extLst",
+    )
+    ser = ZeroOrMore("c:ser", successors=_tag_seq[2:])
+    dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[3:])
+    bubble3D = ZeroOrOne("c:bubble3D", successors=_tag_seq[5:])
+    bubbleScale = ZeroOrOne("c:bubbleScale", successors=_tag_seq[6:])
+    del _tag_seq
+
+
+class CT_BubbleScale(BaseChartElement):
+    """
+    ``<c:bubbleScale>`` custom element class
+    """
+
+    val = OptionalAttribute("val", ST_BubbleScale, default=100)
+
+
+class CT_DoughnutChart(BaseChartElement):
+    """
+    ``<c:doughnutChart>`` element.
+    """
+
+    _tag_seq = (
+        "c:varyColors",
+        "c:ser",
+        "c:dLbls",
+        "c:firstSliceAng",
+        "c:holeSize",
+        "c:extLst",
+    )
+    varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[1:])
+    ser = ZeroOrMore("c:ser", successors=_tag_seq[2:])
+    dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[3:])
+    del _tag_seq
+
+
+class CT_GapAmount(BaseOxmlElement):
+    """
+    ``<c:gapWidth>`` child of ``<c:barChart>`` element, also used for other
+    purposes like error bars.
+    """
+
+    val = OptionalAttribute("val", ST_GapAmount, default=150)
+
+
+class CT_Grouping(BaseOxmlElement):
+    """
+    ``<c:grouping>`` child of an xChart element, specifying a value like
+    'clustered' or 'stacked'. Also used for variants with the same tag name
+    like CT_BarGrouping.
+    """
+
+    val = OptionalAttribute("val", ST_Grouping)
+
+
+class CT_LineChart(BaseChartElement):
+    """
+    ``<c:lineChart>`` custom element class
+    """
+
+    _tag_seq = (
+        "c:grouping",
+        "c:varyColors",
+        "c:ser",
+        "c:dLbls",
+        "c:dropLines",
+        "c:hiLowLines",
+        "c:upDownBars",
+        "c:marker",
+        "c:smooth",
+        "c:axId",
+        "c:extLst",
+    )
+    grouping = ZeroOrOne("c:grouping", successors=(_tag_seq[1:]))
+    varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[2:])
+    ser = ZeroOrMore("c:ser", successors=_tag_seq[3:])
+    dLbls = ZeroOrOne("c:dLbls", successors=(_tag_seq[4:]))
+    del _tag_seq
+
+
+class CT_Overlap(BaseOxmlElement):
+    """
+    ``<c:overlap>`` element specifying bar overlap as an integer percentage
+    of bar width, in range -100 to 100.
+    """
+
+    val = OptionalAttribute("val", ST_Overlap, default=0)
+
+
+class CT_PieChart(BaseChartElement):
+    """
+    ``<c:pieChart>`` custom element class
+    """
+
+    _tag_seq = ("c:varyColors", "c:ser", "c:dLbls", "c:firstSliceAng", "c:extLst")
+    varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[1:])
+    ser = ZeroOrMore("c:ser", successors=_tag_seq[2:])
+    dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[3:])
+    del _tag_seq
+
+
+class CT_RadarChart(BaseChartElement):
+    """
+    ``<c:radarChart>`` custom element class
+    """
+
+    _tag_seq = (
+        "c:radarStyle",
+        "c:varyColors",
+        "c:ser",
+        "c:dLbls",
+        "c:axId",
+        "c:extLst",
+    )
+    varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[2:])
+    ser = ZeroOrMore("c:ser", successors=_tag_seq[3:])
+    dLbls = ZeroOrOne("c:dLbls", successors=(_tag_seq[4:]))
+    del _tag_seq
+
+
+class CT_ScatterChart(BaseChartElement):
+    """
+    ``<c:scatterChart>`` custom element class
+    """
+
+    _tag_seq = (
+        "c:scatterStyle",
+        "c:varyColors",
+        "c:ser",
+        "c:dLbls",
+        "c:axId",
+        "c:extLst",
+    )
+    varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[2:])
+    ser = ZeroOrMore("c:ser", successors=_tag_seq[3:])
+    del _tag_seq
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/series.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/series.py
new file mode 100644
index 00000000..9264d552
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/series.py
@@ -0,0 +1,254 @@
+"""Series-related oxml objects."""
+
+from __future__ import annotations
+
+from pptx.oxml.chart.datalabel import CT_DLbls
+from pptx.oxml.simpletypes import XsdUnsignedInt
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OneAndOnlyOne,
+    OxmlElement,
+    RequiredAttribute,
+    ZeroOrMore,
+    ZeroOrOne,
+)
+
+
+class CT_AxDataSource(BaseOxmlElement):
+    """
+    ``<c:cat>`` custom element class used in category charts to specify
+    category labels and hierarchy.
+    """
+
+    multiLvlStrRef = ZeroOrOne("c:multiLvlStrRef", successors=())
+
+    @property
+    def lvls(self):
+        """
+        Return a list containing the `c:lvl` descendent elements in document
+        order. These will only be present when the required single child
+        is a `c:multiLvlStrRef` element. Returns an empty list when no
+        `c:lvl` descendent elements are present.
+        """
+        return self.xpath(".//c:lvl")
+
+
+class CT_DPt(BaseOxmlElement):
+    """
+    ``<c:dPt>`` custom element class, containing visual properties for a data
+    point.
+    """
+
+    _tag_seq = (
+        "c:idx",
+        "c:invertIfNegative",
+        "c:marker",
+        "c:bubble3D",
+        "c:explosion",
+        "c:spPr",
+        "c:pictureOptions",
+        "c:extLst",
+    )
+    idx = OneAndOnlyOne("c:idx")
+    marker = ZeroOrOne("c:marker", successors=_tag_seq[3:])
+    spPr = ZeroOrOne("c:spPr", successors=_tag_seq[6:])
+    del _tag_seq
+
+    @classmethod
+    def new_dPt(cls):
+        """
+        Return a newly created "loose" `c:dPt` element containing its default
+        subtree.
+        """
+        dPt = OxmlElement("c:dPt")
+        dPt.append(OxmlElement("c:idx"))
+        return dPt
+
+
+class CT_Lvl(BaseOxmlElement):
+    """
+    ``<c:lvl>`` custom element class used in multi-level categories to
+    specify a level of hierarchy.
+    """
+
+    pt = ZeroOrMore("c:pt", successors=())
+
+
+class CT_NumDataSource(BaseOxmlElement):
+    """
+    ``<c:yVal>`` custom element class used in XY and bubble charts, and
+    perhaps others.
+    """
+
+    numRef = OneAndOnlyOne("c:numRef")
+
+    @property
+    def ptCount_val(self):
+        """
+        Return the value of `./c:numRef/c:numCache/c:ptCount/@val`,
+        specifying how many `c:pt` elements are in this numeric data cache.
+        Returns 0 if no `c:ptCount` element is present, as this is the least
+        disruptive way to degrade when no cached point data is available.
+        This situation is not expected, but is valid according to the schema.
+        """
+        results = self.xpath(".//c:ptCount/@val")
+        return int(results[0]) if results else 0
+
+    def pt_v(self, idx):
+        """
+        Return the Y value for data point *idx* in this cache, or None if no
+        value is present for that data point.
+        """
+        results = self.xpath(".//c:pt[@idx=%d]" % idx)
+        return results[0].value if results else None
+
+
+class CT_SeriesComposite(BaseOxmlElement):
+    """
+    ``<c:ser>`` custom element class. Note there are several different series
+    element types in the schema, such as ``CT_LineSer`` and ``CT_BarSer``,
+    but they all share the same tag name. This class acts as a composite and
+    depends on the caller not to do anything invalid for a series belonging
+    to a particular plot type.
+    """
+
+    _tag_seq = (
+        "c:idx",
+        "c:order",
+        "c:tx",
+        "c:spPr",
+        "c:invertIfNegative",
+        "c:pictureOptions",
+        "c:marker",
+        "c:explosion",
+        "c:dPt",
+        "c:dLbls",
+        "c:trendline",
+        "c:errBars",
+        "c:cat",
+        "c:val",
+        "c:xVal",
+        "c:yVal",
+        "c:shape",
+        "c:smooth",
+        "c:bubbleSize",
+        "c:bubble3D",
+        "c:extLst",
+    )
+    idx = OneAndOnlyOne("c:idx")
+    order = OneAndOnlyOne("c:order")
+    tx = ZeroOrOne("c:tx", successors=_tag_seq[3:])
+    spPr = ZeroOrOne("c:spPr", successors=_tag_seq[4:])
+    invertIfNegative = ZeroOrOne("c:invertIfNegative", successors=_tag_seq[5:])
+    marker = ZeroOrOne("c:marker", successors=_tag_seq[7:])
+    dPt = ZeroOrMore("c:dPt", successors=_tag_seq[9:])
+    dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[10:])
+    cat = ZeroOrOne("c:cat", successors=_tag_seq[13:])
+    val = ZeroOrOne("c:val", successors=_tag_seq[14:])
+    xVal = ZeroOrOne("c:xVal", successors=_tag_seq[15:])
+    yVal = ZeroOrOne("c:yVal", successors=_tag_seq[16:])
+    smooth = ZeroOrOne("c:smooth", successors=_tag_seq[18:])
+    bubbleSize = ZeroOrOne("c:bubbleSize", successors=_tag_seq[19:])
+    del _tag_seq
+
+    @property
+    def bubbleSize_ptCount_val(self):
+        """
+        Return the number of bubble size values as reflected in the `val`
+        attribute of `./c:bubbleSize//c:ptCount`, or 0 if not present.
+        """
+        vals = self.xpath("./c:bubbleSize//c:ptCount/@val")
+        if not vals:
+            return 0
+        return int(vals[0])
+
+    @property
+    def cat_ptCount_val(self):
+        """
+        Return the number of categories as reflected in the `val` attribute
+        of `./c:cat//c:ptCount`, or 0 if not present.
+        """
+        vals = self.xpath("./c:cat//c:ptCount/@val")
+        if not vals:
+            return 0
+        return int(vals[0])
+
+    def get_dLbl(self, idx):
+        """
+        Return the `c:dLbl` element representing the label for the data point
+        at offset *idx* in this series, or |None| if not present.
+        """
+        dLbls = self.dLbls
+        if dLbls is None:
+            return None
+        return dLbls.get_dLbl_for_point(idx)
+
+    def get_or_add_dLbl(self, idx):
+        """
+        Return the `c:dLbl` element representing the label of the point at
+        offset *idx* in this series, newly created if not yet present.
+        """
+        dLbls = self.get_or_add_dLbls()
+        return dLbls.get_or_add_dLbl_for_point(idx)
+
+    def get_or_add_dPt_for_point(self, idx):
+        """
+        Return the `c:dPt` child representing the visual properties of the
+        data point at index *idx*.
+        """
+        matches = self.xpath('c:dPt[c:idx[@val="%d"]]' % idx)
+        if matches:
+            return matches[0]
+        dPt = self._add_dPt()
+        dPt.idx.val = idx
+        return dPt
+
+    @property
+    def xVal_ptCount_val(self):
+        """
+        Return the number of X values as reflected in the `val` attribute of
+        `./c:xVal//c:ptCount`, or 0 if not present.
+        """
+        vals = self.xpath("./c:xVal//c:ptCount/@val")
+        if not vals:
+            return 0
+        return int(vals[0])
+
+    @property
+    def yVal_ptCount_val(self):
+        """
+        Return the number of Y values as reflected in the `val` attribute of
+        `./c:yVal//c:ptCount`, or 0 if not present.
+        """
+        vals = self.xpath("./c:yVal//c:ptCount/@val")
+        if not vals:
+            return 0
+        return int(vals[0])
+
+    def _new_dLbls(self):
+        """Override metaclass method that creates `c:dLbls` element."""
+        return CT_DLbls.new_dLbls()
+
+    def _new_dPt(self):
+        """
+        Overrides the metaclass generated method to get `c:dPt` with minimal
+        subtree.
+        """
+        return CT_DPt.new_dPt()
+
+
+class CT_StrVal_NumVal_Composite(BaseOxmlElement):
+    """
+    ``<c:pt>`` element, can be either CT_StrVal or CT_NumVal complex type.
+    Using this class for both, differentiating as needed.
+    """
+
+    v = OneAndOnlyOne("c:v")
+    idx = RequiredAttribute("idx", XsdUnsignedInt)
+
+    @property
+    def value(self):
+        """
+        The float value of the text in the required ``<c:v>`` child.
+        """
+        return float(self.v.text)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/shared.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/shared.py
new file mode 100644
index 00000000..5515aa4b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/shared.py
@@ -0,0 +1,219 @@
+"""Shared oxml objects for charts."""
+
+from __future__ import annotations
+
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.simpletypes import (
+    ST_LayoutMode,
+    XsdBoolean,
+    XsdDouble,
+    XsdString,
+    XsdUnsignedInt,
+)
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrOne,
+)
+
+
+class CT_Boolean(BaseOxmlElement):
+    """
+    Common complex type used for elements having a True/False value.
+    """
+
+    val = OptionalAttribute("val", XsdBoolean, default=True)
+
+
+class CT_Boolean_Explicit(BaseOxmlElement):
+    """Always spells out the `val` attribute, e.g. `val=1`.
+
+    At least one boolean element is improperly interpreted by one or more
+    versions of PowerPoint. The `c:overlay` element is interpreted as |False|
+    when no `val` attribute is present, contrary to the behavior described in
+    the schema. A remedy for this is to interpret a missing `val` attribute
+    as |True| (consistent with the spec), but always write the attribute
+    whenever there is occasion for changing the element.
+    """
+
+    _val = OptionalAttribute("val", XsdBoolean, default=True)
+
+    @property
+    def val(self):
+        return self._val
+
+    @val.setter
+    def val(self, value):
+        val_str = "1" if bool(value) is True else "0"
+        self.set("val", val_str)
+
+
+class CT_Double(BaseOxmlElement):
+    """
+    Used for floating point values.
+    """
+
+    val = RequiredAttribute("val", XsdDouble)
+
+
+class CT_Layout(BaseOxmlElement):
+    """
+    ``<c:layout>`` custom element class
+    """
+
+    manualLayout = ZeroOrOne("c:manualLayout", successors=("c:extLst",))
+
+    @property
+    def horz_offset(self):
+        """
+        The float value in ./c:manualLayout/c:x when
+        c:layout/c:manualLayout/c:xMode@val == "factor". 0.0 if that XPath
+        expression finds no match.
+        """
+        manualLayout = self.manualLayout
+        if manualLayout is None:
+            return 0.0
+        return manualLayout.horz_offset
+
+    @horz_offset.setter
+    def horz_offset(self, offset):
+        """
+        Set the value of ./c:manualLayout/c:x@val to *offset* and
+        ./c:manualLayout/c:xMode@val to "factor". Remove ./c:manualLayout if
+        *offset* == 0.
+        """
+        if offset == 0.0:
+            self._remove_manualLayout()
+            return
+        manualLayout = self.get_or_add_manualLayout()
+        manualLayout.horz_offset = offset
+
+
+class CT_LayoutMode(BaseOxmlElement):
+    """
+    Used for ``<c:xMode>``, ``<c:yMode>``, ``<c:wMode>``, and ``<c:hMode>``
+    child elements of CT_ManualLayout.
+    """
+
+    val = OptionalAttribute("val", ST_LayoutMode, default=ST_LayoutMode.FACTOR)
+
+
+class CT_ManualLayout(BaseOxmlElement):
+    """
+    ``<c:manualLayout>`` custom element class
+    """
+
+    _tag_seq = (
+        "c:layoutTarget",
+        "c:xMode",
+        "c:yMode",
+        "c:wMode",
+        "c:hMode",
+        "c:x",
+        "c:y",
+        "c:w",
+        "c:h",
+        "c:extLst",
+    )
+    xMode = ZeroOrOne("c:xMode", successors=_tag_seq[2:])
+    x = ZeroOrOne("c:x", successors=_tag_seq[6:])
+    del _tag_seq
+
+    @property
+    def horz_offset(self):
+        """
+        The float value in ./c:x@val when ./c:xMode@val == "factor". 0.0 when
+        ./c:x is not present or ./c:xMode@val != "factor".
+        """
+        x, xMode = self.x, self.xMode
+        if x is None or xMode is None or xMode.val != ST_LayoutMode.FACTOR:
+            return 0.0
+        return x.val
+
+    @horz_offset.setter
+    def horz_offset(self, offset):
+        """
+        Set the value of ./c:x@val to *offset* and ./c:xMode@val to "factor".
+        """
+        self.get_or_add_xMode().val = ST_LayoutMode.FACTOR
+        self.get_or_add_x().val = offset
+
+
+class CT_NumFmt(BaseOxmlElement):
+    """
+    ``<c:numFmt>`` element specifying the formatting for number labels on a
+    tick mark or data point.
+    """
+
+    formatCode = RequiredAttribute("formatCode", XsdString)
+    sourceLinked = OptionalAttribute("sourceLinked", XsdBoolean)
+
+
+class CT_Title(BaseOxmlElement):
+    """`c:title` custom element class."""
+
+    _tag_seq = ("c:tx", "c:layout", "c:overlay", "c:spPr", "c:txPr", "c:extLst")
+    tx = ZeroOrOne("c:tx", successors=_tag_seq[1:])
+    spPr = ZeroOrOne("c:spPr", successors=_tag_seq[4:])
+    del _tag_seq
+
+    def get_or_add_tx_rich(self):
+        """Return `c:tx/c:rich`, newly created if not present.
+
+        Return the `c:rich` grandchild at `c:tx/c:rich`. Both the `c:tx` and
+        `c:rich` elements are created if not already present. Any
+        `c:tx/c:strRef` element is removed. (Such an element would contain
+        a cell reference for the axis title text in the chart's Excel
+        worksheet.)
+        """
+        tx = self.get_or_add_tx()
+        tx._remove_strRef()
+        return tx.get_or_add_rich()
+
+    @property
+    def tx_rich(self):
+        """Return `c:tx/c:rich` or |None| if not present."""
+        richs = self.xpath("c:tx/c:rich")
+        if not richs:
+            return None
+        return richs[0]
+
+    @staticmethod
+    def new_title():
+        """Return "loose" `c:title` element containing default children."""
+        return parse_xml(
+            "<c:title %s>" "  <c:layout/>" '  <c:overlay val="0"/>' "</c:title>" % nsdecls("c")
+        )
+
+
+class CT_Tx(BaseOxmlElement):
+    """
+    ``<c:tx>`` element containing the text for a label on a data point or
+    other chart item.
+    """
+
+    strRef = ZeroOrOne("c:strRef")
+    rich = ZeroOrOne("c:rich")
+
+    def _new_rich(self):
+        return parse_xml(
+            "<c:rich %s>"
+            "  <a:bodyPr/>"
+            "  <a:lstStyle/>"
+            "  <a:p>"
+            "    <a:pPr>"
+            "      <a:defRPr/>"
+            "    </a:pPr>"
+            "  </a:p>"
+            "</c:rich>" % nsdecls("c", "a")
+        )
+
+
+class CT_UnsignedInt(BaseOxmlElement):
+    """
+    ``<c:idx>`` element and others.
+    """
+
+    val = RequiredAttribute("val", XsdUnsignedInt)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/coreprops.py b/.venv/lib/python3.12/site-packages/pptx/oxml/coreprops.py
new file mode 100644
index 00000000..de6b26b2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/coreprops.py
@@ -0,0 +1,288 @@
+"""lxml custom element classes for core properties-related XML elements."""
+
+from __future__ import annotations
+
+import datetime as dt
+import re
+from typing import Callable, cast
+
+from lxml.etree import _Element  # pyright: ignore[reportPrivateUsage]
+
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls, qn
+from pptx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne
+
+
+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[[], _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: _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")
+
+    @staticmethod
+    def new_coreProperties() -> CT_CoreProperties:
+        """Return a new `cp:coreProperties` element"""
+        return cast(CT_CoreProperties, parse_xml(CT_CoreProperties._coreProperties_tmpl))
+
+    @property
+    def author_text(self) -> str:
+        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) -> str:
+        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) -> str:
+        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) -> str:
+        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) -> str:
+        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) -> str:
+        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):
+        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) -> int:
+        """Integer value of revision property."""
+        revision = self.revision
+        if revision is None:
+            return 0
+        revision_str = revision.text
+        if revision_str is None:
+            return 0
+        try:
+            revision = int(revision_str)
+        except ValueError:
+            # -- non-integer revision strings also resolve to 0 --
+            return 0
+        # -- as do negative integers --
+        if revision < 0:
+            return 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) -> str:
+        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) -> str:
+        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) -> str:
+        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 = cast("_Element | None", getattr(self, property_name))
+        if element is None:
+            return None
+        datetime_str = element.text
+        if datetime_str is None:
+            return None
+        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):
+        """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, datetime: dt.datetime, offset_str: str):
+        """Return |datetime| instance offset from `datetime` by offset specified in `offset_str`.
+
+        `offset_str` is a string like `'-07:00'`.
+        """
+        match = cls._offset_pattern.match(offset_str)
+        if match is None:
+            raise ValueError(f"{repr(offset_str)} is not a valid offset string")
+        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 datetime + 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:]
+        timestamp = None
+        for tmpl in templates:
+            try:
+                timestamp = dt.datetime.strptime(parseable_part, tmpl)
+            except ValueError:
+                continue
+        if timestamp is None:
+            tmpl = "could not parse W3CDTF datetime string '%s'"
+            raise ValueError(tmpl % w3cdtf_str)
+        if len(offset_str) == 6:
+            return cls._offset_dt(timestamp, offset_str)
+        return timestamp
+
+    def _set_element_datetime(self, prop_name: str, value: dt.datetime) -> None:
+        """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: str) -> None:
+        """Set string value of `name` property to `value`."""
+        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:
+        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/pptx/oxml/dml/__init__.py b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/dml/color.py b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/color.py
new file mode 100644
index 00000000..dfce90aa
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/color.py
@@ -0,0 +1,111 @@
+"""lxml custom element classes for DrawingML-related XML elements."""
+
+from __future__ import annotations
+
+from pptx.enum.dml import MSO_THEME_COLOR
+from pptx.oxml.simpletypes import ST_HexColorRGB, ST_Percentage
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    Choice,
+    RequiredAttribute,
+    ZeroOrOne,
+    ZeroOrOneChoice,
+)
+
+
+class _BaseColorElement(BaseOxmlElement):
+    """
+    Base class for <a:srgbClr> and <a:schemeClr> elements.
+    """
+
+    lumMod = ZeroOrOne("a:lumMod")
+    lumOff = ZeroOrOne("a:lumOff")
+
+    def add_lumMod(self, value):
+        """
+        Return a newly added <a:lumMod> child element.
+        """
+        lumMod = self._add_lumMod()
+        lumMod.val = value
+        return lumMod
+
+    def add_lumOff(self, value):
+        """
+        Return a newly added <a:lumOff> child element.
+        """
+        lumOff = self._add_lumOff()
+        lumOff.val = value
+        return lumOff
+
+    def clear_lum(self):
+        """
+        Return self after removing any <a:lumMod> and <a:lumOff> child
+        elements.
+        """
+        self._remove_lumMod()
+        self._remove_lumOff()
+        return self
+
+
+class CT_Color(BaseOxmlElement):
+    """Custom element class for `a:fgClr`, `a:bgClr` and perhaps others."""
+
+    eg_colorChoice = ZeroOrOneChoice(
+        (
+            Choice("a:scrgbClr"),
+            Choice("a:srgbClr"),
+            Choice("a:hslClr"),
+            Choice("a:sysClr"),
+            Choice("a:schemeClr"),
+            Choice("a:prstClr"),
+        ),
+        successors=(),
+    )
+
+
+class CT_HslColor(_BaseColorElement):
+    """
+    Custom element class for <a:hslClr> element.
+    """
+
+
+class CT_Percentage(BaseOxmlElement):
+    """
+    Custom element class for <a:lumMod> and <a:lumOff> elements.
+    """
+
+    val = RequiredAttribute("val", ST_Percentage)
+
+
+class CT_PresetColor(_BaseColorElement):
+    """
+    Custom element class for <a:prstClr> element.
+    """
+
+
+class CT_SchemeColor(_BaseColorElement):
+    """
+    Custom element class for <a:schemeClr> element.
+    """
+
+    val = RequiredAttribute("val", MSO_THEME_COLOR)
+
+
+class CT_ScRgbColor(_BaseColorElement):
+    """
+    Custom element class for <a:scrgbClr> element.
+    """
+
+
+class CT_SRgbColor(_BaseColorElement):
+    """
+    Custom element class for <a:srgbClr> element.
+    """
+
+    val = RequiredAttribute("val", ST_HexColorRGB)
+
+
+class CT_SystemColor(_BaseColorElement):
+    """
+    Custom element class for <a:sysClr> element.
+    """
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/dml/fill.py b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/fill.py
new file mode 100644
index 00000000..2ff2255d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/fill.py
@@ -0,0 +1,197 @@
+"""lxml custom element classes for DrawingML-related XML elements."""
+
+from __future__ import annotations
+
+from pptx.enum.dml import MSO_PATTERN_TYPE
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.simpletypes import (
+    ST_Percentage,
+    ST_PositiveFixedAngle,
+    ST_PositiveFixedPercentage,
+    ST_RelationshipId,
+)
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    Choice,
+    OneOrMore,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrOne,
+    ZeroOrOneChoice,
+)
+
+
+class CT_Blip(BaseOxmlElement):
+    """
+    <a:blip> element
+    """
+
+    rEmbed = OptionalAttribute("r:embed", ST_RelationshipId)
+
+
+class CT_BlipFillProperties(BaseOxmlElement):
+    """
+    Custom element class for <a:blipFill> element.
+    """
+
+    _tag_seq = ("a:blip", "a:srcRect", "a:tile", "a:stretch")
+    blip = ZeroOrOne("a:blip", successors=_tag_seq[1:])
+    srcRect = ZeroOrOne("a:srcRect", successors=_tag_seq[2:])
+    del _tag_seq
+
+    def crop(self, cropping):
+        """
+        Set `a:srcRect` child to crop according to *cropping* values.
+        """
+        srcRect = self._add_srcRect()
+        srcRect.l, srcRect.t, srcRect.r, srcRect.b = cropping
+
+
+class CT_GradientFillProperties(BaseOxmlElement):
+    """`a:gradFill` custom element class."""
+
+    _tag_seq = ("a:gsLst", "a:lin", "a:path", "a:tileRect")
+    gsLst = ZeroOrOne("a:gsLst", successors=_tag_seq[1:])
+    lin = ZeroOrOne("a:lin", successors=_tag_seq[2:])
+    path = ZeroOrOne("a:path", successors=_tag_seq[3:])
+    del _tag_seq
+
+    @classmethod
+    def new_gradFill(cls):
+        """Return newly-created "loose" default gradient subtree."""
+        return parse_xml(
+            '<a:gradFill %s rotWithShape="1">\n'
+            "  <a:gsLst>\n"
+            '    <a:gs pos="0">\n'
+            '      <a:schemeClr val="accent1">\n'
+            '        <a:tint val="100000"/>\n'
+            '        <a:shade val="100000"/>\n'
+            '        <a:satMod val="130000"/>\n'
+            "      </a:schemeClr>\n"
+            "    </a:gs>\n"
+            '    <a:gs pos="100000">\n'
+            '      <a:schemeClr val="accent1">\n'
+            '        <a:tint val="50000"/>\n'
+            '        <a:shade val="100000"/>\n'
+            '        <a:satMod val="350000"/>\n'
+            "      </a:schemeClr>\n"
+            "    </a:gs>\n"
+            "  </a:gsLst>\n"
+            '  <a:lin scaled="0"/>\n'
+            "</a:gradFill>\n" % nsdecls("a")
+        )
+
+    def _new_gsLst(self):
+        """Override default to add minimum subtree."""
+        return CT_GradientStopList.new_gsLst()
+
+
+class CT_GradientStop(BaseOxmlElement):
+    """`a:gs` custom element class."""
+
+    eg_colorChoice = ZeroOrOneChoice(
+        (
+            Choice("a:scrgbClr"),
+            Choice("a:srgbClr"),
+            Choice("a:hslClr"),
+            Choice("a:sysClr"),
+            Choice("a:schemeClr"),
+            Choice("a:prstClr"),
+        ),
+        successors=(),
+    )
+    pos = RequiredAttribute("pos", ST_PositiveFixedPercentage)
+
+
+class CT_GradientStopList(BaseOxmlElement):
+    """`a:gsLst` custom element class."""
+
+    gs = OneOrMore("a:gs")
+
+    @classmethod
+    def new_gsLst(cls):
+        """Return newly-created "loose" default stop-list subtree.
+
+        An `a:gsLst` element must have at least two `a:gs` children. These
+        are the default from the PowerPoint built-in "White" template.
+        """
+        return parse_xml(
+            "<a:gsLst %s>\n"
+            '  <a:gs pos="0">\n'
+            '    <a:schemeClr val="accent1">\n'
+            '      <a:tint val="100000"/>\n'
+            '      <a:shade val="100000"/>\n'
+            '      <a:satMod val="130000"/>\n'
+            "    </a:schemeClr>\n"
+            "  </a:gs>\n"
+            '  <a:gs pos="100000">\n'
+            '    <a:schemeClr val="accent1">\n'
+            '      <a:tint val="50000"/>\n'
+            '      <a:shade val="100000"/>\n'
+            '      <a:satMod val="350000"/>\n'
+            "    </a:schemeClr>\n"
+            "  </a:gs>\n"
+            "</a:gsLst>\n" % nsdecls("a")
+        )
+
+
+class CT_GroupFillProperties(BaseOxmlElement):
+    """`a:grpFill` custom element class"""
+
+
+class CT_LinearShadeProperties(BaseOxmlElement):
+    """`a:lin` custom element class"""
+
+    ang = OptionalAttribute("ang", ST_PositiveFixedAngle)
+
+
+class CT_NoFillProperties(BaseOxmlElement):
+    """`a:noFill` custom element class"""
+
+
+class CT_PatternFillProperties(BaseOxmlElement):
+    """`a:pattFill` custom element class"""
+
+    _tag_seq = ("a:fgClr", "a:bgClr")
+    fgClr = ZeroOrOne("a:fgClr", successors=_tag_seq[1:])
+    bgClr = ZeroOrOne("a:bgClr", successors=_tag_seq[2:])
+    del _tag_seq
+    prst = OptionalAttribute("prst", MSO_PATTERN_TYPE)
+
+    def _new_bgClr(self):
+        """Override default to add minimum subtree."""
+        xml = ("<a:bgClr %s>\n" ' <a:srgbClr val="FFFFFF"/>\n' "</a:bgClr>\n") % nsdecls("a")
+        bgClr = parse_xml(xml)
+        return bgClr
+
+    def _new_fgClr(self):
+        """Override default to add minimum subtree."""
+        xml = ("<a:fgClr %s>\n" ' <a:srgbClr val="000000"/>\n' "</a:fgClr>\n") % nsdecls("a")
+        fgClr = parse_xml(xml)
+        return fgClr
+
+
+class CT_RelativeRect(BaseOxmlElement):
+    """`a:srcRect` element and perhaps others."""
+
+    l = OptionalAttribute("l", ST_Percentage, default=0.0)  # noqa
+    t = OptionalAttribute("t", ST_Percentage, default=0.0)
+    r = OptionalAttribute("r", ST_Percentage, default=0.0)
+    b = OptionalAttribute("b", ST_Percentage, default=0.0)
+
+
+class CT_SolidColorFillProperties(BaseOxmlElement):
+    """`a:solidFill` custom element class."""
+
+    eg_colorChoice = ZeroOrOneChoice(
+        (
+            Choice("a:scrgbClr"),
+            Choice("a:srgbClr"),
+            Choice("a:hslClr"),
+            Choice("a:sysClr"),
+            Choice("a:schemeClr"),
+            Choice("a:prstClr"),
+        ),
+        successors=(),
+    )
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/dml/line.py b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/line.py
new file mode 100644
index 00000000..720ca8e0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/line.py
@@ -0,0 +1,12 @@
+"""lxml custom element classes for DrawingML line-related XML elements."""
+
+from __future__ import annotations
+
+from pptx.enum.dml import MSO_LINE_DASH_STYLE
+from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute
+
+
+class CT_PresetLineDashProperties(BaseOxmlElement):
+    """`a:prstDash` custom element class"""
+
+    val = OptionalAttribute("val", MSO_LINE_DASH_STYLE)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/ns.py b/.venv/lib/python3.12/site-packages/pptx/oxml/ns.py
new file mode 100644
index 00000000..d900c33b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/ns.py
@@ -0,0 +1,129 @@
+"""Namespace related objects."""
+
+from __future__ import annotations
+
+
+# -- Maps namespace prefix to namespace name for all known PowerPoint XML namespaces --
+_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",
+    "ct": "http://schemas.openxmlformats.org/package/2006/content-types",
+    "dc": "http://purl.org/dc/elements/1.1/",
+    "dcmitype": "http://purl.org/dc/dcmitype/",
+    "dcterms": "http://purl.org/dc/terms/",
+    "ep": "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties",
+    "i": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
+    "m": "http://schemas.openxmlformats.org/officeDocument/2006/math",
+    "mo": "http://schemas.microsoft.com/office/mac/office/2008/main",
+    "mv": "urn:schemas-microsoft-com:mac:vml",
+    "o": "urn:schemas-microsoft-com:office:office",
+    "p": "http://schemas.openxmlformats.org/presentationml/2006/main",
+    "pd": "http://schemas.openxmlformats.org/drawingml/2006/presentationDrawing",
+    "pic": "http://schemas.openxmlformats.org/drawingml/2006/picture",
+    "pr": "http://schemas.openxmlformats.org/package/2006/relationships",
+    "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
+    "sl": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout",
+    "v": "urn:schemas-microsoft-com:vml",
+    "ve": "http://schemas.openxmlformats.org/markup-compatibility/2006",
+    "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
+    "w10": "urn:schemas-microsoft-com:office:word",
+    "wne": "http://schemas.microsoft.com/office/word/2006/wordml",
+    "wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
+    "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):
+        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]
+
+    @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 clark_name(self):
+        return "{%s}%s" % (self._ns_uri, self._local_part)
+
+    @property
+    def local_part(self):
+        """
+        Return the local part of the tag as a string. E.g. 'foobar' is
+        returned for tag 'f:foobar'.
+        """
+        return self._local_part
+
+    @property
+    def nsmap(self):
+        """
+        Return a dict having a single member, mapping the namespace prefix of
+        this tag to it's namespace name (e.g. {'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):
+        """
+        Return the string namespace prefix for the tag, e.g. 'f' is returned
+        for tag 'f:foobar'.
+        """
+        return self._pfx
+
+    @property
+    def nsuri(self):
+        """
+        Return the namespace URI for the tag, e.g. '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 namespaces(*prefixes: str):
+    """Return a dict containing the subset namespace prefix mappings specified by *prefixes*.
+
+    Any number of namespace prefixes can be supplied, e.g. namespaces('a', 'r', 'p').
+    """
+    return {pfx: _nsmap[pfx] for pfx in prefixes}
+
+
+nsmap = namespaces  # alias for more compact use with Element()
+
+
+def nsdecls(*prefixes: str):
+    return " ".join(['xmlns:%s="%s"' % (pfx, _nsmap[pfx]) for pfx in prefixes])
+
+
+def nsuri(nspfx: str):
+    """Return the namespace URI corresponding to `nspfx`.
+
+    Example:
+
+        >>> nsuri("p")
+        "http://schemas.openxmlformats.org/presentationml/2006/main"
+    """
+    return _nsmap[nspfx]
+
+
+def qn(namespace_prefixed_tag: str) -> str:
+    """Return a Clark-notation qualified tag name corresponding to `namespace_prefixed_tag`.
+
+    `namespace_prefixed_tag` is a string like 'p:body'. 'qn' stands for `qualified name`.
+
+    As an example, `qn("p:cSld")` returns:
+        `"{http://schemas.openxmlformats.org/drawingml/2006/main}cSld"`.
+    """
+    nsptag = NamespacePrefixedTag(namespace_prefixed_tag)
+    return nsptag.clark_name
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/presentation.py b/.venv/lib/python3.12/site-packages/pptx/oxml/presentation.py
new file mode 100644
index 00000000..17997c2b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/presentation.py
@@ -0,0 +1,130 @@
+"""Custom element classes for presentation-related XML elements."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, cast
+
+from pptx.oxml.simpletypes import ST_SlideId, ST_SlideSizeCoordinate, XsdString
+from pptx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrMore, ZeroOrOne
+
+if TYPE_CHECKING:
+    from pptx.util import Length
+
+
+class CT_Presentation(BaseOxmlElement):
+    """`p:presentation` element, root of the Presentation part stored as `/ppt/presentation.xml`."""
+
+    get_or_add_sldSz: Callable[[], CT_SlideSize]
+    get_or_add_sldIdLst: Callable[[], CT_SlideIdList]
+    get_or_add_sldMasterIdLst: Callable[[], CT_SlideMasterIdList]
+
+    sldMasterIdLst: CT_SlideMasterIdList | None = (
+        ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+            "p:sldMasterIdLst",
+            successors=(
+                "p:notesMasterIdLst",
+                "p:handoutMasterIdLst",
+                "p:sldIdLst",
+                "p:sldSz",
+                "p:notesSz",
+            ),
+        )
+    )
+    sldIdLst: CT_SlideIdList | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "p:sldIdLst", successors=("p:sldSz", "p:notesSz")
+    )
+    sldSz: CT_SlideSize | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "p:sldSz", successors=("p:notesSz",)
+    )
+
+
+class CT_SlideId(BaseOxmlElement):
+    """`p:sldId` element.
+
+    Direct child of `p:sldIdLst` that contains an `rId` reference to a slide in the presentation.
+    """
+
+    id: int = RequiredAttribute("id", ST_SlideId)  # pyright: ignore[reportAssignmentType]
+    rId: str = RequiredAttribute("r:id", XsdString)  # pyright: ignore[reportAssignmentType]
+
+
+class CT_SlideIdList(BaseOxmlElement):
+    """`p:sldIdLst` element.
+
+    Direct child of <p:presentation> that contains a list of the slide parts in the presentation.
+    """
+
+    sldId_lst: list[CT_SlideId]
+
+    _add_sldId: Callable[..., CT_SlideId]
+    sldId = ZeroOrMore("p:sldId")
+
+    def add_sldId(self, rId: str) -> CT_SlideId:
+        """Create and return a reference to a new `p:sldId` child element.
+
+        The new `p:sldId` element has its r:id attribute set to `rId`.
+        """
+        return self._add_sldId(id=self._next_id, rId=rId)
+
+    @property
+    def _next_id(self) -> int:
+        """The next available slide ID as an `int`.
+
+        Valid slide IDs start at 256. The next integer value greater than the max value in use is
+        chosen, which minimizes that chance of reusing the id of a deleted slide.
+        """
+        MIN_SLIDE_ID = 256
+        MAX_SLIDE_ID = 2147483647
+
+        used_ids = [int(s) for s in cast("list[str]", self.xpath("./p:sldId/@id"))]
+        simple_next = max([MIN_SLIDE_ID - 1] + used_ids) + 1
+        if simple_next <= MAX_SLIDE_ID:
+            return simple_next
+
+        # -- fall back to search for next unused from bottom --
+        valid_used_ids = sorted(id for id in used_ids if (MIN_SLIDE_ID <= id <= MAX_SLIDE_ID))
+        return (
+            next(
+                candidate_id
+                for candidate_id, used_id in enumerate(valid_used_ids, start=MIN_SLIDE_ID)
+                if candidate_id != used_id
+            )
+            if valid_used_ids
+            else 256
+        )
+
+
+class CT_SlideMasterIdList(BaseOxmlElement):
+    """`p:sldMasterIdLst` element.
+
+    Child of `p:presentation` containing references to the slide masters that belong to the
+    presentation.
+    """
+
+    sldMasterId_lst: list[CT_SlideMasterIdListEntry]
+
+    sldMasterId = ZeroOrMore("p:sldMasterId")
+
+
+class CT_SlideMasterIdListEntry(BaseOxmlElement):
+    """
+    ``<p:sldMasterId>`` element, child of ``<p:sldMasterIdLst>`` containing
+    a reference to a slide master.
+    """
+
+    rId: str = RequiredAttribute("r:id", XsdString)  # pyright: ignore[reportAssignmentType]
+
+
+class CT_SlideSize(BaseOxmlElement):
+    """`p:sldSz` element.
+
+    Direct child of <p:presentation> that contains the width and height of slides in the
+    presentation.
+    """
+
+    cx: Length = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "cx", ST_SlideSizeCoordinate
+    )
+    cy: Length = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "cy", ST_SlideSizeCoordinate
+    )
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/__init__.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/__init__.py
new file mode 100644
index 00000000..37f8ef60
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/__init__.py
@@ -0,0 +1,19 @@
+"""Base shape-related objects such as BaseShape."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing_extensions import TypeAlias
+
+    from pptx.oxml.shapes.autoshape import CT_Shape
+    from pptx.oxml.shapes.connector import CT_Connector
+    from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
+    from pptx.oxml.shapes.groupshape import CT_GroupShape
+    from pptx.oxml.shapes.picture import CT_Picture
+
+
+ShapeElement: TypeAlias = (
+    "CT_Connector | CT_GraphicalObjectFrame |  CT_GroupShape | CT_Picture | CT_Shape"
+)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/autoshape.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/autoshape.py
new file mode 100644
index 00000000..5d78f624
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/autoshape.py
@@ -0,0 +1,455 @@
+# pyright: reportPrivateUsage=false
+
+"""lxml custom element classes for shape-related XML elements."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, cast
+
+from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, PP_PLACEHOLDER
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.shapes.shared import BaseShapeElement
+from pptx.oxml.simpletypes import (
+    ST_Coordinate,
+    ST_PositiveCoordinate,
+    XsdBoolean,
+    XsdString,
+)
+from pptx.oxml.text import CT_TextBody
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OneAndOnlyOne,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrMore,
+    ZeroOrOne,
+)
+
+if TYPE_CHECKING:
+    from pptx.oxml.shapes.shared import (
+        CT_ApplicationNonVisualDrawingProps,
+        CT_NonVisualDrawingProps,
+        CT_ShapeProperties,
+    )
+    from pptx.util import Length
+
+
+class CT_AdjPoint2D(BaseOxmlElement):
+    """`a:pt` custom element class."""
+
+    x: Length = RequiredAttribute("x", ST_Coordinate)  # pyright: ignore[reportAssignmentType]
+    y: Length = RequiredAttribute("y", ST_Coordinate)  # pyright: ignore[reportAssignmentType]
+
+
+class CT_CustomGeometry2D(BaseOxmlElement):
+    """`a:custGeom` custom element class."""
+
+    get_or_add_pathLst: Callable[[], CT_Path2DList]
+
+    _tag_seq = ("a:avLst", "a:gdLst", "a:ahLst", "a:cxnLst", "a:rect", "a:pathLst")
+    pathLst: CT_Path2DList | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:pathLst", successors=_tag_seq[6:]
+    )
+
+
+class CT_GeomGuide(BaseOxmlElement):
+    """`a:gd` custom element class.
+
+    Defines a "guide", corresponding to a yellow diamond-shaped handle on an autoshape.
+    """
+
+    name: str = RequiredAttribute("name", XsdString)  # pyright: ignore[reportAssignmentType]
+    fmla: str = RequiredAttribute("fmla", XsdString)  # pyright: ignore[reportAssignmentType]
+
+
+class CT_GeomGuideList(BaseOxmlElement):
+    """`a:avLst` custom element class."""
+
+    _add_gd: Callable[[], CT_GeomGuide]
+
+    gd_lst: list[CT_GeomGuide]
+
+    gd = ZeroOrMore("a:gd")
+
+
+class CT_NonVisualDrawingShapeProps(BaseShapeElement):
+    """`p:cNvSpPr` custom element class."""
+
+    spLocks = ZeroOrOne("a:spLocks")
+    txBox: bool | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "txBox", XsdBoolean
+    )
+
+
+class CT_Path2D(BaseOxmlElement):
+    """`a:path` custom element class."""
+
+    _add_close: Callable[[], CT_Path2DClose]
+    _add_lnTo: Callable[[], CT_Path2DLineTo]
+    _add_moveTo: Callable[[], CT_Path2DMoveTo]
+
+    close = ZeroOrMore("a:close", successors=())
+    lnTo = ZeroOrMore("a:lnTo", successors=())
+    moveTo = ZeroOrMore("a:moveTo", successors=())
+    w: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "w", ST_PositiveCoordinate
+    )
+    h: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "h", ST_PositiveCoordinate
+    )
+
+    def add_close(self) -> CT_Path2DClose:
+        """Return a newly created `a:close` element.
+
+        The new `a:close` element is appended to this `a:path` element.
+        """
+        return self._add_close()
+
+    def add_lnTo(self, x: Length, y: Length) -> CT_Path2DLineTo:
+        """Return a newly created `a:lnTo` subtree with end point *(x, y)*.
+
+        The new `a:lnTo` element is appended to this `a:path` element.
+        """
+        lnTo = self._add_lnTo()
+        pt = lnTo._add_pt()
+        pt.x, pt.y = x, y
+        return lnTo
+
+    def add_moveTo(self, x: Length, y: Length):
+        """Return a newly created `a:moveTo` subtree with point `(x, y)`.
+
+        The new `a:moveTo` element is appended to this `a:path` element.
+        """
+        moveTo = self._add_moveTo()
+        pt = moveTo._add_pt()
+        pt.x, pt.y = x, y
+        return moveTo
+
+
+class CT_Path2DClose(BaseOxmlElement):
+    """`a:close` custom element class."""
+
+
+class CT_Path2DLineTo(BaseOxmlElement):
+    """`a:lnTo` custom element class."""
+
+    _add_pt: Callable[[], CT_AdjPoint2D]
+
+    pt = ZeroOrOne("a:pt", successors=())
+
+
+class CT_Path2DList(BaseOxmlElement):
+    """`a:pathLst` custom element class."""
+
+    _add_path: Callable[[], CT_Path2D]
+
+    path = ZeroOrMore("a:path", successors=())
+
+    def add_path(self, w: Length, h: Length):
+        """Return a newly created `a:path` child element."""
+        path = self._add_path()
+        path.w, path.h = w, h
+        return path
+
+
+class CT_Path2DMoveTo(BaseOxmlElement):
+    """`a:moveTo` custom element class."""
+
+    _add_pt: Callable[[], CT_AdjPoint2D]
+
+    pt = ZeroOrOne("a:pt", successors=())
+
+
+class CT_PresetGeometry2D(BaseOxmlElement):
+    """`a:prstGeom` custom element class."""
+
+    _add_avLst: Callable[[], CT_GeomGuideList]
+    _remove_avLst: Callable[[], None]
+
+    avLst: CT_GeomGuideList | None = ZeroOrOne("a:avLst")  # pyright: ignore[reportAssignmentType]
+    prst: MSO_AUTO_SHAPE_TYPE = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "prst", MSO_AUTO_SHAPE_TYPE
+    )
+
+    @property
+    def gd_lst(self) -> list[CT_GeomGuide]:
+        """Sequence of `a:gd` element children of `a:avLst`. Empty if none are present."""
+        avLst = self.avLst
+        if avLst is None:
+            return []
+        return avLst.gd_lst
+
+    def rewrite_guides(self, guides: list[tuple[str, int]]):
+        """Replace any `a:gd` element children of `a:avLst` with ones forme from `guides`."""
+        self._remove_avLst()
+        avLst = self._add_avLst()
+        for name, val in guides:
+            gd = avLst._add_gd()
+            gd.name = name
+            gd.fmla = "val %d" % val
+
+
+class CT_Shape(BaseShapeElement):
+    """`p:sp` custom element class."""
+
+    get_or_add_txBody: Callable[[], CT_TextBody]
+
+    nvSpPr: CT_ShapeNonVisual = OneAndOnlyOne("p:nvSpPr")  # pyright: ignore[reportAssignmentType]
+    spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr")  # pyright: ignore[reportAssignmentType]
+    txBody: CT_TextBody | None = ZeroOrOne("p:txBody", successors=("p:extLst",))  # pyright: ignore
+
+    def add_path(self, w: Length, h: Length) -> CT_Path2D:
+        custGeom = self.spPr.custGeom
+        if custGeom is None:
+            raise ValueError("shape must be freeform")
+        pathLst = custGeom.get_or_add_pathLst()
+        return pathLst.add_path(w=w, h=h)
+
+    def get_or_add_ln(self):
+        """Return the `a:ln` grandchild element, newly added if not present."""
+        return self.spPr.get_or_add_ln()
+
+    @property
+    def has_custom_geometry(self):
+        """True if this shape has custom geometry, i.e. is a freeform shape.
+
+        A shape has custom geometry if it has a `p:spPr/a:custGeom`
+        descendant (instead of `p:spPr/a:prstGeom`).
+        """
+        return self.spPr.custGeom is not None
+
+    @property
+    def is_autoshape(self):
+        """True if this shape is an auto shape.
+
+        A shape is an auto shape if it has a `a:prstGeom` element and does not have a txBox="1"
+        attribute on cNvSpPr.
+        """
+        prstGeom = self.prstGeom
+        if prstGeom is None:
+            return False
+        return self.nvSpPr.cNvSpPr.txBox is not True
+
+    @property
+    def is_textbox(self):
+        """True if this shape is a text box.
+
+        A shape is a text box if it has a `txBox` attribute on cNvSpPr that resolves to |True|.
+        The default when the txBox attribute is missing is |False|.
+        """
+        return self.nvSpPr.cNvSpPr.txBox is True
+
+    @property
+    def ln(self):
+        """`a:ln` grand-child element or |None| if not present."""
+        return self.spPr.ln
+
+    @staticmethod
+    def new_autoshape_sp(
+        id_: int, name: str, prst: str, left: int, top: int, width: int, height: int
+    ) -> CT_Shape:
+        """Return a new `p:sp` element tree configured as a base auto shape."""
+        xml = (
+            "<p:sp %s>\n"
+            "  <p:nvSpPr>\n"
+            '    <p:cNvPr id="%s" name="%s"/>\n'
+            "    <p:cNvSpPr/>\n"
+            "    <p:nvPr/>\n"
+            "  </p:nvSpPr>\n"
+            "  <p:spPr>\n"
+            "    <a:xfrm>\n"
+            '      <a:off x="%s" y="%s"/>\n'
+            '      <a:ext cx="%s" cy="%s"/>\n'
+            "    </a:xfrm>\n"
+            '    <a:prstGeom prst="%s">\n'
+            "      <a:avLst/>\n"
+            "    </a:prstGeom>\n"
+            "  </p:spPr>\n"
+            "  <p:style>\n"
+            '    <a:lnRef idx="1">\n'
+            '      <a:schemeClr val="accent1"/>\n'
+            "    </a:lnRef>\n"
+            '    <a:fillRef idx="3">\n'
+            '      <a:schemeClr val="accent1"/>\n'
+            "    </a:fillRef>\n"
+            '    <a:effectRef idx="2">\n'
+            '      <a:schemeClr val="accent1"/>\n'
+            "    </a:effectRef>\n"
+            '    <a:fontRef idx="minor">\n'
+            '      <a:schemeClr val="lt1"/>\n'
+            "    </a:fontRef>\n"
+            "  </p:style>\n"
+            "  <p:txBody>\n"
+            '    <a:bodyPr rtlCol="0" anchor="ctr"/>\n'
+            "    <a:lstStyle/>\n"
+            "    <a:p>\n"
+            '      <a:pPr algn="ctr"/>\n'
+            "    </a:p>\n"
+            "  </p:txBody>\n"
+            "</p:sp>" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d", "%s")
+        ) % (id_, name, left, top, width, height, prst)
+        return cast(CT_Shape, parse_xml(xml))
+
+    @staticmethod
+    def new_freeform_sp(shape_id: int, name: str, x: int, y: int, cx: int, cy: int):
+        """Return new `p:sp` element tree configured as freeform shape.
+
+        The returned shape has a `a:custGeom` subtree but no paths in its
+        path list.
+        """
+        xml = (
+            "<p:sp %s>\n"
+            "  <p:nvSpPr>\n"
+            '    <p:cNvPr id="%s" name="%s"/>\n'
+            "    <p:cNvSpPr/>\n"
+            "    <p:nvPr/>\n"
+            "  </p:nvSpPr>\n"
+            "  <p:spPr>\n"
+            "    <a:xfrm>\n"
+            '      <a:off x="%s" y="%s"/>\n'
+            '      <a:ext cx="%s" cy="%s"/>\n'
+            "    </a:xfrm>\n"
+            "    <a:custGeom>\n"
+            "      <a:avLst/>\n"
+            "      <a:gdLst/>\n"
+            "      <a:ahLst/>\n"
+            "      <a:cxnLst/>\n"
+            '      <a:rect l="l" t="t" r="r" b="b"/>\n'
+            "      <a:pathLst/>\n"
+            "    </a:custGeom>\n"
+            "  </p:spPr>\n"
+            "  <p:style>\n"
+            '    <a:lnRef idx="1">\n'
+            '      <a:schemeClr val="accent1"/>\n'
+            "    </a:lnRef>\n"
+            '    <a:fillRef idx="3">\n'
+            '      <a:schemeClr val="accent1"/>\n'
+            "    </a:fillRef>\n"
+            '    <a:effectRef idx="2">\n'
+            '      <a:schemeClr val="accent1"/>\n'
+            "    </a:effectRef>\n"
+            '    <a:fontRef idx="minor">\n'
+            '      <a:schemeClr val="lt1"/>\n'
+            "    </a:fontRef>\n"
+            "  </p:style>\n"
+            "  <p:txBody>\n"
+            '    <a:bodyPr rtlCol="0" anchor="ctr"/>\n'
+            "    <a:lstStyle/>\n"
+            "    <a:p>\n"
+            '      <a:pPr algn="ctr"/>\n'
+            "    </a:p>\n"
+            "  </p:txBody>\n"
+            "</p:sp>" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d")
+        ) % (shape_id, name, x, y, cx, cy)
+        return cast(CT_Shape, parse_xml(xml))
+
+    @staticmethod
+    def new_placeholder_sp(
+        id_: int, name: str, ph_type: PP_PLACEHOLDER, orient: str, sz, idx
+    ) -> CT_Shape:
+        """Return a new `p:sp` element tree configured as a placeholder shape."""
+        sp = cast(
+            CT_Shape,
+            parse_xml(
+                f"<p:sp {nsdecls('a', 'p')}>\n"
+                f"  <p:nvSpPr>\n"
+                f'    <p:cNvPr id="{id_}" name="{name}"/>\n'
+                f"    <p:cNvSpPr>\n"
+                f'      <a:spLocks noGrp="1"/>\n'
+                f"    </p:cNvSpPr>\n"
+                f"    <p:nvPr/>\n"
+                f"  </p:nvSpPr>\n"
+                f"  <p:spPr/>\n"
+                f"</p:sp>"
+            ),
+        )
+
+        ph = sp.nvSpPr.nvPr.get_or_add_ph()
+        ph.type = ph_type
+        ph.idx = idx
+        ph.orient = orient
+        ph.sz = sz
+
+        placeholder_types_that_have_a_text_frame = (
+            PP_PLACEHOLDER.TITLE,
+            PP_PLACEHOLDER.CENTER_TITLE,
+            PP_PLACEHOLDER.SUBTITLE,
+            PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.OBJECT,
+        )
+
+        if ph_type in placeholder_types_that_have_a_text_frame:
+            sp.append(CT_TextBody.new())
+
+        return sp
+
+    @staticmethod
+    def new_textbox_sp(id_, name, left, top, width, height):
+        """Return a new `p:sp` element tree configured as a base textbox shape."""
+        tmpl = CT_Shape._textbox_sp_tmpl()
+        xml = tmpl % (id_, name, left, top, width, height)
+        sp = parse_xml(xml)
+        return sp
+
+    @property
+    def prst(self):
+        """Value of `prst` attribute of `a:prstGeom` element or |None| if not present."""
+        prstGeom = self.prstGeom
+        if prstGeom is None:
+            return None
+        return prstGeom.prst
+
+    @property
+    def prstGeom(self) -> CT_PresetGeometry2D:
+        """Reference to `a:prstGeom` child element.
+
+        |None| if this shape doesn't have one, for example, if it's a placeholder shape.
+        """
+        return self.spPr.prstGeom
+
+    def _new_txBody(self):
+        return CT_TextBody.new_p_txBody()
+
+    @staticmethod
+    def _textbox_sp_tmpl():
+        return (
+            "<p:sp %s>\n"
+            "  <p:nvSpPr>\n"
+            '    <p:cNvPr id="%s" name="%s"/>\n'
+            '    <p:cNvSpPr txBox="1"/>\n'
+            "    <p:nvPr/>\n"
+            "  </p:nvSpPr>\n"
+            "  <p:spPr>\n"
+            "    <a:xfrm>\n"
+            '      <a:off x="%s" y="%s"/>\n'
+            '      <a:ext cx="%s" cy="%s"/>\n'
+            "    </a:xfrm>\n"
+            '    <a:prstGeom prst="rect">\n'
+            "      <a:avLst/>\n"
+            "    </a:prstGeom>\n"
+            "    <a:noFill/>\n"
+            "  </p:spPr>\n"
+            "  <p:txBody>\n"
+            '    <a:bodyPr wrap="none">\n'
+            "      <a:spAutoFit/>\n"
+            "    </a:bodyPr>\n"
+            "    <a:lstStyle/>\n"
+            "    <a:p/>\n"
+            "  </p:txBody>\n"
+            "</p:sp>" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d")
+        )
+
+
+class CT_ShapeNonVisual(BaseShapeElement):
+    """`p:nvSpPr` custom element class."""
+
+    cNvPr: CT_NonVisualDrawingProps = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "p:cNvPr"
+    )
+    cNvSpPr: CT_NonVisualDrawingShapeProps = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "p:cNvSpPr"
+    )
+    nvPr: CT_ApplicationNonVisualDrawingProps = (  # pyright: ignore[reportAssignmentType]
+        OneAndOnlyOne("p:nvPr")
+    )
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/connector.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/connector.py
new file mode 100644
index 00000000..91261f78
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/connector.py
@@ -0,0 +1,107 @@
+"""lxml custom element classes for XML elements related to the Connector shape."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.shapes.shared import BaseShapeElement
+from pptx.oxml.simpletypes import ST_DrawingElementId, XsdUnsignedInt
+from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne, RequiredAttribute, ZeroOrOne
+
+if TYPE_CHECKING:
+    from pptx.oxml.shapes.shared import CT_ShapeProperties
+
+
+class CT_Connection(BaseShapeElement):
+    """A `a:stCxn` or `a:endCxn` element.
+
+    Specifies a connection between an end-point of a connector and a shape connection point.
+    """
+
+    id = RequiredAttribute("id", ST_DrawingElementId)
+    idx = RequiredAttribute("idx", XsdUnsignedInt)
+
+
+class CT_Connector(BaseShapeElement):
+    """A line/connector shape `p:cxnSp` element"""
+
+    _tag_seq = ("p:nvCxnSpPr", "p:spPr", "p:style", "p:extLst")
+    nvCxnSpPr = OneAndOnlyOne("p:nvCxnSpPr")
+    spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr")  # pyright: ignore[reportAssignmentType]
+    del _tag_seq
+
+    @classmethod
+    def new_cxnSp(
+        cls,
+        id_: int,
+        name: str,
+        prst: str,
+        x: int,
+        y: int,
+        cx: int,
+        cy: int,
+        flipH: bool,
+        flipV: bool,
+    ) -> CT_Connector:
+        """Return a new `p:cxnSp` element tree configured as a base connector."""
+        flip = (' flipH="1"' if flipH else "") + (' flipV="1"' if flipV else "")
+        return cast(
+            CT_Connector,
+            parse_xml(
+                f"<p:cxnSp {nsdecls('a', 'p')}>\n"
+                f"  <p:nvCxnSpPr>\n"
+                f'    <p:cNvPr id="{id_}" name="{name}"/>\n'
+                f"    <p:cNvCxnSpPr/>\n"
+                f"    <p:nvPr/>\n"
+                f"  </p:nvCxnSpPr>\n"
+                f"  <p:spPr>\n"
+                f"    <a:xfrm{flip}>\n"
+                f'      <a:off x="{x}" y="{y}"/>\n'
+                f'      <a:ext cx="{cx}" cy="{cy}"/>\n'
+                f"    </a:xfrm>\n"
+                f'    <a:prstGeom prst="{prst}">\n'
+                f"      <a:avLst/>\n"
+                f"    </a:prstGeom>\n"
+                f"  </p:spPr>\n"
+                f"  <p:style>\n"
+                f'    <a:lnRef idx="2">\n'
+                f'      <a:schemeClr val="accent1"/>\n'
+                f"    </a:lnRef>\n"
+                f'    <a:fillRef idx="0">\n'
+                f'      <a:schemeClr val="accent1"/>\n'
+                f"    </a:fillRef>\n"
+                f'    <a:effectRef idx="1">\n'
+                f'      <a:schemeClr val="accent1"/>\n'
+                f"    </a:effectRef>\n"
+                f'    <a:fontRef idx="minor">\n'
+                f'      <a:schemeClr val="tx1"/>\n'
+                f"    </a:fontRef>\n"
+                f"  </p:style>\n"
+                f"</p:cxnSp>"
+            ),
+        )
+
+
+class CT_ConnectorNonVisual(BaseOxmlElement):
+    """
+    `p:nvCxnSpPr` element, container for the non-visual properties of
+    a connector, such as name, id, etc.
+    """
+
+    cNvPr = OneAndOnlyOne("p:cNvPr")
+    cNvCxnSpPr = OneAndOnlyOne("p:cNvCxnSpPr")
+    nvPr = OneAndOnlyOne("p:nvPr")
+
+
+class CT_NonVisualConnectorProperties(BaseOxmlElement):
+    """
+    `p:cNvCxnSpPr` element, container for the non-visual properties specific
+    to a connector shape, such as connections and connector locking.
+    """
+
+    _tag_seq = ("a:cxnSpLocks", "a:stCxn", "a:endCxn", "a:extLst")
+    stCxn = ZeroOrOne("a:stCxn", successors=_tag_seq[2:])
+    endCxn = ZeroOrOne("a:endCxn", successors=_tag_seq[3:])
+    del _tag_seq
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/graphfrm.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/graphfrm.py
new file mode 100644
index 00000000..efa0b363
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/graphfrm.py
@@ -0,0 +1,342 @@
+"""lxml custom element class for CT_GraphicalObjectFrame XML element."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+from pptx.oxml import parse_xml
+from pptx.oxml.chart.chart import CT_Chart
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.shapes.shared import BaseShapeElement
+from pptx.oxml.simpletypes import XsdBoolean, XsdString
+from pptx.oxml.table import CT_Table
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    OneAndOnlyOne,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrOne,
+)
+from pptx.spec import (
+    GRAPHIC_DATA_URI_CHART,
+    GRAPHIC_DATA_URI_OLEOBJ,
+    GRAPHIC_DATA_URI_TABLE,
+)
+
+if TYPE_CHECKING:
+    from pptx.oxml.shapes.shared import (
+        CT_ApplicationNonVisualDrawingProps,
+        CT_NonVisualDrawingProps,
+        CT_Transform2D,
+    )
+
+
+class CT_GraphicalObject(BaseOxmlElement):
+    """`a:graphic` element.
+
+    The container for the reference to or definition of the framed graphical object (table, chart,
+    etc.).
+    """
+
+    graphicData: CT_GraphicalObjectData = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "a:graphicData"
+    )
+
+    @property
+    def chart(self) -> CT_Chart | None:
+        """The `c:chart` grandchild element, or |None| if not present."""
+        return self.graphicData.chart
+
+
+class CT_GraphicalObjectData(BaseShapeElement):
+    """`p:graphicData` element.
+
+    The direct container for a table, a chart, or another graphical object.
+    """
+
+    chart: CT_Chart | None = ZeroOrOne("c:chart")  # pyright: ignore[reportAssignmentType]
+    tbl: CT_Table | None = ZeroOrOne("a:tbl")  # pyright: ignore[reportAssignmentType]
+    uri: str = RequiredAttribute("uri", XsdString)  # pyright: ignore[reportAssignmentType]
+
+    @property
+    def blob_rId(self) -> str | None:
+        """Optional `r:id` attribute value of `p:oleObj` descendent element.
+
+        This value is `None` when this `p:graphicData` element does not enclose an OLE object.
+        This value could also be `None` if an enclosed OLE object does not specify this attribute
+        (it is specified optional in the schema) but so far, all OLE objects we've encountered
+        specify this value.
+        """
+        return None if self._oleObj is None else self._oleObj.rId
+
+    @property
+    def is_embedded_ole_obj(self) -> bool | None:
+        """Optional boolean indicating an embedded OLE object.
+
+        Returns `None` when this `p:graphicData` element does not enclose an OLE object. `True`
+        indicates an embedded OLE object and `False` indicates a linked OLE object.
+        """
+        return None if self._oleObj is None else self._oleObj.is_embedded
+
+    @property
+    def progId(self) -> str | None:
+        """Optional str value of "progId" attribute of `p:oleObj` descendent.
+
+        This value identifies the "type" of the embedded object in terms of the application used
+        to open it.
+
+        This value is `None` when this `p:graphicData` element does not enclose an OLE object.
+        This could also be `None` if an enclosed OLE object does not specify this attribute (it is
+        specified optional in the schema) but so far, all OLE objects we've encountered specify
+        this value.
+        """
+        return None if self._oleObj is None else self._oleObj.progId
+
+    @property
+    def showAsIcon(self) -> bool | None:
+        """Optional value of "showAsIcon" attribute value of `p:oleObj` descendent.
+
+        This value is `None` when this `p:graphicData` element does not enclose an OLE object. It
+        is False when the `showAsIcon` attribute is omitted on the `p:oleObj` element.
+        """
+        return None if self._oleObj is None else self._oleObj.showAsIcon
+
+    @property
+    def _oleObj(self) -> CT_OleObject | None:
+        """Optional `p:oleObj` element contained in this `p:graphicData' element.
+
+        Returns `None` when this graphic-data element does not enclose an OLE object. Note that
+        this returns the last `p:oleObj` element found. There can be more than one `p:oleObj`
+        element because an `mc.AlternateContent` element may appear as the child of
+        `p:graphicData` and that alternate-content subtree can contain multiple compatibility
+        choices. The last one should suit best for reading purposes because it contains the lowest
+        common denominator.
+        """
+        oleObjs = cast("list[CT_OleObject]", self.xpath(".//p:oleObj"))
+        return oleObjs[-1] if oleObjs else None
+
+
+class CT_GraphicalObjectFrame(BaseShapeElement):
+    """`p:graphicFrame` element.
+
+    A container for a table, a chart, or another graphical object.
+    """
+
+    nvGraphicFramePr: CT_GraphicalObjectFrameNonVisual = (  # pyright: ignore[reportAssignmentType]
+        OneAndOnlyOne("p:nvGraphicFramePr")
+    )
+    xfrm: CT_Transform2D = OneAndOnlyOne("p:xfrm")  # pyright: ignore
+    graphic: CT_GraphicalObject = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "a:graphic"
+    )
+
+    @property
+    def chart(self) -> CT_Chart | None:
+        """The `c:chart` great-grandchild element, or |None| if not present."""
+        return self.graphic.chart
+
+    @property
+    def chart_rId(self) -> str | None:
+        """The `rId` attribute of the `c:chart` great-grandchild element.
+
+        |None| if not present.
+        """
+        chart = self.chart
+        if chart is None:
+            return None
+        return chart.rId
+
+    def get_or_add_xfrm(self) -> CT_Transform2D:
+        """Return the required `p:xfrm` child element.
+
+        Overrides version on BaseShapeElement.
+        """
+        return self.xfrm
+
+    @property
+    def graphicData(self) -> CT_GraphicalObjectData:
+        """`a:graphicData` grandchild of this graphic-frame element."""
+        return self.graphic.graphicData
+
+    @property
+    def graphicData_uri(self) -> str:
+        """str value of `uri` attribute of `a:graphicData` grandchild."""
+        return self.graphic.graphicData.uri
+
+    @property
+    def has_oleobj(self) -> bool:
+        """`True` for graphicFrame containing an OLE object, `False` otherwise."""
+        return self.graphicData.uri == GRAPHIC_DATA_URI_OLEOBJ
+
+    @property
+    def is_embedded_ole_obj(self) -> bool | None:
+        """Optional boolean indicating an embedded OLE object.
+
+        Returns `None` when this `p:graphicFrame` element does not enclose an OLE object. `True`
+        indicates an embedded OLE object and `False` indicates a linked OLE object.
+        """
+        return self.graphicData.is_embedded_ole_obj
+
+    @classmethod
+    def new_chart_graphicFrame(
+        cls, id_: int, name: str, rId: str, x: int, y: int, cx: int, cy: int
+    ) -> CT_GraphicalObjectFrame:
+        """Return a `p:graphicFrame` element tree populated with a chart element."""
+        graphicFrame = CT_GraphicalObjectFrame.new_graphicFrame(id_, name, x, y, cx, cy)
+        graphicData = graphicFrame.graphic.graphicData
+        graphicData.uri = GRAPHIC_DATA_URI_CHART
+        graphicData.append(CT_Chart.new_chart(rId))
+        return graphicFrame
+
+    @classmethod
+    def new_graphicFrame(
+        cls, id_: int, name: str, x: int, y: int, cx: int, cy: int
+    ) -> CT_GraphicalObjectFrame:
+        """Return a new `p:graphicFrame` element tree suitable for containing a table or chart.
+
+        Note that a graphicFrame element is not a valid shape until it contains a graphical object
+        such as a table.
+        """
+        return cast(
+            CT_GraphicalObjectFrame,
+            parse_xml(
+                f"<p:graphicFrame {nsdecls('a', 'p')}>\n"
+                f"  <p:nvGraphicFramePr>\n"
+                f'    <p:cNvPr id="{id_}" name="{name}"/>\n'
+                f"    <p:cNvGraphicFramePr>\n"
+                f'      <a:graphicFrameLocks noGrp="1"/>\n'
+                f"    </p:cNvGraphicFramePr>\n"
+                f"    <p:nvPr/>\n"
+                f"  </p:nvGraphicFramePr>\n"
+                f"  <p:xfrm>\n"
+                f'    <a:off x="{x}" y="{y}"/>\n'
+                f'    <a:ext cx="{cx}" cy="{cy}"/>\n'
+                f"  </p:xfrm>\n"
+                f"  <a:graphic>\n"
+                f"    <a:graphicData/>\n"
+                f"  </a:graphic>\n"
+                f"</p:graphicFrame>"
+            ),
+        )
+
+    @classmethod
+    def new_ole_object_graphicFrame(
+        cls,
+        id_: int,
+        name: str,
+        ole_object_rId: str,
+        progId: str,
+        icon_rId: str,
+        x: int,
+        y: int,
+        cx: int,
+        cy: int,
+        imgW: int,
+        imgH: int,
+    ) -> CT_GraphicalObjectFrame:
+        """Return newly-created `p:graphicFrame` for embedded OLE-object.
+
+        `ole_object_rId` identifies the relationship to the OLE-object part.
+
+        `progId` is a str identifying the object-type in terms of the application (program) used
+        to open it. This becomes an attribute of the same name in the `p:oleObj` element.
+
+        `icon_rId` identifies the relationship to an image part used to display the OLE-object as
+        an icon (vs. a preview).
+        """
+        return cast(
+            CT_GraphicalObjectFrame,
+            parse_xml(
+                f"<p:graphicFrame {nsdecls('a', 'p', 'r')}>\n"
+                f"  <p:nvGraphicFramePr>\n"
+                f'    <p:cNvPr id="{id_}" name="{name}"/>\n'
+                f"    <p:cNvGraphicFramePr>\n"
+                f'      <a:graphicFrameLocks noGrp="1"/>\n'
+                f"    </p:cNvGraphicFramePr>\n"
+                f"    <p:nvPr/>\n"
+                f"  </p:nvGraphicFramePr>\n"
+                f"  <p:xfrm>\n"
+                f'    <a:off x="{x}" y="{y}"/>\n'
+                f'    <a:ext cx="{cx}" cy="{cy}"/>\n'
+                f"  </p:xfrm>\n"
+                f"  <a:graphic>\n"
+                f"    <a:graphicData"
+                f'        uri="http://schemas.openxmlformats.org/presentationml/2006/ole">\n'
+                f'      <p:oleObj showAsIcon="1"'
+                f'                r:id="{ole_object_rId}"'
+                f'                imgW="{imgW}"'
+                f'                imgH="{imgH}"'
+                f'                progId="{progId}">\n'
+                f"        <p:embed/>\n"
+                f"        <p:pic>\n"
+                f"          <p:nvPicPr>\n"
+                f'            <p:cNvPr id="0" name=""/>\n'
+                f"            <p:cNvPicPr/>\n"
+                f"            <p:nvPr/>\n"
+                f"          </p:nvPicPr>\n"
+                f"          <p:blipFill>\n"
+                f'            <a:blip r:embed="{icon_rId}"/>\n'
+                f"            <a:stretch>\n"
+                f"              <a:fillRect/>\n"
+                f"            </a:stretch>\n"
+                f"          </p:blipFill>\n"
+                f"          <p:spPr>\n"
+                f"            <a:xfrm>\n"
+                f'              <a:off x="{x}" y="{y}"/>\n'
+                f'              <a:ext cx="{cx}" cy="{cy}"/>\n'
+                f"            </a:xfrm>\n"
+                f'            <a:prstGeom prst="rect">\n'
+                f"              <a:avLst/>\n"
+                f"            </a:prstGeom>\n"
+                f"          </p:spPr>\n"
+                f"        </p:pic>\n"
+                f"      </p:oleObj>\n"
+                f"    </a:graphicData>\n"
+                f"  </a:graphic>\n"
+                f"</p:graphicFrame>"
+            ),
+        )
+
+    @classmethod
+    def new_table_graphicFrame(
+        cls, id_: int, name: str, rows: int, cols: int, x: int, y: int, cx: int, cy: int
+    ) -> CT_GraphicalObjectFrame:
+        """Return a `p:graphicFrame` element tree populated with a table element."""
+        graphicFrame = cls.new_graphicFrame(id_, name, x, y, cx, cy)
+        graphicFrame.graphic.graphicData.uri = GRAPHIC_DATA_URI_TABLE
+        graphicFrame.graphic.graphicData.append(CT_Table.new_tbl(rows, cols, cx, cy))
+        return graphicFrame
+
+
+class CT_GraphicalObjectFrameNonVisual(BaseOxmlElement):
+    """`p:nvGraphicFramePr` element.
+
+    This contains the non-visual properties of a graphic frame, such as name, id, etc.
+    """
+
+    cNvPr: CT_NonVisualDrawingProps = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "p:cNvPr"
+    )
+    nvPr: CT_ApplicationNonVisualDrawingProps = (  # pyright: ignore[reportAssignmentType]
+        OneAndOnlyOne("p:nvPr")
+    )
+
+
+class CT_OleObject(BaseOxmlElement):
+    """`p:oleObj` element, container for an OLE object (e.g. Excel file).
+
+    An OLE object can be either linked or embedded (hence the name).
+    """
+
+    progId: str | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "progId", XsdString
+    )
+    rId: str | None = OptionalAttribute("r:id", XsdString)  # pyright: ignore[reportAssignmentType]
+    showAsIcon: bool = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "showAsIcon", XsdBoolean, default=False
+    )
+
+    @property
+    def is_embedded(self) -> bool:
+        """True when this OLE object is embedded, False when it is linked."""
+        return len(self.xpath("./p:embed")) > 0
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/groupshape.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/groupshape.py
new file mode 100644
index 00000000..f62bc666
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/groupshape.py
@@ -0,0 +1,280 @@
+"""lxml custom element classes for shape-tree-related XML elements."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, Iterator
+
+from pptx.enum.shapes import MSO_CONNECTOR_TYPE
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls, qn
+from pptx.oxml.shapes.autoshape import CT_Shape
+from pptx.oxml.shapes.connector import CT_Connector
+from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
+from pptx.oxml.shapes.picture import CT_Picture
+from pptx.oxml.shapes.shared import BaseShapeElement
+from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne, ZeroOrOne
+from pptx.util import Emu
+
+if TYPE_CHECKING:
+    from pptx.enum.shapes import PP_PLACEHOLDER
+    from pptx.oxml.shapes import ShapeElement
+    from pptx.oxml.shapes.shared import CT_Transform2D
+
+
+class CT_GroupShape(BaseShapeElement):
+    """Used for shape tree (`p:spTree`) as well as the group shape (`p:grpSp`) elements."""
+
+    nvGrpSpPr: CT_GroupShapeNonVisual = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "p:nvGrpSpPr"
+    )
+    grpSpPr: CT_GroupShapeProperties = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "p:grpSpPr"
+    )
+
+    _shape_tags = (
+        qn("p:sp"),
+        qn("p:grpSp"),
+        qn("p:graphicFrame"),
+        qn("p:cxnSp"),
+        qn("p:pic"),
+        qn("p:contentPart"),
+    )
+
+    def add_autoshape(
+        self, id_: int, name: str, prst: str, x: int, y: int, cx: int, cy: int
+    ) -> CT_Shape:
+        """Return new `p:sp` appended to the group/shapetree with specified attributes."""
+        sp = CT_Shape.new_autoshape_sp(id_, name, prst, x, y, cx, cy)
+        self.insert_element_before(sp, "p:extLst")
+        return sp
+
+    def add_cxnSp(
+        self,
+        id_: int,
+        name: str,
+        type_member: MSO_CONNECTOR_TYPE,
+        x: int,
+        y: int,
+        cx: int,
+        cy: int,
+        flipH: bool,
+        flipV: bool,
+    ) -> CT_Connector:
+        """Return new `p:cxnSp` appended to the group/shapetree with the specified attribues."""
+        prst = MSO_CONNECTOR_TYPE.to_xml(type_member)
+        cxnSp = CT_Connector.new_cxnSp(id_, name, prst, x, y, cx, cy, flipH, flipV)
+        self.insert_element_before(cxnSp, "p:extLst")
+        return cxnSp
+
+    def add_freeform_sp(self, x: int, y: int, cx: int, cy: int) -> CT_Shape:
+        """Append a new freeform `p:sp` with specified position and size."""
+        shape_id = self._next_shape_id
+        name = "Freeform %d" % (shape_id - 1,)
+        sp = CT_Shape.new_freeform_sp(shape_id, name, x, y, cx, cy)
+        self.insert_element_before(sp, "p:extLst")
+        return sp
+
+    def add_grpSp(self) -> CT_GroupShape:
+        """Return `p:grpSp` element newly appended to this shape tree.
+
+        The element contains no sub-shapes, is positioned at (0, 0), and has
+        width and height of zero.
+        """
+        shape_id = self._next_shape_id
+        name = "Group %d" % (shape_id - 1,)
+        grpSp = CT_GroupShape.new_grpSp(shape_id, name)
+        self.insert_element_before(grpSp, "p:extLst")
+        return grpSp
+
+    def add_pic(
+        self, id_: int, name: str, desc: str, rId: str, x: int, y: int, cx: int, cy: int
+    ) -> CT_Picture:
+        """Append a `p:pic` shape to the group/shapetree having properties as specified in call."""
+        pic = CT_Picture.new_pic(id_, name, desc, rId, x, y, cx, cy)
+        self.insert_element_before(pic, "p:extLst")
+        return pic
+
+    def add_placeholder(
+        self, id_: int, name: str, ph_type: PP_PLACEHOLDER, orient: str, sz: str, idx: int
+    ) -> CT_Shape:
+        """Append a newly-created placeholder `p:sp` shape having the specified properties."""
+        sp = CT_Shape.new_placeholder_sp(id_, name, ph_type, orient, sz, idx)
+        self.insert_element_before(sp, "p:extLst")
+        return sp
+
+    def add_table(
+        self, id_: int, name: str, rows: int, cols: int, x: int, y: int, cx: int, cy: int
+    ) -> CT_GraphicalObjectFrame:
+        """Append a `p:graphicFrame` shape containing a table as specified in call."""
+        graphicFrame = CT_GraphicalObjectFrame.new_table_graphicFrame(
+            id_, name, rows, cols, x, y, cx, cy
+        )
+        self.insert_element_before(graphicFrame, "p:extLst")
+        return graphicFrame
+
+    def add_textbox(self, id_: int, name: str, x: int, y: int, cx: int, cy: int) -> CT_Shape:
+        """Append a newly-created textbox `p:sp` shape having the specified position and size."""
+        sp = CT_Shape.new_textbox_sp(id_, name, x, y, cx, cy)
+        self.insert_element_before(sp, "p:extLst")
+        return sp
+
+    @property
+    def chExt(self):
+        """Descendent `p:grpSpPr/a:xfrm/a:chExt` element."""
+        return self.grpSpPr.get_or_add_xfrm().get_or_add_chExt()
+
+    @property
+    def chOff(self):
+        """Descendent `p:grpSpPr/a:xfrm/a:chOff` element."""
+        return self.grpSpPr.get_or_add_xfrm().get_or_add_chOff()
+
+    def get_or_add_xfrm(self) -> CT_Transform2D:
+        """Return the `a:xfrm` grandchild element, newly-added if not present."""
+        return self.grpSpPr.get_or_add_xfrm()
+
+    def iter_ph_elms(self):
+        """Generate each placeholder shape child element in document order."""
+        for e in self.iter_shape_elms():
+            if e.has_ph_elm:
+                yield e
+
+    def iter_shape_elms(self) -> Iterator[ShapeElement]:
+        """Generate each child of this `p:spTree` element that corresponds to a shape.
+
+        Items appear in XML document order.
+        """
+        for elm in self.iterchildren():
+            if elm.tag in self._shape_tags:
+                yield elm
+
+    @property
+    def max_shape_id(self) -> int:
+        """Maximum int value assigned as @id in this slide.
+
+        This is generally a shape-id, but ids can be assigned to other
+        objects so we just check all @id values anywhere in the document
+        (XML id-values have document scope).
+
+        In practice, its minimum value is 1 because the spTree element itself
+        is always assigned id="1".
+        """
+        id_str_lst = self.xpath("//@id")
+        used_ids = [int(id_str) for id_str in id_str_lst if id_str.isdigit()]
+        return max(used_ids) if used_ids else 0
+
+    @classmethod
+    def new_grpSp(cls, id_: int, name: str) -> CT_GroupShape:
+        """Return new "loose" `p:grpSp` element having `id_` and `name`."""
+        xml = (
+            "<p:grpSp %s>\n"
+            "  <p:nvGrpSpPr>\n"
+            '    <p:cNvPr id="%%d" name="%%s"/>\n'
+            "    <p:cNvGrpSpPr/>\n"
+            "    <p:nvPr/>\n"
+            "  </p:nvGrpSpPr>\n"
+            "  <p:grpSpPr>\n"
+            "    <a:xfrm>\n"
+            '      <a:off x="0" y="0"/>\n'
+            '      <a:ext cx="0" cy="0"/>\n'
+            '      <a:chOff x="0" y="0"/>\n'
+            '      <a:chExt cx="0" cy="0"/>\n'
+            "    </a:xfrm>\n"
+            "  </p:grpSpPr>\n"
+            "</p:grpSp>" % nsdecls("a", "p", "r")
+        ) % (id_, name)
+        grpSp = parse_xml(xml)
+        return grpSp
+
+    def recalculate_extents(self) -> None:
+        """Adjust x, y, cx, and cy to incorporate all contained shapes.
+
+        This would typically be called when a contained shape is added,
+        removed, or its position or size updated.
+
+        This method is recursive "upwards" since a change in a group shape
+        can change the position and size of its containing group.
+        """
+        if not self.tag == qn("p:grpSp"):
+            return
+
+        x, y, cx, cy = self._child_extents
+
+        self.chOff.x = self.x = x
+        self.chOff.y = self.y = y
+        self.chExt.cx = self.cx = cx
+        self.chExt.cy = self.cy = cy
+        self.getparent().recalculate_extents()
+
+    @property
+    def xfrm(self) -> CT_Transform2D | None:
+        """The `a:xfrm` grandchild element or |None| if not found."""
+        return self.grpSpPr.xfrm
+
+    @property
+    def _child_extents(self) -> tuple[int, int, int, int]:
+        """(x, y, cx, cy) tuple representing net position and size.
+
+        The values are formed as a composite of the contained child shapes.
+        """
+        child_shape_elms = list(self.iter_shape_elms())
+
+        if not child_shape_elms:
+            return Emu(0), Emu(0), Emu(0), Emu(0)
+
+        min_x = min([xSp.x for xSp in child_shape_elms])
+        min_y = min([xSp.y for xSp in child_shape_elms])
+        max_x = max([(xSp.x + xSp.cx) for xSp in child_shape_elms])
+        max_y = max([(xSp.y + xSp.cy) for xSp in child_shape_elms])
+
+        x = min_x
+        y = min_y
+        cx = max_x - min_x
+        cy = max_y - min_y
+
+        return x, y, cx, cy
+
+    @property
+    def _next_shape_id(self) -> int:
+        """Return unique shape id suitable for use with a new shape element.
+
+        The returned id is the next available positive integer drawing object
+        id in shape tree, starting from 1 and making use of any gaps in
+        numbering. In practice, the minimum id is 2 because the spTree
+        element itself is always assigned id="1".
+        """
+        id_str_lst = self.xpath("//@id")
+        used_ids = [int(id_str) for id_str in id_str_lst if id_str.isdigit()]
+        for n in range(1, len(used_ids) + 2):
+            if n not in used_ids:
+                return n
+
+
+class CT_GroupShapeNonVisual(BaseShapeElement):
+    """`p:nvGrpSpPr` element."""
+
+    cNvPr = OneAndOnlyOne("p:cNvPr")
+
+
+class CT_GroupShapeProperties(BaseOxmlElement):
+    """p:grpSpPr element"""
+
+    get_or_add_xfrm: Callable[[], CT_Transform2D]
+
+    _tag_seq = (
+        "a:xfrm",
+        "a:noFill",
+        "a:solidFill",
+        "a:gradFill",
+        "a:blipFill",
+        "a:pattFill",
+        "a:grpFill",
+        "a:effectLst",
+        "a:effectDag",
+        "a:scene3d",
+        "a:extLst",
+    )
+    xfrm: CT_Transform2D | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:xfrm", successors=_tag_seq[1:]
+    )
+    effectLst = ZeroOrOne("a:effectLst", successors=_tag_seq[8:])
+    del _tag_seq
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/picture.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/picture.py
new file mode 100644
index 00000000..bacc9719
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/picture.py
@@ -0,0 +1,270 @@
+"""lxml custom element classes for picture-related XML elements."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+from xml.sax.saxutils import escape
+
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.shapes.shared import BaseShapeElement
+from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne
+
+if TYPE_CHECKING:
+    from pptx.oxml.shapes.shared import CT_ShapeProperties
+    from pptx.util import Length
+
+
+class CT_Picture(BaseShapeElement):
+    """`p:pic` element.
+
+    Represents a picture shape (an image placement on a slide).
+    """
+
+    nvPicPr = OneAndOnlyOne("p:nvPicPr")
+    blipFill = OneAndOnlyOne("p:blipFill")
+    spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr")  # pyright: ignore[reportAssignmentType]
+
+    @property
+    def blip_rId(self) -> str | None:
+        """Value of `p:blipFill/a:blip/@r:embed`.
+
+        Returns |None| if not present.
+        """
+        blip = self.blipFill.blip
+        if blip is not None and blip.rEmbed is not None:
+            return blip.rEmbed
+        return None
+
+    def crop_to_fit(self, image_size, view_size):
+        """
+        Set cropping values in `p:blipFill/a:srcRect` such that an image of
+        *image_size* will stretch to exactly fit *view_size* when its aspect
+        ratio is preserved.
+        """
+        self.blipFill.crop(self._fill_cropping(image_size, view_size))
+
+    def get_or_add_ln(self):
+        """
+        Return the <a:ln> grandchild element, newly added if not present.
+        """
+        return self.spPr.get_or_add_ln()
+
+    @property
+    def ln(self):
+        """
+        ``<a:ln>`` grand-child element or |None| if not present
+        """
+        return self.spPr.ln
+
+    @classmethod
+    def new_ph_pic(cls, id_, name, desc, rId):
+        """
+        Return a new `p:pic` placeholder element populated with the supplied
+        parameters.
+        """
+        return parse_xml(cls._pic_ph_tmpl() % (id_, name, desc, rId))
+
+    @classmethod
+    def new_pic(cls, shape_id, name, desc, rId, x, y, cx, cy):
+        """Return new `<p:pic>` element tree configured with supplied parameters."""
+        return parse_xml(cls._pic_tmpl() % (shape_id, name, escape(desc), rId, x, y, cx, cy))
+
+    @classmethod
+    def new_video_pic(
+        cls,
+        shape_id: int,
+        shape_name: str,
+        video_rId: str,
+        media_rId: str,
+        poster_frame_rId: str,
+        x: Length,
+        y: Length,
+        cx: Length,
+        cy: Length,
+    ) -> CT_Picture:
+        """Return a new `p:pic` populated with the specified video."""
+        return cast(
+            CT_Picture,
+            parse_xml(
+                cls._pic_video_tmpl()
+                % (
+                    shape_id,
+                    shape_name,
+                    video_rId,
+                    media_rId,
+                    poster_frame_rId,
+                    x,
+                    y,
+                    cx,
+                    cy,
+                )
+            ),
+        )
+
+    @property
+    def srcRect_b(self):
+        """Value of `p:blipFill/a:srcRect/@b` or 0.0 if not present."""
+        return self._srcRect_x("b")
+
+    @srcRect_b.setter
+    def srcRect_b(self, value):
+        self.blipFill.get_or_add_srcRect().b = value
+
+    @property
+    def srcRect_l(self):
+        """Value of `p:blipFill/a:srcRect/@l` or 0.0 if not present."""
+        return self._srcRect_x("l")
+
+    @srcRect_l.setter
+    def srcRect_l(self, value):
+        self.blipFill.get_or_add_srcRect().l = value  # noqa
+
+    @property
+    def srcRect_r(self):
+        """Value of `p:blipFill/a:srcRect/@r` or 0.0 if not present."""
+        return self._srcRect_x("r")
+
+    @srcRect_r.setter
+    def srcRect_r(self, value):
+        self.blipFill.get_or_add_srcRect().r = value
+
+    @property
+    def srcRect_t(self):
+        """Value of `p:blipFill/a:srcRect/@t` or 0.0 if not present."""
+        return self._srcRect_x("t")
+
+    @srcRect_t.setter
+    def srcRect_t(self, value):
+        self.blipFill.get_or_add_srcRect().t = value
+
+    def _fill_cropping(self, image_size, view_size):
+        """
+        Return a (left, top, right, bottom) 4-tuple containing the cropping
+        values required to display an image of *image_size* in *view_size*
+        when stretched proportionately. Each value is a percentage expressed
+        as a fraction of 1.0, e.g. 0.425 represents 42.5%. *image_size* and
+        *view_size* are each (width, height) pairs.
+        """
+
+        def aspect_ratio(width, height):
+            return width / height
+
+        ar_view = aspect_ratio(*view_size)
+        ar_image = aspect_ratio(*image_size)
+
+        if ar_view < ar_image:  # image too wide
+            crop = (1.0 - (ar_view / ar_image)) / 2.0
+            return (crop, 0.0, crop, 0.0)
+        if ar_view > ar_image:  # image too tall
+            crop = (1.0 - (ar_image / ar_view)) / 2.0
+            return (0.0, crop, 0.0, crop)
+        return (0.0, 0.0, 0.0, 0.0)
+
+    @classmethod
+    def _pic_ph_tmpl(cls):
+        return (
+            "<p:pic %s>\n"
+            "  <p:nvPicPr>\n"
+            '    <p:cNvPr id="%%d" name="%%s" descr="%%s"/>\n'
+            "    <p:cNvPicPr>\n"
+            '      <a:picLocks noGrp="1" noChangeAspect="1"/>\n'
+            "    </p:cNvPicPr>\n"
+            "    <p:nvPr/>\n"
+            "  </p:nvPicPr>\n"
+            "  <p:blipFill>\n"
+            '    <a:blip r:embed="%%s"/>\n'
+            "    <a:stretch>\n"
+            "      <a:fillRect/>\n"
+            "    </a:stretch>\n"
+            "  </p:blipFill>\n"
+            "  <p:spPr/>\n"
+            "</p:pic>" % nsdecls("p", "a", "r")
+        )
+
+    @classmethod
+    def _pic_tmpl(cls):
+        return (
+            "<p:pic %s>\n"
+            "  <p:nvPicPr>\n"
+            '    <p:cNvPr id="%%d" name="%%s" descr="%%s"/>\n'
+            "    <p:cNvPicPr>\n"
+            '      <a:picLocks noChangeAspect="1"/>\n'
+            "    </p:cNvPicPr>\n"
+            "    <p:nvPr/>\n"
+            "  </p:nvPicPr>\n"
+            "  <p:blipFill>\n"
+            '    <a:blip r:embed="%%s"/>\n'
+            "    <a:stretch>\n"
+            "      <a:fillRect/>\n"
+            "    </a:stretch>\n"
+            "  </p:blipFill>\n"
+            "  <p:spPr>\n"
+            "    <a:xfrm>\n"
+            '      <a:off x="%%d" y="%%d"/>\n'
+            '      <a:ext cx="%%d" cy="%%d"/>\n'
+            "    </a:xfrm>\n"
+            '    <a:prstGeom prst="rect">\n'
+            "      <a:avLst/>\n"
+            "    </a:prstGeom>\n"
+            "  </p:spPr>\n"
+            "</p:pic>" % nsdecls("a", "p", "r")
+        )
+
+    @classmethod
+    def _pic_video_tmpl(cls):
+        return (
+            "<p:pic %s>\n"
+            "  <p:nvPicPr>\n"
+            '    <p:cNvPr id="%%d" name="%%s">\n'
+            '      <a:hlinkClick r:id="" action="ppaction://media"/>\n'
+            "    </p:cNvPr>\n"
+            "    <p:cNvPicPr>\n"
+            '      <a:picLocks noChangeAspect="1"/>\n'
+            "    </p:cNvPicPr>\n"
+            "    <p:nvPr>\n"
+            '      <a:videoFile r:link="%%s"/>\n'
+            "      <p:extLst>\n"
+            '        <p:ext uri="{DAA4B4D4-6D71-4841-9C94-3DE7FCFB9230}">\n'
+            '          <p14:media xmlns:p14="http://schemas.microsoft.com/of'
+            'fice/powerpoint/2010/main" r:embed="%%s"/>\n'
+            "        </p:ext>\n"
+            "      </p:extLst>\n"
+            "    </p:nvPr>\n"
+            "  </p:nvPicPr>\n"
+            "  <p:blipFill>\n"
+            '    <a:blip r:embed="%%s"/>\n'
+            "    <a:stretch>\n"
+            "      <a:fillRect/>\n"
+            "    </a:stretch>\n"
+            "  </p:blipFill>\n"
+            "  <p:spPr>\n"
+            "    <a:xfrm>\n"
+            '      <a:off x="%%d" y="%%d"/>\n'
+            '      <a:ext cx="%%d" cy="%%d"/>\n'
+            "    </a:xfrm>\n"
+            '    <a:prstGeom prst="rect">\n'
+            "      <a:avLst/>\n"
+            "    </a:prstGeom>\n"
+            "  </p:spPr>\n"
+            "</p:pic>" % nsdecls("a", "p", "r")
+        )
+
+    def _srcRect_x(self, attr_name):
+        """
+        Value of `p:blipFill/a:srcRect/@{attr_name}` or 0.0 if not present.
+        """
+        srcRect = self.blipFill.srcRect
+        if srcRect is None:
+            return 0.0
+        return getattr(srcRect, attr_name)
+
+
+class CT_PictureNonVisual(BaseOxmlElement):
+    """
+    ``<p:nvPicPr>`` element, containing non-visual properties for a picture
+    shape.
+    """
+
+    cNvPr = OneAndOnlyOne("p:cNvPr")
+    nvPr = OneAndOnlyOne("p:nvPr")
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/shared.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/shared.py
new file mode 100644
index 00000000..d9f94569
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/shared.py
@@ -0,0 +1,523 @@
+"""Common shape-related oxml objects."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable
+
+from pptx.dml.fill import CT_GradientFillProperties
+from pptx.enum.shapes import PP_PLACEHOLDER
+from pptx.oxml.ns import qn
+from pptx.oxml.simpletypes import (
+    ST_Angle,
+    ST_Coordinate,
+    ST_Direction,
+    ST_DrawingElementId,
+    ST_LineWidth,
+    ST_PlaceholderSize,
+    ST_PositiveCoordinate,
+    XsdBoolean,
+    XsdString,
+    XsdUnsignedInt,
+)
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    Choice,
+    OptionalAttribute,
+    OxmlElement,
+    RequiredAttribute,
+    ZeroOrOne,
+    ZeroOrOneChoice,
+)
+from pptx.util import Emu
+
+if TYPE_CHECKING:
+    from pptx.oxml.action import CT_Hyperlink
+    from pptx.oxml.shapes.autoshape import CT_CustomGeometry2D, CT_PresetGeometry2D
+    from pptx.util import Length
+
+
+class BaseShapeElement(BaseOxmlElement):
+    """Provides common behavior for shape element classes like CT_Shape, CT_Picture, etc."""
+
+    spPr: CT_ShapeProperties
+
+    @property
+    def cx(self) -> Length:
+        return self._get_xfrm_attr("cx")
+
+    @cx.setter
+    def cx(self, value):
+        self._set_xfrm_attr("cx", value)
+
+    @property
+    def cy(self) -> Length:
+        return self._get_xfrm_attr("cy")
+
+    @cy.setter
+    def cy(self, value):
+        self._set_xfrm_attr("cy", value)
+
+    @property
+    def flipH(self):
+        return bool(self._get_xfrm_attr("flipH"))
+
+    @flipH.setter
+    def flipH(self, value):
+        self._set_xfrm_attr("flipH", value)
+
+    @property
+    def flipV(self):
+        return bool(self._get_xfrm_attr("flipV"))
+
+    @flipV.setter
+    def flipV(self, value):
+        self._set_xfrm_attr("flipV", value)
+
+    def get_or_add_xfrm(self):
+        """Return the `a:xfrm` grandchild element, newly-added if not present.
+
+        This version works for `p:sp`, `p:cxnSp`, and `p:pic` elements, others will need to
+        override.
+        """
+        return self.spPr.get_or_add_xfrm()
+
+    @property
+    def has_ph_elm(self):
+        """
+        True if this shape element has a `p:ph` descendant, indicating it
+        is a placeholder shape. False otherwise.
+        """
+        return self.ph is not None
+
+    @property
+    def ph(self) -> CT_Placeholder | None:
+        """The `p:ph` descendant element if there is one, None otherwise."""
+        ph_elms = self.xpath("./*[1]/p:nvPr/p:ph")
+        if len(ph_elms) == 0:
+            return None
+        return ph_elms[0]
+
+    @property
+    def ph_idx(self) -> int:
+        """Integer value of placeholder idx attribute.
+
+        Raises |ValueError| if shape is not a placeholder.
+        """
+        ph = self.ph
+        if ph is None:
+            raise ValueError("not a placeholder shape")
+        return ph.idx
+
+    @property
+    def ph_orient(self) -> str:
+        """Placeholder orientation, e.g. 'vert'.
+
+        Raises |ValueError| if shape is not a placeholder.
+        """
+        ph = self.ph
+        if ph is None:
+            raise ValueError("not a placeholder shape")
+        return ph.orient
+
+    @property
+    def ph_sz(self) -> str:
+        """Placeholder size, e.g. ST_PlaceholderSize.HALF.
+
+        Raises `ValueError` if shape is not a placeholder.
+        """
+        ph = self.ph
+        if ph is None:
+            raise ValueError("not a placeholder shape")
+        return ph.sz
+
+    @property
+    def ph_type(self):
+        """Placeholder type, e.g. ST_PlaceholderType.TITLE ('title').
+
+        Raises `ValueError` if shape is not a placeholder.
+        """
+        ph = self.ph
+        if ph is None:
+            raise ValueError("not a placeholder shape")
+        return ph.type
+
+    @property
+    def rot(self) -> float:
+        """Float representing degrees this shape is rotated clockwise."""
+        xfrm = self.xfrm
+        if xfrm is None or xfrm.rot is None:
+            return 0.0
+        return xfrm.rot
+
+    @rot.setter
+    def rot(self, value: float):
+        self.get_or_add_xfrm().rot = value
+
+    @property
+    def shape_id(self):
+        """
+        Integer id of this shape
+        """
+        return self._nvXxPr.cNvPr.id
+
+    @property
+    def shape_name(self):
+        """
+        Name of this shape
+        """
+        return self._nvXxPr.cNvPr.name
+
+    @property
+    def txBody(self):
+        """Child `p:txBody` element, None if not present."""
+        return self.find(qn("p:txBody"))
+
+    @property
+    def x(self) -> Length:
+        return self._get_xfrm_attr("x")
+
+    @x.setter
+    def x(self, value):
+        self._set_xfrm_attr("x", value)
+
+    @property
+    def xfrm(self):
+        """The `a:xfrm` grandchild element or |None| if not found.
+
+        This version works for `p:sp`, `p:cxnSp`, and `p:pic` elements, others will need to
+        override.
+        """
+        return self.spPr.xfrm
+
+    @property
+    def y(self) -> Length:
+        return self._get_xfrm_attr("y")
+
+    @y.setter
+    def y(self, value):
+        self._set_xfrm_attr("y", value)
+
+    @property
+    def _nvXxPr(self):
+        """
+        Required non-visual shape properties element for this shape. Actual
+        name depends on the shape type, e.g. `p:nvPicPr` for picture
+        shape.
+        """
+        return self.xpath("./*[1]")[0]
+
+    def _get_xfrm_attr(self, name: str) -> Length | None:
+        xfrm = self.xfrm
+        if xfrm is None:
+            return None
+        return getattr(xfrm, name)
+
+    def _set_xfrm_attr(self, name, value):
+        xfrm = self.get_or_add_xfrm()
+        setattr(xfrm, name, value)
+
+
+class CT_ApplicationNonVisualDrawingProps(BaseOxmlElement):
+    """`p:nvPr` element."""
+
+    get_or_add_ph: Callable[[], CT_Placeholder]
+
+    ph = ZeroOrOne(
+        "p:ph",
+        successors=(
+            "a:audioCd",
+            "a:wavAudioFile",
+            "a:audioFile",
+            "a:videoFile",
+            "a:quickTimeFile",
+            "p:custDataLst",
+            "p:extLst",
+        ),
+    )
+
+
+class CT_LineProperties(BaseOxmlElement):
+    """Custom element class for <a:ln> element"""
+
+    _tag_seq = (
+        "a:noFill",
+        "a:solidFill",
+        "a:gradFill",
+        "a:pattFill",
+        "a:prstDash",
+        "a:custDash",
+        "a:round",
+        "a:bevel",
+        "a:miter",
+        "a:headEnd",
+        "a:tailEnd",
+        "a:extLst",
+    )
+    eg_lineFillProperties = ZeroOrOneChoice(
+        (
+            Choice("a:noFill"),
+            Choice("a:solidFill"),
+            Choice("a:gradFill"),
+            Choice("a:pattFill"),
+        ),
+        successors=_tag_seq[4:],
+    )
+    prstDash = ZeroOrOne("a:prstDash", successors=_tag_seq[5:])
+    custDash = ZeroOrOne("a:custDash", successors=_tag_seq[6:])
+    del _tag_seq
+    w = OptionalAttribute("w", ST_LineWidth, default=Emu(0))
+
+    @property
+    def eg_fillProperties(self):
+        """
+        Required to fulfill the interface used by dml.fill.
+        """
+        return self.eg_lineFillProperties
+
+    @property
+    def prstDash_val(self):
+        """Return value of `val` attribute of `a:prstDash` child.
+
+        Return |None| if not present.
+        """
+        prstDash = self.prstDash
+        if prstDash is None:
+            return None
+        return prstDash.val
+
+    @prstDash_val.setter
+    def prstDash_val(self, val):
+        self._remove_custDash()
+        prstDash = self.get_or_add_prstDash()
+        prstDash.val = val
+
+
+class CT_NonVisualDrawingProps(BaseOxmlElement):
+    """`p:cNvPr` custom element class."""
+
+    get_or_add_hlinkClick: Callable[[], CT_Hyperlink]
+    get_or_add_hlinkHover: Callable[[], CT_Hyperlink]
+
+    _tag_seq = ("a:hlinkClick", "a:hlinkHover", "a:extLst")
+    hlinkClick: CT_Hyperlink | None = ZeroOrOne("a:hlinkClick", successors=_tag_seq[1:])
+    hlinkHover: CT_Hyperlink | None = ZeroOrOne("a:hlinkHover", successors=_tag_seq[2:])
+    id = RequiredAttribute("id", ST_DrawingElementId)
+    name = RequiredAttribute("name", XsdString)
+    del _tag_seq
+
+
+class CT_Placeholder(BaseOxmlElement):
+    """`p:ph` custom element class."""
+
+    type: PP_PLACEHOLDER = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "type", PP_PLACEHOLDER, default=PP_PLACEHOLDER.OBJECT
+    )
+    orient: str = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "orient", ST_Direction, default=ST_Direction.HORZ
+    )
+    sz: str = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "sz", ST_PlaceholderSize, default=ST_PlaceholderSize.FULL
+    )
+    idx: int = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "idx", XsdUnsignedInt, default=0
+    )
+
+
+class CT_Point2D(BaseOxmlElement):
+    """
+    Custom element class for <a:off> element.
+    """
+
+    x: Length = RequiredAttribute("x", ST_Coordinate)  # pyright: ignore[reportAssignmentType]
+    y: Length = RequiredAttribute("y", ST_Coordinate)  # pyright: ignore[reportAssignmentType]
+
+
+class CT_PositiveSize2D(BaseOxmlElement):
+    """
+    Custom element class for <a:ext> element.
+    """
+
+    cx = RequiredAttribute("cx", ST_PositiveCoordinate)
+    cy = RequiredAttribute("cy", ST_PositiveCoordinate)
+
+
+class CT_ShapeProperties(BaseOxmlElement):
+    """Custom element class for `p:spPr` element.
+
+    Shared by `p:sp`, `p:cxnSp`,  and `p:pic` elements as well as a few more obscure ones.
+    """
+
+    get_or_add_xfrm: Callable[[], CT_Transform2D]
+    get_or_add_ln: Callable[[], CT_LineProperties]
+    _add_prstGeom: Callable[[], CT_PresetGeometry2D]
+    _remove_custGeom: Callable[[], None]
+
+    _tag_seq = (
+        "a:xfrm",
+        "a:custGeom",
+        "a:prstGeom",
+        "a:noFill",
+        "a:solidFill",
+        "a:gradFill",
+        "a:blipFill",
+        "a:pattFill",
+        "a:grpFill",
+        "a:ln",
+        "a:effectLst",
+        "a:effectDag",
+        "a:scene3d",
+        "a:sp3d",
+        "a:extLst",
+    )
+    xfrm: CT_Transform2D | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:xfrm", successors=_tag_seq[1:]
+    )
+    custGeom: CT_CustomGeometry2D | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:custGeom", successors=_tag_seq[2:]
+    )
+    prstGeom: CT_PresetGeometry2D | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:prstGeom", successors=_tag_seq[3:]
+    )
+    eg_fillProperties = ZeroOrOneChoice(
+        (
+            Choice("a:noFill"),
+            Choice("a:solidFill"),
+            Choice("a:gradFill"),
+            Choice("a:blipFill"),
+            Choice("a:pattFill"),
+            Choice("a:grpFill"),
+        ),
+        successors=_tag_seq[9:],
+    )
+    ln: CT_LineProperties | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:ln", successors=_tag_seq[10:]
+    )
+    effectLst = ZeroOrOne("a:effectLst", successors=_tag_seq[11:])
+    del _tag_seq
+
+    @property
+    def cx(self):
+        """
+        Shape width as an instance of Emu, or None if not present.
+        """
+        cx_str_lst = self.xpath("./a:xfrm/a:ext/@cx")
+        if not cx_str_lst:
+            return None
+        return Emu(cx_str_lst[0])
+
+    @property
+    def cy(self):
+        """
+        Shape height as an instance of Emu, or None if not present.
+        """
+        cy_str_lst = self.xpath("./a:xfrm/a:ext/@cy")
+        if not cy_str_lst:
+            return None
+        return Emu(cy_str_lst[0])
+
+    @property
+    def x(self) -> Length | None:
+        """Distance between the left edge of the slide and left edge of the shape.
+
+        0 if not present.
+        """
+        x_str_lst = self.xpath("./a:xfrm/a:off/@x")
+        if not x_str_lst:
+            return None
+        return Emu(x_str_lst[0])
+
+    @property
+    def y(self):
+        """
+        The offset of the top of the shape from the top of the slide, as an
+        instance of Emu. None if not present.
+        """
+        y_str_lst = self.xpath("./a:xfrm/a:off/@y")
+        if not y_str_lst:
+            return None
+        return Emu(y_str_lst[0])
+
+    def _new_gradFill(self):
+        return CT_GradientFillProperties.new_gradFill()
+
+
+class CT_Transform2D(BaseOxmlElement):
+    """`a:xfrm` custom element class.
+
+    NOTE: this is a composite including CT_GroupTransform2D, which appears
+    with the `a:xfrm` tag in a group shape (including a slide `p:spTree`).
+    """
+
+    _tag_seq = ("a:off", "a:ext", "a:chOff", "a:chExt")
+    off: CT_Point2D | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:off", successors=_tag_seq[1:]
+    )
+    ext = ZeroOrOne("a:ext", successors=_tag_seq[2:])
+    chOff = ZeroOrOne("a:chOff", successors=_tag_seq[3:])
+    chExt = ZeroOrOne("a:chExt", successors=_tag_seq[4:])
+    del _tag_seq
+    rot: float | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "rot", ST_Angle, default=0.0
+    )
+    flipH = OptionalAttribute("flipH", XsdBoolean, default=False)
+    flipV = OptionalAttribute("flipV", XsdBoolean, default=False)
+
+    @property
+    def x(self):
+        off = self.off
+        if off is None:
+            return None
+        return off.x
+
+    @x.setter
+    def x(self, value):
+        off = self.get_or_add_off()
+        off.x = value
+
+    @property
+    def y(self):
+        off = self.off
+        if off is None:
+            return None
+        return off.y
+
+    @y.setter
+    def y(self, value):
+        off = self.get_or_add_off()
+        off.y = value
+
+    @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
+
+    def _new_ext(self):
+        ext = OxmlElement("a:ext")
+        ext.cx = 0
+        ext.cy = 0
+        return ext
+
+    def _new_off(self):
+        off = OxmlElement("a:off")
+        off.x = 0
+        off.y = 0
+        return off
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/simpletypes.py b/.venv/lib/python3.12/site-packages/pptx/oxml/simpletypes.py
new file mode 100644
index 00000000..6ceb06f7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/simpletypes.py
@@ -0,0 +1,740 @@
+"""Simple-type classes.
+
+A "simple-type" is a scalar type, generally serving as an XML attribute. This is in contrast to a
+"complex-type" which would specify an XML element.
+
+These objects providing 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
+
+import numbers
+from typing import Any
+
+from pptx.exc import InvalidXmlError
+from pptx.util import Centipoints, Emu
+
+
+class BaseSimpleType:
+    @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 validate_float(cls, value: Any):
+        """Note that int values are accepted."""
+        if not isinstance(value, (int, float)):
+            raise TypeError("value must be a number, got %s" % type(value))
+
+    @classmethod
+    def validate_int(cls, value):
+        if not isinstance(value, numbers.Integral):
+            raise TypeError("value must be an integral type, got %s" % type(value))
+
+    @classmethod
+    def validate_float_in_range(cls, value, min_inclusive, max_inclusive):
+        cls.validate_float(value)
+        if value < min_inclusive or value > max_inclusive:
+            raise ValueError(
+                "value must be in range %s to %s inclusive, got %s"
+                % (min_inclusive, max_inclusive, value)
+            )
+
+    @classmethod
+    def validate_int_in_range(cls, value, min_inclusive, max_inclusive):
+        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):
+        if isinstance(value, str):
+            return value
+        try:
+            if isinstance(value, basestring):
+                return value
+        except NameError:  # means we're on Python 3
+            pass
+        raise TypeError("value must be a string, got %s" % type(value))
+
+
+class BaseFloatType(BaseSimpleType):
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        return float(str_value)
+
+    @classmethod
+    def convert_to_xml(cls, value):
+        return str(float(value))
+
+    @classmethod
+    def validate(cls, value):
+        if not isinstance(value, (int, float)):
+            raise TypeError("value must be a number, got %s" % type(value))
+
+
+class BaseIntType(BaseSimpleType):
+    @classmethod
+    def convert_from_percent_literal(cls, str_value):
+        int_str = str_value.replace("%", "")
+        return int(int_str)
+
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        return int(str_value)
+
+    @classmethod
+    def convert_to_xml(cls, value):
+        return str(value)
+
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int(value)
+
+
+class BaseStringType(BaseSimpleType):
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        return str_value
+
+    @classmethod
+    def convert_to_xml(cls, value):
+        return value
+
+    @classmethod
+    def validate(cls, value):
+        cls.validate_string(value)
+
+
+class BaseStringEnumerationType(BaseStringType):
+    @classmethod
+    def validate(cls, value):
+        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 regular expression this is supposed to meet but so far thinking
+    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):
+        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):
+        return {True: "1", False: "0"}[value]
+
+    @classmethod
+    def validate(cls, value):
+        if value not in (True, False):
+            raise TypeError(
+                "only True or False (and possibly None) may be assigned, got" " '%s'" % value
+            )
+
+
+class XsdDouble(BaseFloatType):
+    pass
+
+
+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.
+    """
+
+
+class XsdInt(BaseIntType):
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, -2147483648, 2147483647)
+
+
+class XsdLong(BaseIntType):
+    @classmethod
+    def validate(cls, value):
+        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.
+    """
+
+
+class XsdTokenEnumeration(BaseStringEnumerationType):
+    """
+    xsd:string with whitespace collapsing, e.g. multiple spaces reduced to
+    one, leading and trailing space stripped.
+    """
+
+
+class XsdUnsignedByte(BaseIntType):
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, 0, 255)
+
+
+class XsdUnsignedInt(BaseIntType):
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, 0, 4294967295)
+
+
+class XsdUnsignedShort(BaseIntType):
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, 0, 65535)
+
+
+class ST_Angle(XsdInt):
+    """
+    Valid values for `rot` attribute on `<a:xfrm>` element. 60000ths of
+    a degree rotation.
+    """
+
+    DEGREE_INCREMENTS = 60000
+    THREE_SIXTY = 360 * DEGREE_INCREMENTS
+
+    @classmethod
+    def convert_from_xml(cls, str_value: str) -> float:
+        rot = int(str_value) % cls.THREE_SIXTY
+        return float(rot) / cls.DEGREE_INCREMENTS
+
+    @classmethod
+    def convert_to_xml(cls, value):
+        """
+        Convert signed angle float like -42.42 to int 60000 per degree,
+        normalized to positive value.
+        """
+        # modulo normalizes negative and >360 degree values
+        rot = int(round(value * cls.DEGREE_INCREMENTS)) % cls.THREE_SIXTY
+        return str(rot)
+
+    @classmethod
+    def validate(cls, value):
+        BaseFloatType.validate(value)
+
+
+class ST_AxisUnit(XsdDouble):
+    """
+    Valid values for val attribute on c:majorUnit and others.
+    """
+
+    @classmethod
+    def validate(cls, value):
+        super(ST_AxisUnit, cls).validate(value)
+        if value <= 0.0:
+            raise ValueError("must be positive numeric value, got %s" % value)
+
+
+class ST_BarDir(XsdStringEnumeration):
+    """
+    Valid values for <c:barDir val="?"> attribute
+    """
+
+    BAR = "bar"
+    COL = "col"
+
+    _members = (BAR, COL)
+
+
+class ST_BubbleScale(BaseIntType):
+    """
+    String value is an integer in range 0-300, representing a percent,
+    optionally including a '%' suffix.
+    """
+
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        if "%" in str_value:
+            return cls.convert_from_percent_literal(str_value)
+        return super(ST_BubbleScale, cls).convert_from_xml(str_value)
+
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, 0, 300)
+
+
+class ST_ContentType(XsdString):
+    """
+    Has a pretty wicked regular expression it needs to match in the schema,
+    but figuring it's not worth the trouble or run time to identify
+    a programming error (as opposed to a user/runtime error).
+    """
+
+    pass
+
+
+class ST_Coordinate(BaseSimpleType):
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        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 convert_to_xml(cls, value):
+        return str(value)
+
+    @classmethod
+    def validate(cls, value):
+        ST_CoordinateUnqualified.validate(value)
+
+
+class ST_Coordinate32(BaseSimpleType):
+    """
+    xsd:union of ST_Coordinate32Unqualified, ST_UniversalMeasure
+    """
+
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        if "i" in str_value or "m" in str_value or "p" in str_value:
+            return ST_UniversalMeasure.convert_from_xml(str_value)
+        return ST_Coordinate32Unqualified.convert_from_xml(str_value)
+
+    @classmethod
+    def convert_to_xml(cls, value):
+        return ST_Coordinate32Unqualified.convert_to_xml(value)
+
+    @classmethod
+    def validate(cls, value):
+        ST_Coordinate32Unqualified.validate(value)
+
+
+class ST_Coordinate32Unqualified(XsdInt):
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        return Emu(int(str_value))
+
+
+class ST_CoordinateUnqualified(XsdLong):
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, -27273042329600, 27273042316900)
+
+
+class ST_Direction(XsdTokenEnumeration):
+    """Valid values for `<p:ph orient="...">` attribute."""
+
+    HORZ = "horz"
+    VERT = "vert"
+
+    _members = (HORZ, VERT)
+
+
+class ST_DrawingElementId(XsdUnsignedInt):
+    pass
+
+
+class ST_Extension(XsdString):
+    """
+    Has a regular expression it needs to match in the schema, but figuring
+    it's not worth the trouble or run time to identify a programming error
+    (as opposed to a user/runtime error).
+    """
+
+    pass
+
+
+class ST_GapAmount(BaseIntType):
+    """
+    String value is an integer in range 0-500, representing a percent,
+    optionally including a '%' suffix.
+    """
+
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        if "%" in str_value:
+            return cls.convert_from_percent_literal(str_value)
+        return super(ST_GapAmount, cls).convert_from_xml(str_value)
+
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, 0, 500)
+
+
+class ST_Grouping(XsdStringEnumeration):
+    """
+    Valid values for <c:grouping val=""> attribute. Overloaded for use as
+    ST_BarGrouping using same tag name.
+    """
+
+    CLUSTERED = "clustered"
+    PERCENT_STACKED = "percentStacked"
+    STACKED = "stacked"
+    STANDARD = "standard"
+
+    _members = (CLUSTERED, PERCENT_STACKED, STACKED, STANDARD)
+
+
+class ST_HexColorRGB(BaseStringType):
+    @classmethod
+    def convert_to_xml(cls, value):
+        """
+        Keep alpha characters all uppercase just for consistency.
+        """
+        return value.upper()
+
+    @classmethod
+    def validate(cls, value):
+        # must be string ---------------
+        str_value = cls.validate_string(value)
+
+        # must be 6 chars long----------
+        if len(str_value) != 6:
+            raise ValueError("RGB string must be six characters long, got '%s'" % str_value)
+
+        # must parse as hex int --------
+        try:
+            int(str_value, 16)
+        except ValueError:
+            raise ValueError("RGB string must be valid hex string, got '%s'" % str_value)
+
+
+class ST_LayoutMode(XsdStringEnumeration):
+    """
+    Valid values for `val` attribute on c:xMode and other elements of type
+    CT_LayoutMode.
+    """
+
+    EDGE = "edge"
+    FACTOR = "factor"
+
+    _members = (EDGE, FACTOR)
+
+
+class ST_LblOffset(XsdUnsignedShort):
+    """
+    Unsigned integer value between 0 and 1000 inclusive, with optional
+    percent character ('%') suffix.
+    """
+
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        if str_value.endswith("%"):
+            return cls.convert_from_percent_literal(str_value)
+        return int(str_value)
+
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, 0, 1000)
+
+
+class ST_LineWidth(XsdInt):
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        return Emu(int(str_value))
+
+    @classmethod
+    def validate(cls, value):
+        super(ST_LineWidth, cls).validate(value)
+        if value < 0 or value > 20116800:
+            raise ValueError(
+                "value must be in range 0-20116800 inclusive (0-1584 points)" ", got %d" % value
+            )
+
+
+class ST_MarkerSize(XsdUnsignedByte):
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, 2, 72)
+
+
+class ST_Orientation(XsdStringEnumeration):
+    """Valid values for `val` attribute on c:orientation (CT_Orientation)."""
+
+    MAX_MIN = "maxMin"
+    MIN_MAX = "minMax"
+
+    _members = (MAX_MIN, MIN_MAX)
+
+
+class ST_Overlap(BaseIntType):
+    """
+    String value is an integer in range -100..100, representing a percent,
+    optionally including a '%' suffix.
+    """
+
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        if "%" in str_value:
+            return cls.convert_from_percent_literal(str_value)
+        return super(ST_Overlap, cls).convert_from_xml(str_value)
+
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, -100, 100)
+
+
+class ST_Percentage(BaseIntType):
+    """Percentage value like 42000 or '42.0%'
+
+    Either an integer literal representing 1000ths of a percent
+    (e.g. "42000"), or a floating point literal with a '%' suffix
+    (e.g. "42.0%).
+    """
+
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        if "%" in str_value:
+            return cls._convert_from_percent_literal(str_value)
+        return int(str_value) / 100000.0
+
+    @classmethod
+    def convert_to_xml(cls, value):
+        return str(int(round(value * 100000.0)))
+
+    @classmethod
+    def validate(cls, value):
+        cls.validate_float_in_range(value, -21474.83648, 21474.83647)
+
+    @classmethod
+    def _convert_from_percent_literal(cls, str_value):
+        float_part = str_value[:-1]  # trim off '%' character
+        return float(float_part) / 100.0
+
+
+class ST_PlaceholderSize(XsdTokenEnumeration):
+    """
+    Valid values for <p:ph> sz (size) attribute
+    """
+
+    FULL = "full"
+    HALF = "half"
+    QUARTER = "quarter"
+
+    _members = (FULL, HALF, QUARTER)
+
+
+class ST_PositiveCoordinate(XsdLong):
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        int_value = super(ST_PositiveCoordinate, cls).convert_from_xml(str_value)
+        return Emu(int_value)
+
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, 0, 27273042316900)
+
+
+class ST_PositiveFixedAngle(ST_Angle):
+    """Valid values for `a:lin@ang`.
+
+    60000ths of a degree rotation, constained to positive angles less than
+    360 degrees.
+    """
+
+    @classmethod
+    def convert_to_xml(cls, degrees):
+        """Convert signed angle float like -427.42 to int 60000 per degree.
+
+        Value is normalized to a positive value less than 360 degrees.
+        """
+        if degrees < 0.0:
+            degrees %= -360
+            degrees += 360
+        elif degrees > 0.0:
+            degrees %= 360
+
+        return str(int(round(degrees * cls.DEGREE_INCREMENTS)))
+
+
+class ST_PositiveFixedPercentage(ST_Percentage):
+    """Percentage value between 0 and 100% like 42000 or '42.0%'
+
+    Either an integer literal representing 1000ths of a percent
+    (e.g. "42000"), or a floating point literal with a '%' suffix
+    (e.g. "42.0%). Value is constrained to range of 0% to 100%. The source
+    value is a float between 0.0 and 1.0.
+    """
+
+    @classmethod
+    def validate(cls, value):
+        cls.validate_float_in_range(value, 0.0, 1.0)
+
+
+class ST_RelationshipId(XsdString):
+    pass
+
+
+class ST_SlideId(XsdUnsignedInt):
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, 256, 2147483647)
+
+
+class ST_SlideSizeCoordinate(BaseIntType):
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        return Emu(str_value)
+
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int(value)
+        if value < 914400 or value > 51206400:
+            raise ValueError(
+                "value must be in range(914400, 51206400) (1-56 inches), got" " %d" % value
+            )
+
+
+class ST_Style(XsdUnsignedByte):
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, 1, 48)
+
+
+class ST_TargetMode(XsdString):
+    """
+    The valid values for the ``TargetMode`` attribute in a Relationship
+    element, either 'External' or 'Internal'.
+    """
+
+    @classmethod
+    def validate(cls, value):
+        cls.validate_string(value)
+        if value not in ("External", "Internal"):
+            raise ValueError("must be one of 'Internal' or 'External', got '%s'" % value)
+
+
+class ST_TextFontScalePercentOrPercentString(BaseFloatType):
+    """
+    Valid values for the `fontScale` attribute of ``<a:normAutofit>``.
+    Translates to a float value.
+    """
+
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        if str_value.endswith("%"):
+            return float(str_value[:-1])  # trim off '%' character
+        return int(str_value) / 1000.0
+
+    @classmethod
+    def convert_to_xml(cls, value):
+        return str(int(value * 1000.0))
+
+    @classmethod
+    def validate(cls, value):
+        BaseFloatType.validate(value)
+        if value < 1.0 or value > 100.0:
+            raise ValueError("value must be in range 1.0..100.0 (percent), got %s" % value)
+
+
+class ST_TextFontSize(BaseIntType):
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, 100, 400000)
+
+
+class ST_TextIndentLevelType(BaseIntType):
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, 0, 8)
+
+
+class ST_TextSpacingPercentOrPercentString(BaseFloatType):
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        if str_value.endswith("%"):
+            return cls._convert_from_percent_literal(str_value)
+        return int(str_value) / 100000.0
+
+    @classmethod
+    def _convert_from_percent_literal(cls, str_value):
+        float_part = str_value[:-1]  # trim off '%' character
+        percent_value = float(float_part)
+        lines_value = percent_value / 100.0
+        return lines_value
+
+    @classmethod
+    def convert_to_xml(cls, value):
+        """
+        1.75 -> '175000'
+        """
+        lines = value * 100000.0
+        return str(int(round(lines)))
+
+    @classmethod
+    def validate(cls, value):
+        cls.validate_float_in_range(value, 0.0, 132.0)
+
+
+class ST_TextSpacingPoint(BaseIntType):
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        """
+        Reads string integer centipoints, returns |Length| value.
+        """
+        return Centipoints(int(str_value))
+
+    @classmethod
+    def convert_to_xml(cls, value):
+        length = Emu(value)  # just to make sure
+        return str(length.centipoints)
+
+    @classmethod
+    def validate(cls, value):
+        cls.validate_int_in_range(value, 0, 20116800)
+
+
+class ST_TextTypeface(XsdString):
+    pass
+
+
+class ST_TextWrappingType(XsdTokenEnumeration):
+    """
+    Valid values for <a:bodyPr wrap=""> attribute
+    """
+
+    NONE = "none"
+    SQUARE = "square"
+
+    _members = (NONE, SQUARE)
+
+
+class ST_UniversalMeasure(BaseSimpleType):
+    @classmethod
+    def convert_from_xml(cls, str_value):
+        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]
+        emu_value = Emu(int(round(quantity * multiplier)))
+        return emu_value
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/slide.py b/.venv/lib/python3.12/site-packages/pptx/oxml/slide.py
new file mode 100644
index 00000000..37a9780f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/slide.py
@@ -0,0 +1,347 @@
+"""Slide-related custom element classes, including those for masters."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, cast
+
+from pptx.oxml import parse_from_template, parse_xml
+from pptx.oxml.dml.fill import CT_GradientFillProperties
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.simpletypes import XsdString
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    Choice,
+    OneAndOnlyOne,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrMore,
+    ZeroOrOne,
+    ZeroOrOneChoice,
+)
+
+if TYPE_CHECKING:
+    from pptx.oxml.shapes.groupshape import CT_GroupShape
+
+
+class _BaseSlideElement(BaseOxmlElement):
+    """Base class for the six slide types, providing common methods."""
+
+    cSld: CT_CommonSlideData
+
+    @property
+    def spTree(self) -> CT_GroupShape:
+        """Return required `p:cSld/p:spTree` grandchild."""
+        return self.cSld.spTree
+
+
+class CT_Background(BaseOxmlElement):
+    """`p:bg` element."""
+
+    _insert_bgPr: Callable[[CT_BackgroundProperties], None]
+
+    # ---these two are actually a choice, not a sequence, but simpler for
+    # ---present purposes this way.
+    _tag_seq = ("p:bgPr", "p:bgRef")
+    bgPr: CT_BackgroundProperties | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "p:bgPr", successors=()
+    )
+    bgRef = ZeroOrOne("p:bgRef", successors=())
+    del _tag_seq
+
+    def add_noFill_bgPr(self):
+        """Return a new `p:bgPr` element with noFill properties."""
+        xml = "<p:bgPr %s>\n" "  <a:noFill/>\n" "  <a:effectLst/>\n" "</p:bgPr>" % nsdecls("a", "p")
+        bgPr = cast(CT_BackgroundProperties, parse_xml(xml))
+        self._insert_bgPr(bgPr)
+        return bgPr
+
+
+class CT_BackgroundProperties(BaseOxmlElement):
+    """`p:bgPr` element."""
+
+    _tag_seq = (
+        "a:noFill",
+        "a:solidFill",
+        "a:gradFill",
+        "a:blipFill",
+        "a:pattFill",
+        "a:grpFill",
+        "a:effectLst",
+        "a:effectDag",
+        "a:extLst",
+    )
+    eg_fillProperties = ZeroOrOneChoice(
+        (
+            Choice("a:noFill"),
+            Choice("a:solidFill"),
+            Choice("a:gradFill"),
+            Choice("a:blipFill"),
+            Choice("a:pattFill"),
+            Choice("a:grpFill"),
+        ),
+        successors=_tag_seq[6:],
+    )
+    del _tag_seq
+
+    def _new_gradFill(self):
+        """Override default to add default gradient subtree."""
+        return CT_GradientFillProperties.new_gradFill()
+
+
+class CT_CommonSlideData(BaseOxmlElement):
+    """`p:cSld` element."""
+
+    _remove_bg: Callable[[], None]
+    get_or_add_bg: Callable[[], CT_Background]
+
+    _tag_seq = ("p:bg", "p:spTree", "p:custDataLst", "p:controls", "p:extLst")
+    bg: CT_Background | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "p:bg", successors=_tag_seq[1:]
+    )
+    spTree: CT_GroupShape = OneAndOnlyOne("p:spTree")  # pyright: ignore[reportAssignmentType]
+    del _tag_seq
+    name: str = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "name", XsdString, default=""
+    )
+
+    def get_or_add_bgPr(self) -> CT_BackgroundProperties:
+        """Return `p:bg/p:bgPr` grandchild.
+
+        If no such grandchild is present, any existing `p:bg` child is first removed and a new
+        default `p:bg` with noFill settings is added.
+        """
+        bg = self.bg
+        if bg is None or bg.bgPr is None:
+            bg = self._change_to_noFill_bg()
+        return cast(CT_BackgroundProperties, bg.bgPr)
+
+    def _change_to_noFill_bg(self) -> CT_Background:
+        """Establish a `p:bg` child with no-fill settings.
+
+        Any existing `p:bg` child is first removed.
+        """
+        self._remove_bg()
+        bg = self.get_or_add_bg()
+        bg.add_noFill_bgPr()
+        return bg
+
+
+class CT_NotesMaster(_BaseSlideElement):
+    """`p:notesMaster` element, root of a notes master part."""
+
+    _tag_seq = ("p:cSld", "p:clrMap", "p:hf", "p:notesStyle", "p:extLst")
+    cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld")  # pyright: ignore[reportAssignmentType]
+    del _tag_seq
+
+    @classmethod
+    def new_default(cls) -> CT_NotesMaster:
+        """Return a new `p:notesMaster` element based on the built-in default template."""
+        return cast(CT_NotesMaster, parse_from_template("notesMaster"))
+
+
+class CT_NotesSlide(_BaseSlideElement):
+    """`p:notes` element, root of a notes slide part."""
+
+    _tag_seq = ("p:cSld", "p:clrMapOvr", "p:extLst")
+    cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld")  # pyright: ignore[reportAssignmentType]
+    del _tag_seq
+
+    @classmethod
+    def new(cls) -> CT_NotesSlide:
+        """Return a new ``<p:notes>`` element based on the default template.
+
+        Note that the template does not include placeholders, which must be subsequently cloned
+        from the notes master.
+        """
+        return cast(CT_NotesSlide, parse_from_template("notes"))
+
+
+class CT_Slide(_BaseSlideElement):
+    """`p:sld` element, root element of a slide part (XML document)."""
+
+    _tag_seq = ("p:cSld", "p:clrMapOvr", "p:transition", "p:timing", "p:extLst")
+    cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld")  # pyright: ignore[reportAssignmentType]
+    clrMapOvr = ZeroOrOne("p:clrMapOvr", successors=_tag_seq[2:])
+    timing = ZeroOrOne("p:timing", successors=_tag_seq[4:])
+    del _tag_seq
+
+    @classmethod
+    def new(cls) -> CT_Slide:
+        """Return new `p:sld` element configured as base slide shape."""
+        return cast(CT_Slide, parse_xml(cls._sld_xml()))
+
+    @property
+    def bg(self):
+        """Return `p:bg` grandchild or None if not present."""
+        return self.cSld.bg
+
+    def get_or_add_childTnLst(self):
+        """Return parent element for a new `p:video` child element.
+
+        The `p:video` element causes play controls to appear under a video
+        shape (pic shape containing video). There can be more than one video
+        shape on a slide, which causes the precondition to vary. It needs to
+        handle the case when there is no `p:sld/p:timing` element and when
+        that element already exists. If the case isn't simple, it just nukes
+        what's there and adds a fresh one. This could theoretically remove
+        desired existing timing information, but there isn't any evidence
+        available to me one way or the other, so I've taken the simple
+        approach.
+        """
+        childTnLst = self._childTnLst
+        if childTnLst is None:
+            childTnLst = self._add_childTnLst()
+        return childTnLst
+
+    def _add_childTnLst(self):
+        """Add `./p:timing/p:tnLst/p:par/p:cTn/p:childTnLst` descendant.
+
+        Any existing `p:timing` child element is ruthlessly removed and
+        replaced.
+        """
+        self.remove(self.get_or_add_timing())
+        timing = parse_xml(self._childTnLst_timing_xml())
+        self._insert_timing(timing)
+        return timing.xpath("./p:tnLst/p:par/p:cTn/p:childTnLst")[0]
+
+    @property
+    def _childTnLst(self):
+        """Return `./p:timing/p:tnLst/p:par/p:cTn/p:childTnLst` descendant.
+
+        Return None if that element is not present.
+        """
+        childTnLsts = self.xpath("./p:timing/p:tnLst/p:par/p:cTn/p:childTnLst")
+        if not childTnLsts:
+            return None
+        return childTnLsts[0]
+
+    @staticmethod
+    def _childTnLst_timing_xml():
+        return (
+            "<p:timing %s>\n"
+            "  <p:tnLst>\n"
+            "    <p:par>\n"
+            '      <p:cTn id="1" dur="indefinite" restart="never" nodeType="'
+            'tmRoot">\n'
+            "        <p:childTnLst/>\n"
+            "      </p:cTn>\n"
+            "    </p:par>\n"
+            "  </p:tnLst>\n"
+            "</p:timing>" % nsdecls("p")
+        )
+
+    @staticmethod
+    def _sld_xml():
+        return (
+            "<p:sld %s>\n"
+            "  <p:cSld>\n"
+            "    <p:spTree>\n"
+            "      <p:nvGrpSpPr>\n"
+            '        <p:cNvPr id="1" name=""/>\n'
+            "        <p:cNvGrpSpPr/>\n"
+            "        <p:nvPr/>\n"
+            "      </p:nvGrpSpPr>\n"
+            "      <p:grpSpPr/>\n"
+            "    </p:spTree>\n"
+            "  </p:cSld>\n"
+            "  <p:clrMapOvr>\n"
+            "    <a:masterClrMapping/>\n"
+            "  </p:clrMapOvr>\n"
+            "</p:sld>" % nsdecls("a", "p", "r")
+        )
+
+
+class CT_SlideLayout(_BaseSlideElement):
+    """`p:sldLayout` element, root of a slide layout part."""
+
+    _tag_seq = ("p:cSld", "p:clrMapOvr", "p:transition", "p:timing", "p:hf", "p:extLst")
+    cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld")  # pyright: ignore[reportAssignmentType]
+    del _tag_seq
+
+
+class CT_SlideLayoutIdList(BaseOxmlElement):
+    """`p:sldLayoutIdLst` element, child of `p:sldMaster`.
+
+    Contains references to the slide layouts that inherit from the slide master.
+    """
+
+    sldLayoutId_lst: list[CT_SlideLayoutIdListEntry]
+
+    sldLayoutId = ZeroOrMore("p:sldLayoutId")
+
+
+class CT_SlideLayoutIdListEntry(BaseOxmlElement):
+    """`p:sldLayoutId` element, child of `p:sldLayoutIdLst`.
+
+    Contains a reference to a slide layout.
+    """
+
+    rId: str = RequiredAttribute("r:id", XsdString)  # pyright: ignore[reportAssignmentType]
+
+
+class CT_SlideMaster(_BaseSlideElement):
+    """`p:sldMaster` element, root of a slide master part."""
+
+    get_or_add_sldLayoutIdLst: Callable[[], CT_SlideLayoutIdList]
+
+    _tag_seq = (
+        "p:cSld",
+        "p:clrMap",
+        "p:sldLayoutIdLst",
+        "p:transition",
+        "p:timing",
+        "p:hf",
+        "p:txStyles",
+        "p:extLst",
+    )
+    cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld")  # pyright: ignore[reportAssignmentType]
+    sldLayoutIdLst: CT_SlideLayoutIdList = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "p:sldLayoutIdLst", successors=_tag_seq[3:]
+    )
+    del _tag_seq
+
+
+class CT_SlideTiming(BaseOxmlElement):
+    """`p:timing` element, specifying animations and timed behaviors."""
+
+    _tag_seq = ("p:tnLst", "p:bldLst", "p:extLst")
+    tnLst = ZeroOrOne("p:tnLst", successors=_tag_seq[1:])
+    del _tag_seq
+
+
+class CT_TimeNodeList(BaseOxmlElement):
+    """`p:tnLst` or `p:childTnList` element."""
+
+    def add_video(self, shape_id):
+        """Add a new `p:video` child element for movie having *shape_id*."""
+        video_xml = (
+            "<p:video %s>\n"
+            '  <p:cMediaNode vol="80000">\n'
+            '    <p:cTn id="%d" fill="hold" display="0">\n'
+            "      <p:stCondLst>\n"
+            '        <p:cond delay="indefinite"/>\n'
+            "      </p:stCondLst>\n"
+            "    </p:cTn>\n"
+            "    <p:tgtEl>\n"
+            '      <p:spTgt spid="%d"/>\n'
+            "    </p:tgtEl>\n"
+            "  </p:cMediaNode>\n"
+            "</p:video>\n" % (nsdecls("p"), self._next_cTn_id, shape_id)
+        )
+        video = parse_xml(video_xml)
+        self.append(video)
+
+    @property
+    def _next_cTn_id(self):
+        """Return the next available unique ID (int) for p:cTn element."""
+        cTn_id_strs = self.xpath("/p:sld/p:timing//p:cTn/@id")
+        ids = [int(id_str) for id_str in cTn_id_strs]
+        return max(ids) + 1
+
+
+class CT_TLMediaNodeVideo(BaseOxmlElement):
+    """`p:video` element, specifying video media details."""
+
+    _tag_seq = ("p:cMediaNode",)
+    cMediaNode = OneAndOnlyOne("p:cMediaNode")
+    del _tag_seq
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/table.py b/.venv/lib/python3.12/site-packages/pptx/oxml/table.py
new file mode 100644
index 00000000..cd3e9ebc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/table.py
@@ -0,0 +1,588 @@
+"""Custom element classes for table-related XML elements"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, Iterator, cast
+
+from pptx.enum.text import MSO_VERTICAL_ANCHOR
+from pptx.oxml import parse_xml
+from pptx.oxml.dml.fill import CT_GradientFillProperties
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.simpletypes import ST_Coordinate, ST_Coordinate32, XsdBoolean, XsdInt
+from pptx.oxml.text import CT_TextBody
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    Choice,
+    OneAndOnlyOne,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrMore,
+    ZeroOrOne,
+    ZeroOrOneChoice,
+)
+from pptx.util import Emu, lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.util import Length
+
+
+class CT_Table(BaseOxmlElement):
+    """`a:tbl` custom element class"""
+
+    get_or_add_tblPr: Callable[[], CT_TableProperties]
+    tr_lst: list[CT_TableRow]
+    _add_tr: Callable[..., CT_TableRow]
+
+    _tag_seq = ("a:tblPr", "a:tblGrid", "a:tr")
+    tblPr: CT_TableProperties | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:tblPr", successors=_tag_seq[1:]
+    )
+    tblGrid: CT_TableGrid = OneAndOnlyOne("a:tblGrid")  # pyright: ignore[reportAssignmentType]
+    tr = ZeroOrMore("a:tr", successors=_tag_seq[3:])
+    del _tag_seq
+
+    def add_tr(self, height: Length) -> CT_TableRow:
+        """Return a newly created `a:tr` child element having its `h` attribute set to `height`."""
+        return self._add_tr(h=height)
+
+    @property
+    def bandCol(self) -> bool:
+        return self._get_boolean_property("bandCol")
+
+    @bandCol.setter
+    def bandCol(self, value: bool):
+        self._set_boolean_property("bandCol", value)
+
+    @property
+    def bandRow(self) -> bool:
+        return self._get_boolean_property("bandRow")
+
+    @bandRow.setter
+    def bandRow(self, value: bool):
+        self._set_boolean_property("bandRow", value)
+
+    @property
+    def firstCol(self) -> bool:
+        return self._get_boolean_property("firstCol")
+
+    @firstCol.setter
+    def firstCol(self, value: bool):
+        self._set_boolean_property("firstCol", value)
+
+    @property
+    def firstRow(self) -> bool:
+        return self._get_boolean_property("firstRow")
+
+    @firstRow.setter
+    def firstRow(self, value: bool):
+        self._set_boolean_property("firstRow", value)
+
+    def iter_tcs(self) -> Iterator[CT_TableCell]:
+        """Generate each `a:tc` element in this tbl.
+
+        `a:tc` elements are generated left-to-right, top-to-bottom.
+        """
+        return (tc for tr in self.tr_lst for tc in tr.tc_lst)
+
+    @property
+    def lastCol(self) -> bool:
+        return self._get_boolean_property("lastCol")
+
+    @lastCol.setter
+    def lastCol(self, value: bool):
+        self._set_boolean_property("lastCol", value)
+
+    @property
+    def lastRow(self) -> bool:
+        return self._get_boolean_property("lastRow")
+
+    @lastRow.setter
+    def lastRow(self, value: bool):
+        self._set_boolean_property("lastRow", value)
+
+    @classmethod
+    def new_tbl(
+        cls, rows: int, cols: int, width: int, height: int, tableStyleId: str | None = None
+    ) -> CT_Table:
+        """Return a new `p:tbl` element tree."""
+        # working hypothesis is this is the default table style GUID
+        if tableStyleId is None:
+            tableStyleId = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"
+
+        xml = cls._tbl_tmpl() % (tableStyleId)
+        tbl = cast(CT_Table, parse_xml(xml))
+
+        # add specified number of rows and columns
+        rowheight = height // rows
+        colwidth = width // cols
+
+        for col in range(cols):
+            # adjust width of last col to absorb any div error
+            if col == cols - 1:
+                colwidth = width - ((cols - 1) * colwidth)
+            tbl.tblGrid.add_gridCol(width=Emu(colwidth))
+
+        for row in range(rows):
+            # adjust height of last row to absorb any div error
+            if row == rows - 1:
+                rowheight = height - ((rows - 1) * rowheight)
+            tr = tbl.add_tr(height=Emu(rowheight))
+            for col in range(cols):
+                tr.add_tc()
+
+        return tbl
+
+    def tc(self, row_idx: int, col_idx: int) -> CT_TableCell:
+        """Return `a:tc` element at `row_idx`, `col_idx`."""
+        return self.tr_lst[row_idx].tc_lst[col_idx]
+
+    def _get_boolean_property(self, propname: str) -> bool:
+        """Generalized getter for the boolean properties on the `a:tblPr` child element.
+
+        Defaults to False if `propname` attribute is missing or `a:tblPr` element itself is not
+        present.
+        """
+        tblPr = self.tblPr
+        if tblPr is None:
+            return False
+        propval = getattr(tblPr, propname)
+        return {True: True, False: False, None: False}[propval]
+
+    def _set_boolean_property(self, propname: str, value: bool) -> None:
+        """Generalized setter for boolean properties on the `a:tblPr` child element.
+
+        Sets `propname` attribute appropriately based on `value`. If `value` is True, the
+        attribute is set to "1"; a tblPr child element is added if necessary. If `value` is False,
+        the `propname` attribute is removed if present, allowing its default value of False to be
+        its effective value.
+        """
+        if value not in (True, False):
+            raise ValueError("assigned value must be either True or False, got %s" % value)
+        tblPr = self.get_or_add_tblPr()
+        setattr(tblPr, propname, value)
+
+    @classmethod
+    def _tbl_tmpl(cls):
+        return (
+            "<a:tbl %s>\n"
+            '  <a:tblPr firstRow="1" bandRow="1">\n'
+            "    <a:tableStyleId>%s</a:tableStyleId>\n"
+            "  </a:tblPr>\n"
+            "  <a:tblGrid/>\n"
+            "</a:tbl>" % (nsdecls("a"), "%s")
+        )
+
+
+class CT_TableCell(BaseOxmlElement):
+    """`a:tc` custom element class"""
+
+    get_or_add_tcPr: Callable[[], CT_TableCellProperties]
+    get_or_add_txBody: Callable[[], CT_TextBody]
+
+    _tag_seq = ("a:txBody", "a:tcPr", "a:extLst")
+    txBody: CT_TextBody | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:txBody", successors=_tag_seq[1:]
+    )
+    tcPr: CT_TableCellProperties | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:tcPr", successors=_tag_seq[2:]
+    )
+    del _tag_seq
+
+    gridSpan: int = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "gridSpan", XsdInt, default=1
+    )
+    rowSpan: int = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "rowSpan", XsdInt, default=1
+    )
+    hMerge: bool = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "hMerge", XsdBoolean, default=False
+    )
+    vMerge: bool = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "vMerge", XsdBoolean, default=False
+    )
+
+    @property
+    def anchor(self) -> MSO_VERTICAL_ANCHOR | None:
+        """String held in `anchor` attribute of `a:tcPr` child element of this `a:tc` element."""
+        if self.tcPr is None:
+            return None
+        return self.tcPr.anchor
+
+    @anchor.setter
+    def anchor(self, anchor_enum_idx: MSO_VERTICAL_ANCHOR | None):
+        """Set value of anchor attribute on `a:tcPr` child element."""
+        if anchor_enum_idx is None and self.tcPr is None:
+            return
+        tcPr = self.get_or_add_tcPr()
+        tcPr.anchor = anchor_enum_idx
+
+    def append_ps_from(self, spanned_tc: CT_TableCell):
+        """Append `a:p` elements taken from `spanned_tc`.
+
+        Any non-empty paragraph elements in `spanned_tc` are removed and appended to the
+        text-frame of this cell. If `spanned_tc` is left with no content after this process, a
+        single empty `a:p` element is added to ensure the cell is compliant with the spec.
+        """
+        source_txBody = spanned_tc.get_or_add_txBody()
+        target_txBody = self.get_or_add_txBody()
+
+        # ---if source is empty, there's nothing to do---
+        if source_txBody.is_empty:
+            return
+
+        # ---a single empty paragraph in target is overwritten---
+        if target_txBody.is_empty:
+            target_txBody.clear_content()
+
+        for p in source_txBody.p_lst:
+            target_txBody.append(p)
+
+        # ---neither source nor target can be left without ps---
+        source_txBody.unclear_content()
+        target_txBody.unclear_content()
+
+    @property
+    def col_idx(self) -> int:
+        """Offset of this cell's column in its table."""
+        # ---tc elements come before any others in `a:tr` element---
+        return cast(CT_TableRow, self.getparent()).index(self)
+
+    @property
+    def is_merge_origin(self) -> bool:
+        """True if cell is top-left in merged cell range."""
+        if self.gridSpan > 1 and not self.vMerge:
+            return True
+        return self.rowSpan > 1 and not self.hMerge
+
+    @property
+    def is_spanned(self) -> bool:
+        """True if cell is in merged cell range but not merge origin cell."""
+        return self.hMerge or self.vMerge
+
+    @property
+    def marT(self) -> Length:
+        """Top margin for this cell.
+
+        This value is stored in the `marT` attribute of the `a:tcPr` child element of this `a:tc`.
+
+        Read/write. If the attribute is not present, the default value `45720` (0.05 inches) is
+        returned for top and bottom; `91440` (0.10 inches) is the default for left and right.
+        Assigning |None| to any `marX` property clears that attribute from the element,
+        effectively setting it to the default value.
+        """
+        return self._get_marX("marT", Emu(45720))
+
+    @marT.setter
+    def marT(self, value: Length | None):
+        self._set_marX("marT", value)
+
+    @property
+    def marR(self) -> Length:
+        """Right margin value represented in `marR` attribute."""
+        return self._get_marX("marR", Emu(91440))
+
+    @marR.setter
+    def marR(self, value: Length | None):
+        self._set_marX("marR", value)
+
+    @property
+    def marB(self) -> Length:
+        """Bottom margin value represented in `marB` attribute."""
+        return self._get_marX("marB", Emu(45720))
+
+    @marB.setter
+    def marB(self, value: Length | None):
+        self._set_marX("marB", value)
+
+    @property
+    def marL(self) -> Length:
+        """Left margin value represented in `marL` attribute."""
+        return self._get_marX("marL", Emu(91440))
+
+    @marL.setter
+    def marL(self, value: Length | None):
+        self._set_marX("marL", value)
+
+    @classmethod
+    def new(cls) -> CT_TableCell:
+        """Return a new `a:tc` element subtree."""
+        return cast(
+            CT_TableCell,
+            parse_xml(
+                f"<a:tc {nsdecls('a')}>\n"
+                f"  <a:txBody>\n"
+                f"    <a:bodyPr/>\n"
+                f"    <a:lstStyle/>\n"
+                f"    <a:p/>\n"
+                f"  </a:txBody>\n"
+                f"  <a:tcPr/>\n"
+                f"</a:tc>"
+            ),
+        )
+
+    @property
+    def row_idx(self) -> int:
+        """Offset of this cell's row in its table."""
+        return cast(CT_TableRow, self.getparent()).row_idx
+
+    @property
+    def tbl(self) -> CT_Table:
+        """Table element this cell belongs to."""
+        return cast(CT_Table, self.xpath("ancestor::a:tbl")[0])
+
+    @property
+    def text(self) -> str:  # pyright: ignore[reportIncompatibleMethodOverride]
+        """str text contained in cell"""
+        # ---note this shadows lxml _Element.text---
+        txBody = self.txBody
+        if txBody is None:
+            return ""
+        return "\n".join([p.text for p in txBody.p_lst])
+
+    def _get_marX(self, attr_name: str, default: Length) -> Length:
+        """Generalized method to get margin values."""
+        if self.tcPr is None:
+            return Emu(default)
+        return Emu(int(self.tcPr.get(attr_name, default)))
+
+    def _new_txBody(self) -> CT_TextBody:
+        return CT_TextBody.new_a_txBody()
+
+    def _set_marX(self, marX: str, value: Length | None) -> None:
+        """Set value of marX attribute on `a:tcPr` child element.
+
+        If `marX` is |None|, the marX attribute is removed. `marX` is a string, one of `('marL',
+        'marR', 'marT', 'marB')`.
+        """
+        if value is None and self.tcPr is None:
+            return
+        tcPr = self.get_or_add_tcPr()
+        setattr(tcPr, marX, value)
+
+
+class CT_TableCellProperties(BaseOxmlElement):
+    """`a:tcPr` custom element class"""
+
+    eg_fillProperties = ZeroOrOneChoice(
+        (
+            Choice("a:noFill"),
+            Choice("a:solidFill"),
+            Choice("a:gradFill"),
+            Choice("a:blipFill"),
+            Choice("a:pattFill"),
+            Choice("a:grpFill"),
+        ),
+        successors=("a:headers", "a:extLst"),
+    )
+    anchor: MSO_VERTICAL_ANCHOR | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "anchor", MSO_VERTICAL_ANCHOR
+    )
+    marL: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "marL", ST_Coordinate32
+    )
+    marR: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "marR", ST_Coordinate32
+    )
+    marT: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "marT", ST_Coordinate32
+    )
+    marB: Length | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "marB", ST_Coordinate32
+    )
+
+    def _new_gradFill(self):
+        return CT_GradientFillProperties.new_gradFill()
+
+
+class CT_TableCol(BaseOxmlElement):
+    """`a:gridCol` custom element class."""
+
+    w: Length = RequiredAttribute("w", ST_Coordinate)  # pyright: ignore[reportAssignmentType]
+
+
+class CT_TableGrid(BaseOxmlElement):
+    """`a:tblGrid` custom element class."""
+
+    gridCol_lst: list[CT_TableCol]
+    _add_gridCol: Callable[..., CT_TableCol]
+
+    gridCol = ZeroOrMore("a:gridCol")
+
+    def add_gridCol(self, width: Length) -> CT_TableCol:
+        """A newly appended `a:gridCol` child element having its `w` attribute set to `width`."""
+        return self._add_gridCol(w=width)
+
+
+class CT_TableProperties(BaseOxmlElement):
+    """`a:tblPr` custom element class."""
+
+    bandRow = OptionalAttribute("bandRow", XsdBoolean, default=False)
+    bandCol = OptionalAttribute("bandCol", XsdBoolean, default=False)
+    firstRow = OptionalAttribute("firstRow", XsdBoolean, default=False)
+    firstCol = OptionalAttribute("firstCol", XsdBoolean, default=False)
+    lastRow = OptionalAttribute("lastRow", XsdBoolean, default=False)
+    lastCol = OptionalAttribute("lastCol", XsdBoolean, default=False)
+
+
+class CT_TableRow(BaseOxmlElement):
+    """`a:tr` custom element class."""
+
+    tc_lst: list[CT_TableCell]
+    _add_tc: Callable[[], CT_TableCell]
+
+    tc = ZeroOrMore("a:tc", successors=("a:extLst",))
+    h: Length = RequiredAttribute("h", ST_Coordinate)  # pyright: ignore[reportAssignmentType]
+
+    def add_tc(self) -> CT_TableCell:
+        """A newly added minimal valid `a:tc` child element."""
+        return self._add_tc()
+
+    @property
+    def row_idx(self) -> int:
+        """Offset of this row in its table."""
+        return cast(CT_Table, self.getparent()).tr_lst.index(self)
+
+    def _new_tc(self):
+        return CT_TableCell.new()
+
+
+class TcRange(object):
+    """A 2D block of `a:tc` cell elements in a table.
+
+    This object assumes the structure of the underlying table does not change during its lifetime.
+    Structural changes in this context would be insertion or removal of rows or columns.
+
+    The client is expected to create, use, and then abandon an instance in the context of a single
+    user operation that is known to have no structural side-effects of this type.
+    """
+
+    def __init__(self, tc: CT_TableCell, other_tc: CT_TableCell):
+        self._tc = tc
+        self._other_tc = other_tc
+
+    @classmethod
+    def from_merge_origin(cls, tc: CT_TableCell):
+        """Return instance created from merge-origin tc element."""
+        other_tc = tc.tbl.tc(
+            tc.row_idx + tc.rowSpan - 1,  # ---other_row_idx
+            tc.col_idx + tc.gridSpan - 1,  # ---other_col_idx
+        )
+        return cls(tc, other_tc)
+
+    @lazyproperty
+    def contains_merged_cell(self) -> bool:
+        """True if one or more cells in range are part of a merged cell."""
+        for tc in self.iter_tcs():
+            if tc.gridSpan > 1:
+                return True
+            if tc.rowSpan > 1:
+                return True
+            if tc.hMerge:
+                return True
+            if tc.vMerge:
+                return True
+        return False
+
+    @lazyproperty
+    def dimensions(self) -> tuple[int, int]:
+        """(row_count, col_count) pair describing size of range."""
+        _, _, width, height = self._extents
+        return height, width
+
+    @lazyproperty
+    def in_same_table(self):
+        """True if both cells provided to constructor are in same table."""
+        if self._tc.tbl is self._other_tc.tbl:
+            return True
+        return False
+
+    def iter_except_left_col_tcs(self):
+        """Generate each `a:tc` element not in leftmost column of range."""
+        for tr in self._tbl.tr_lst[self._top : self._bottom]:
+            for tc in tr.tc_lst[self._left + 1 : self._right]:
+                yield tc
+
+    def iter_except_top_row_tcs(self):
+        """Generate each `a:tc` element in non-first rows of range."""
+        for tr in self._tbl.tr_lst[self._top + 1 : self._bottom]:
+            for tc in tr.tc_lst[self._left : self._right]:
+                yield tc
+
+    def iter_left_col_tcs(self):
+        """Generate each `a:tc` element in leftmost column of range."""
+        col_idx = self._left
+        for tr in self._tbl.tr_lst[self._top : self._bottom]:
+            yield tr.tc_lst[col_idx]
+
+    def iter_tcs(self):
+        """Generate each `a:tc` element in this range.
+
+        Cell elements are generated left-to-right, top-to-bottom.
+        """
+        return (
+            tc
+            for tr in self._tbl.tr_lst[self._top : self._bottom]
+            for tc in tr.tc_lst[self._left : self._right]
+        )
+
+    def iter_top_row_tcs(self):
+        """Generate each `a:tc` element in topmost row of range."""
+        tr = self._tbl.tr_lst[self._top]
+        for tc in tr.tc_lst[self._left : self._right]:
+            yield tc
+
+    def move_content_to_origin(self):
+        """Move all paragraphs in range to origin cell."""
+        tcs = list(self.iter_tcs())
+        origin_tc = tcs[0]
+        for spanned_tc in tcs[1:]:
+            origin_tc.append_ps_from(spanned_tc)
+
+    @lazyproperty
+    def _bottom(self):
+        """Index of row following last row of range"""
+        _, top, _, height = self._extents
+        return top + height
+
+    @lazyproperty
+    def _extents(self) -> tuple[int, int, int, int]:
+        """A (left, top, width, height) tuple describing range extents.
+
+        Note this is normalized to accommodate the various orderings of the corner cells provided
+        on construction, which may be in any of four configurations such as (top-left,
+        bottom-right), (bottom-left, top-right), etc.
+        """
+
+        def start_and_size(idx: int, other_idx: int) -> tuple[int, int]:
+            """Return beginning and length of range based on two indexes."""
+            return min(idx, other_idx), abs(idx - other_idx) + 1
+
+        tc, other_tc = self._tc, self._other_tc
+
+        left, width = start_and_size(tc.col_idx, other_tc.col_idx)
+        top, height = start_and_size(tc.row_idx, other_tc.row_idx)
+
+        return left, top, width, height
+
+    @lazyproperty
+    def _left(self):
+        """Index of leftmost column in range."""
+        left, _, _, _ = self._extents
+        return left
+
+    @lazyproperty
+    def _right(self):
+        """Index of column following the last column in range."""
+        left, _, width, _ = self._extents
+        return left + width
+
+    @lazyproperty
+    def _tbl(self):
+        """`a:tbl` element containing this cell range."""
+        return self._tc.tbl
+
+    @lazyproperty
+    def _top(self):
+        """Index of topmost row in range."""
+        _, top, _, _ = self._extents
+        return top
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/text.py b/.venv/lib/python3.12/site-packages/pptx/oxml/text.py
new file mode 100644
index 00000000..0f9ecc15
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/text.py
@@ -0,0 +1,618 @@
+"""Custom element classes for text-related XML elements"""
+
+from __future__ import annotations
+
+import re
+from typing import TYPE_CHECKING, Callable, cast
+
+from pptx.enum.lang import MSO_LANGUAGE_ID
+from pptx.enum.text import (
+    MSO_AUTO_SIZE,
+    MSO_TEXT_UNDERLINE_TYPE,
+    MSO_VERTICAL_ANCHOR,
+    PP_PARAGRAPH_ALIGNMENT,
+)
+from pptx.exc import InvalidXmlError
+from pptx.oxml import parse_xml
+from pptx.oxml.dml.fill import CT_GradientFillProperties
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.simpletypes import (
+    ST_Coordinate32,
+    ST_TextFontScalePercentOrPercentString,
+    ST_TextFontSize,
+    ST_TextIndentLevelType,
+    ST_TextSpacingPercentOrPercentString,
+    ST_TextSpacingPoint,
+    ST_TextTypeface,
+    ST_TextWrappingType,
+    XsdBoolean,
+)
+from pptx.oxml.xmlchemy import (
+    BaseOxmlElement,
+    Choice,
+    OneAndOnlyOne,
+    OneOrMore,
+    OptionalAttribute,
+    RequiredAttribute,
+    ZeroOrMore,
+    ZeroOrOne,
+    ZeroOrOneChoice,
+)
+from pptx.util import Emu, Length
+
+if TYPE_CHECKING:
+    from pptx.oxml.action import CT_Hyperlink
+
+
+class CT_RegularTextRun(BaseOxmlElement):
+    """`a:r` custom element class"""
+
+    get_or_add_rPr: Callable[[], CT_TextCharacterProperties]
+
+    rPr: CT_TextCharacterProperties | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:rPr", successors=("a:t",)
+    )
+    t: BaseOxmlElement = OneAndOnlyOne("a:t")  # pyright: ignore[reportAssignmentType]
+
+    @property
+    def text(self) -> str:
+        """All text of (required) `a:t` child."""
+        text = self.t.text
+        # -- t.text is None when t element is empty, e.g. '<a:t/>' --
+        return text or ""
+
+    @text.setter
+    def text(self, value: str):  # pyright: ignore[reportIncompatibleMethodOverride]
+        self.t.text = self._escape_ctrl_chars(value)
+
+    @staticmethod
+    def _escape_ctrl_chars(s: str) -> str:
+        """Return str after replacing each control character with a plain-text escape.
+
+        For example, a BEL character (x07) would appear as "_x0007_". Horizontal-tab
+        (x09) and line-feed (x0A) are not escaped. All other characters in the range
+        x00-x1F are escaped.
+        """
+        return re.sub(r"([\x00-\x08\x0B-\x1F])", lambda match: "_x%04X_" % ord(match.group(1)), s)
+
+
+class CT_TextBody(BaseOxmlElement):
+    """`p:txBody` custom element class.
+
+    Also used for `c:txPr` in charts and perhaps other elements.
+    """
+
+    add_p: Callable[[], CT_TextParagraph]
+    p_lst: list[CT_TextParagraph]
+
+    bodyPr: CT_TextBodyProperties = OneAndOnlyOne(  # pyright: ignore[reportAssignmentType]
+        "a:bodyPr"
+    )
+    p: CT_TextParagraph = OneOrMore("a:p")  # pyright: ignore[reportAssignmentType]
+
+    def clear_content(self):
+        """Remove all `a:p` children, but leave any others.
+
+        cf. lxml `_Element.clear()` method which removes all children.
+        """
+        for p in self.p_lst:
+            self.remove(p)
+
+    @property
+    def defRPr(self) -> CT_TextCharacterProperties:
+        """`a:defRPr` element of required first `p` child, added with its ancestors if not present.
+
+        Used when element is a ``c:txPr`` in a chart and the `p` element is used only to specify
+        formatting, not content.
+        """
+        p = self.p_lst[0]
+        pPr = p.get_or_add_pPr()
+        defRPr = pPr.get_or_add_defRPr()
+        return defRPr
+
+    @property
+    def is_empty(self) -> bool:
+        """True if only a single empty `a:p` element is present."""
+        ps = self.p_lst
+        if len(ps) > 1:
+            return False
+
+        if not ps:
+            raise InvalidXmlError("p:txBody must have at least one a:p")
+
+        if ps[0].text != "":
+            return False
+        return True
+
+    @classmethod
+    def new(cls):
+        """Return a new `p:txBody` element tree."""
+        xml = cls._txBody_tmpl()
+        txBody = parse_xml(xml)
+        return txBody
+
+    @classmethod
+    def new_a_txBody(cls) -> CT_TextBody:
+        """Return a new `a:txBody` element tree.
+
+        Suitable for use in a table cell and possibly other situations.
+        """
+        xml = cls._a_txBody_tmpl()
+        txBody = cast(CT_TextBody, parse_xml(xml))
+        return txBody
+
+    @classmethod
+    def new_p_txBody(cls):
+        """Return a new `p:txBody` element tree, suitable for use in an `p:sp` element."""
+        xml = cls._p_txBody_tmpl()
+        return parse_xml(xml)
+
+    @classmethod
+    def new_txPr(cls):
+        """Return a `c:txPr` element tree.
+
+        Suitable for use in a chart object like data labels or tick labels.
+        """
+        xml = (
+            "<c:txPr %s>\n"
+            "  <a:bodyPr/>\n"
+            "  <a:lstStyle/>\n"
+            "  <a:p>\n"
+            "    <a:pPr>\n"
+            "      <a:defRPr/>\n"
+            "    </a:pPr>\n"
+            "  </a:p>\n"
+            "</c:txPr>\n"
+        ) % nsdecls("c", "a")
+        txPr = parse_xml(xml)
+        return txPr
+
+    def unclear_content(self):
+        """Ensure p:txBody has at least one a:p child.
+
+        Intuitively, reverse a ".clear_content()" operation to minimum conformance with spec
+        (single empty paragraph).
+        """
+        if len(self.p_lst) > 0:
+            return
+        self.add_p()
+
+    @classmethod
+    def _a_txBody_tmpl(cls):
+        return "<a:txBody %s>\n" "  <a:bodyPr/>\n" "  <a:p/>\n" "</a:txBody>\n" % (nsdecls("a"))
+
+    @classmethod
+    def _p_txBody_tmpl(cls):
+        return (
+            "<p:txBody %s>\n" "  <a:bodyPr/>\n" "  <a:p/>\n" "</p:txBody>\n" % (nsdecls("p", "a"))
+        )
+
+    @classmethod
+    def _txBody_tmpl(cls):
+        return (
+            "<p:txBody %s>\n"
+            "  <a:bodyPr/>\n"
+            "  <a:lstStyle/>\n"
+            "  <a:p/>\n"
+            "</p:txBody>\n" % (nsdecls("a", "p"))
+        )
+
+
+class CT_TextBodyProperties(BaseOxmlElement):
+    """`a:bodyPr` custom element class."""
+
+    _add_noAutofit: Callable[[], BaseOxmlElement]
+    _add_normAutofit: Callable[[], CT_TextNormalAutofit]
+    _add_spAutoFit: Callable[[], BaseOxmlElement]
+    _remove_eg_textAutoFit: Callable[[], None]
+
+    noAutofit: BaseOxmlElement | None
+    normAutofit: CT_TextNormalAutofit | None
+    spAutoFit: BaseOxmlElement | None
+
+    eg_textAutoFit = ZeroOrOneChoice(
+        (Choice("a:noAutofit"), Choice("a:normAutofit"), Choice("a:spAutoFit")),
+        successors=("a:scene3d", "a:sp3d", "a:flatTx", "a:extLst"),
+    )
+    lIns: Length = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "lIns", ST_Coordinate32, default=Emu(91440)
+    )
+    tIns: Length = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "tIns", ST_Coordinate32, default=Emu(45720)
+    )
+    rIns: Length = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "rIns", ST_Coordinate32, default=Emu(91440)
+    )
+    bIns: Length = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "bIns", ST_Coordinate32, default=Emu(45720)
+    )
+    anchor: MSO_VERTICAL_ANCHOR | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "anchor", MSO_VERTICAL_ANCHOR
+    )
+    wrap: str | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "wrap", ST_TextWrappingType
+    )
+
+    @property
+    def autofit(self):
+        """The autofit setting for the text frame, a member of the `MSO_AUTO_SIZE` enumeration."""
+        if self.noAutofit is not None:
+            return MSO_AUTO_SIZE.NONE
+        if self.normAutofit is not None:
+            return MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
+        if self.spAutoFit is not None:
+            return MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT
+        return None
+
+    @autofit.setter
+    def autofit(self, value: MSO_AUTO_SIZE | None):
+        if value is not None and value not in MSO_AUTO_SIZE:
+            raise ValueError(
+                f"only None or a member of the MSO_AUTO_SIZE enumeration can be assigned to"
+                f" CT_TextBodyProperties.autofit, got {value}"
+            )
+        self._remove_eg_textAutoFit()
+        if value == MSO_AUTO_SIZE.NONE:
+            self._add_noAutofit()
+        elif value == MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE:
+            self._add_normAutofit()
+        elif value == MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT:
+            self._add_spAutoFit()
+
+
+class CT_TextCharacterProperties(BaseOxmlElement):
+    """Custom element class for `a:rPr`, `a:defRPr`, and `a:endParaRPr`.
+
+    'rPr' is short for 'run properties', and it corresponds to the |Font| proxy class.
+    """
+
+    get_or_add_hlinkClick: Callable[[], CT_Hyperlink]
+    get_or_add_latin: Callable[[], CT_TextFont]
+    _remove_latin: Callable[[], None]
+    _remove_hlinkClick: Callable[[], None]
+
+    eg_fillProperties = ZeroOrOneChoice(
+        (
+            Choice("a:noFill"),
+            Choice("a:solidFill"),
+            Choice("a:gradFill"),
+            Choice("a:blipFill"),
+            Choice("a:pattFill"),
+            Choice("a:grpFill"),
+        ),
+        successors=(
+            "a:effectLst",
+            "a:effectDag",
+            "a:highlight",
+            "a:uLnTx",
+            "a:uLn",
+            "a:uFillTx",
+            "a:uFill",
+            "a:latin",
+            "a:ea",
+            "a:cs",
+            "a:sym",
+            "a:hlinkClick",
+            "a:hlinkMouseOver",
+            "a:rtl",
+            "a:extLst",
+        ),
+    )
+    latin: CT_TextFont | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:latin",
+        successors=(
+            "a:ea",
+            "a:cs",
+            "a:sym",
+            "a:hlinkClick",
+            "a:hlinkMouseOver",
+            "a:rtl",
+            "a:extLst",
+        ),
+    )
+    hlinkClick: CT_Hyperlink | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:hlinkClick", successors=("a:hlinkMouseOver", "a:rtl", "a:extLst")
+    )
+
+    lang: MSO_LANGUAGE_ID | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "lang", MSO_LANGUAGE_ID
+    )
+    sz: int | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "sz", ST_TextFontSize
+    )
+    b: bool | None = OptionalAttribute("b", XsdBoolean)  # pyright: ignore[reportAssignmentType]
+    i: bool | None = OptionalAttribute("i", XsdBoolean)  # pyright: ignore[reportAssignmentType]
+    u: MSO_TEXT_UNDERLINE_TYPE | None = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "u", MSO_TEXT_UNDERLINE_TYPE
+    )
+
+    def _new_gradFill(self):
+        return CT_GradientFillProperties.new_gradFill()
+
+    def add_hlinkClick(self, rId: str) -> CT_Hyperlink:
+        """Add an `a:hlinkClick` child element with r:id attribute set to `rId`."""
+        hlinkClick = self.get_or_add_hlinkClick()
+        hlinkClick.rId = rId
+        return hlinkClick
+
+
+class CT_TextField(BaseOxmlElement):
+    """`a:fld` field element, for either a slide number or date field."""
+
+    get_or_add_rPr: Callable[[], CT_TextCharacterProperties]
+
+    rPr: CT_TextCharacterProperties | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:rPr", successors=("a:pPr", "a:t")
+    )
+    t: BaseOxmlElement | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:t", successors=()
+    )
+
+    @property
+    def text(self) -> str:  # pyright: ignore[reportIncompatibleMethodOverride]
+        """The text of the `a:t` child element."""
+        t = self.t
+        if t is None:
+            return ""
+        return t.text or ""
+
+
+class CT_TextFont(BaseOxmlElement):
+    """Custom element class for `a:latin`, `a:ea`, `a:cs`, and `a:sym`.
+
+    These occur as child elements of CT_TextCharacterProperties, e.g. `a:rPr`.
+    """
+
+    typeface: str = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "typeface", ST_TextTypeface
+    )
+
+
+class CT_TextLineBreak(BaseOxmlElement):
+    """`a:br` line break element"""
+
+    get_or_add_rPr: Callable[[], CT_TextCharacterProperties]
+
+    rPr = ZeroOrOne("a:rPr", successors=())
+
+    @property
+    def text(self):  # pyright: ignore[reportIncompatibleMethodOverride]
+        """Unconditionally a single vertical-tab character.
+
+        A line break element can contain no text other than the implicit line feed it
+        represents.
+        """
+        return "\v"
+
+
+class CT_TextNormalAutofit(BaseOxmlElement):
+    """`a:normAutofit` element specifying fit text to shape font reduction, etc."""
+
+    fontScale = OptionalAttribute(
+        "fontScale", ST_TextFontScalePercentOrPercentString, default=100.0
+    )
+
+
+class CT_TextParagraph(BaseOxmlElement):
+    """`a:p` custom element class"""
+
+    get_or_add_endParaRPr: Callable[[], CT_TextCharacterProperties]
+    get_or_add_pPr: Callable[[], CT_TextParagraphProperties]
+    r_lst: list[CT_RegularTextRun]
+    _add_br: Callable[[], CT_TextLineBreak]
+    _add_r: Callable[[], CT_RegularTextRun]
+
+    pPr: CT_TextParagraphProperties | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:pPr", successors=("a:r", "a:br", "a:fld", "a:endParaRPr")
+    )
+    r = ZeroOrMore("a:r", successors=("a:endParaRPr",))
+    br = ZeroOrMore("a:br", successors=("a:endParaRPr",))
+    endParaRPr: CT_TextCharacterProperties | None = ZeroOrOne(
+        "a:endParaRPr", successors=()
+    )  # pyright: ignore[reportAssignmentType]
+
+    def add_br(self) -> CT_TextLineBreak:
+        """Return a newly appended `a:br` element."""
+        return self._add_br()
+
+    def add_r(self, text: str | None = None) -> CT_RegularTextRun:
+        """Return a newly appended `a:r` element."""
+        r = self._add_r()
+        if text:
+            r.text = text
+        return r
+
+    def append_text(self, text: str):
+        """Append `a:r` and `a:br` elements to `p` based on `text`.
+
+        Any `\n` or `\v` (vertical-tab) characters in `text` delimit `a:r` (run) elements and
+        themselves are translated to `a:br` (line-break) elements. The vertical-tab character
+        appears in clipboard text from PowerPoint at "soft" line-breaks (new-line, but not new
+        paragraph).
+        """
+        for idx, r_str in enumerate(re.split("\n|\v", text)):
+            # ---breaks are only added _between_ items, not at start---
+            if idx > 0:
+                self.add_br()
+            # ---runs that would be empty are not added---
+            if r_str:
+                self.add_r(r_str)
+
+    @property
+    def content_children(self) -> tuple[CT_RegularTextRun | CT_TextLineBreak | CT_TextField, ...]:
+        """Sequence containing text-container child elements of this `a:p` element.
+
+        These include `a:r`, `a:br`, and `a:fld`.
+        """
+        return tuple(
+            e for e in self if isinstance(e, (CT_RegularTextRun, CT_TextLineBreak, CT_TextField))
+        )
+
+    @property
+    def text(self) -> str:  # pyright: ignore[reportIncompatibleMethodOverride]
+        """str text contained in this paragraph."""
+        # ---note this shadows the lxml _Element.text---
+        return "".join([child.text for child in self.content_children])
+
+    def _new_r(self):
+        r_xml = "<a:r %s><a:t/></a:r>" % nsdecls("a")
+        return parse_xml(r_xml)
+
+
+class CT_TextParagraphProperties(BaseOxmlElement):
+    """`a:pPr` custom element class."""
+
+    get_or_add_defRPr: Callable[[], CT_TextCharacterProperties]
+    _add_lnSpc: Callable[[], CT_TextSpacing]
+    _add_spcAft: Callable[[], CT_TextSpacing]
+    _add_spcBef: Callable[[], CT_TextSpacing]
+    _remove_lnSpc: Callable[[], None]
+    _remove_spcAft: Callable[[], None]
+    _remove_spcBef: Callable[[], None]
+
+    _tag_seq = (
+        "a:lnSpc",
+        "a:spcBef",
+        "a:spcAft",
+        "a:buClrTx",
+        "a:buClr",
+        "a:buSzTx",
+        "a:buSzPct",
+        "a:buSzPts",
+        "a:buFontTx",
+        "a:buFont",
+        "a:buNone",
+        "a:buAutoNum",
+        "a:buChar",
+        "a:buBlip",
+        "a:tabLst",
+        "a:defRPr",
+        "a:extLst",
+    )
+    lnSpc: CT_TextSpacing | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:lnSpc", successors=_tag_seq[1:]
+    )
+    spcBef: CT_TextSpacing | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:spcBef", successors=_tag_seq[2:]
+    )
+    spcAft: CT_TextSpacing | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:spcAft", successors=_tag_seq[3:]
+    )
+    defRPr: CT_TextCharacterProperties | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:defRPr", successors=_tag_seq[16:]
+    )
+    lvl: int = OptionalAttribute(  # pyright: ignore[reportAssignmentType]
+        "lvl", ST_TextIndentLevelType, default=0
+    )
+    algn: PP_PARAGRAPH_ALIGNMENT | None = OptionalAttribute(
+        "algn", PP_PARAGRAPH_ALIGNMENT
+    )  # pyright: ignore[reportAssignmentType]
+    del _tag_seq
+
+    @property
+    def line_spacing(self) -> float | Length | None:
+        """The spacing between baselines of successive lines in this paragraph.
+
+        A float value indicates a number of lines. A |Length| value indicates a fixed spacing.
+        Value is contained in `./a:lnSpc/a:spcPts/@val` or `./a:lnSpc/a:spcPct/@val`. Value is
+        |None| if no element is present.
+        """
+        lnSpc = self.lnSpc
+        if lnSpc is None:
+            return None
+        if lnSpc.spcPts is not None:
+            return lnSpc.spcPts.val
+        return cast(CT_TextSpacingPercent, lnSpc.spcPct).val
+
+    @line_spacing.setter
+    def line_spacing(self, value: float | Length | None):
+        self._remove_lnSpc()
+        if value is None:
+            return
+        if isinstance(value, Length):
+            self._add_lnSpc().set_spcPts(value)
+        else:
+            self._add_lnSpc().set_spcPct(value)
+
+    @property
+    def space_after(self) -> Length | None:
+        """The EMU equivalent of the centipoints value in `./a:spcAft/a:spcPts/@val`."""
+        spcAft = self.spcAft
+        if spcAft is None:
+            return None
+        spcPts = spcAft.spcPts
+        if spcPts is None:
+            return None
+        return spcPts.val
+
+    @space_after.setter
+    def space_after(self, value: Length | None):
+        self._remove_spcAft()
+        if value is not None:
+            self._add_spcAft().set_spcPts(value)
+
+    @property
+    def space_before(self):
+        """The EMU equivalent of the centipoints value in `./a:spcBef/a:spcPts/@val`."""
+        spcBef = self.spcBef
+        if spcBef is None:
+            return None
+        spcPts = spcBef.spcPts
+        if spcPts is None:
+            return None
+        return spcPts.val
+
+    @space_before.setter
+    def space_before(self, value: Length | None):
+        self._remove_spcBef()
+        if value is not None:
+            self._add_spcBef().set_spcPts(value)
+
+
+class CT_TextSpacing(BaseOxmlElement):
+    """Used for `a:lnSpc`, `a:spcBef`, and `a:spcAft` elements."""
+
+    get_or_add_spcPct: Callable[[], CT_TextSpacingPercent]
+    get_or_add_spcPts: Callable[[], CT_TextSpacingPoint]
+    _remove_spcPct: Callable[[], None]
+    _remove_spcPts: Callable[[], None]
+
+    # this should actually be a OneAndOnlyOneChoice, but that's not
+    # implemented yet.
+    spcPct: CT_TextSpacingPercent | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:spcPct"
+    )
+    spcPts: CT_TextSpacingPoint | None = ZeroOrOne(  # pyright: ignore[reportAssignmentType]
+        "a:spcPts"
+    )
+
+    def set_spcPct(self, value: float):
+        """Set spacing to `value` lines, e.g. 1.75 lines.
+
+        A ./a:spcPts child is removed if present.
+        """
+        self._remove_spcPts()
+        spcPct = self.get_or_add_spcPct()
+        spcPct.val = value
+
+    def set_spcPts(self, value: Length):
+        """Set spacing to `value` points. A ./a:spcPct child is removed if present."""
+        self._remove_spcPct()
+        spcPts = self.get_or_add_spcPts()
+        spcPts.val = value
+
+
+class CT_TextSpacingPercent(BaseOxmlElement):
+    """`a:spcPct` element, specifying spacing in thousandths of a percent in its `val` attribute."""
+
+    val: float = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "val", ST_TextSpacingPercentOrPercentString
+    )
+
+
+class CT_TextSpacingPoint(BaseOxmlElement):
+    """`a:spcPts` element, specifying spacing in centipoints in its `val` attribute."""
+
+    val: Length = RequiredAttribute(  # pyright: ignore[reportAssignmentType]
+        "val", ST_TextSpacingPoint
+    )
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/theme.py b/.venv/lib/python3.12/site-packages/pptx/oxml/theme.py
new file mode 100644
index 00000000..19ac8dea
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/theme.py
@@ -0,0 +1,29 @@
+"""lxml custom element classes for theme-related XML elements."""
+
+from __future__ import annotations
+
+from . import parse_from_template
+from .xmlchemy import BaseOxmlElement
+
+
+class CT_OfficeStyleSheet(BaseOxmlElement):
+    """
+    ``<a:theme>`` element, root of a theme part
+    """
+
+    _tag_seq = (
+        "a:themeElements",
+        "a:objectDefaults",
+        "a:extraClrSchemeLst",
+        "a:custClrLst",
+        "a:extLst",
+    )
+    del _tag_seq
+
+    @classmethod
+    def new_default(cls):
+        """
+        Return a new ``<a:theme>`` element containing default settings
+        suitable for use with a notes master.
+        """
+        return parse_from_template("theme")
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/xmlchemy.py b/.venv/lib/python3.12/site-packages/pptx/oxml/xmlchemy.py
new file mode 100644
index 00000000..41fb2e17
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/xmlchemy.py
@@ -0,0 +1,717 @@
+"""Base and meta classes enabling declarative definition of custom element classes."""
+
+from __future__ import annotations
+
+import re
+from typing import Any, Callable, Iterable, Protocol, Sequence, Type, cast
+
+from lxml import etree
+from lxml.etree import ElementBase, _Element  # pyright: ignore[reportPrivateUsage]
+
+from pptx.exc import InvalidXmlError
+from pptx.oxml import oxml_parser
+from pptx.oxml.ns import NamespacePrefixedTag, _nsmap, qn  # pyright: ignore[reportPrivateUsage]
+from pptx.util import lazyproperty
+
+
+class AttributeType(Protocol):
+    """Interface for an object that can act as an attribute type.
+
+    An attribute-type specifies how values are transformed to and from the XML "string" value of the
+    attribute.
+    """
+
+    @classmethod
+    def from_xml(cls, xml_value: str) -> Any:
+        """Transform an attribute value to a Python value."""
+        ...
+
+    @classmethod
+    def to_xml(cls, value: Any) -> str:
+        """Transform a Python value to a str value suitable to this XML attribute."""
+        ...
+
+
+def OxmlElement(nsptag_str: str, nsmap: dict[str, str] | None = None) -> BaseOxmlElement:
+    """Return a "loose" lxml element having the tag specified by `nsptag_str`.
+
+    `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.
+    """
+    nsptag = NamespacePrefixedTag(nsptag_str)
+    nsmap = nsmap if nsmap is not None else nsptag.nsmap
+    return oxml_parser.makeelement(nsptag.clark_name, nsmap=nsmap)
+
+
+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; 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) -> bool:
+        """True if the element in `line_2` is XML-equivalent to the element in `line`.
+
+        In particular, the order of attributes in XML is not significant.
+        """
+        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
+
+    def _parse_line(self, line: str):
+        """Return front, attrs, close, text 4-tuple result of parsing XML element string `line`."""
+        match = self._xml_elm_line_patt.match(line)
+        if match is None:
+            raise ValueError("`line` does not match pattern for an XML element")
+        front, attrs, close, text = [match.group(n) for n in range(1, 5)]
+        return front, attrs, close, text
+
+
+class MetaOxmlElement(type):
+    """Metaclass for BaseOxmlElement."""
+
+    def __init__(cls, clsname: str, bases: tuple[type, ...], clsdict: dict[str, Any]):
+        dispatchable = (
+            OneAndOnlyOne,
+            OneOrMore,
+            OptionalAttribute,
+            RequiredAttribute,
+            ZeroOrMore,
+            ZeroOrOne,
+            ZeroOrOneChoice,
+        )
+        for key, value in clsdict.items():
+            if isinstance(value, dispatchable):
+                value.populate_class_members(cls, key)
+
+
+class BaseAttribute:
+    """Base class for OptionalAttribute and RequiredAttribute, providing common methods."""
+
+    def __init__(self, attr_name: str, simple_type: type[AttributeType]):
+        self._attr_name = attr_name
+        self._simple_type = simple_type
+
+    def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
+        """
+        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]:
+        """Callable suitable for the "get" side of the attribute property descriptor."""
+        raise NotImplementedError("must be implemented by each subclass")
+
+    @property
+    def _setter(self) -> Callable[[BaseOxmlElement, Any], None]:
+        """Callable suitable for the "set" side of the attribute property descriptor."""
+        raise NotImplementedError("must be implemented by each subclass")
+
+
+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.
+    """
+
+    def __init__(self, attr_name: str, simple_type: type[AttributeType], default: Any = None):
+        super(OptionalAttribute, self).__init__(attr_name, simple_type)
+        self._default = default
+
+    @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, or |None| (or spec"
+            "ified default value) if not present. Assigning the default valu"
+            "e causes the attribute to be removed from the element."
+            % (self._simple_type.__name__, self._attr_name)
+        )
+
+    @property
+    def _getter(self) -> Callable[[BaseOxmlElement], Any]:
+        """Callable suitable for the "get" side of the attribute property descriptor."""
+
+        def get_attr_value(obj: BaseOxmlElement) -> Any:
+            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]:
+        """Callable suitable for the "set" side of the attribute property descriptor."""
+
+        def set_attr_value(obj: BaseOxmlElement, value: Any) -> None:
+            # -- when an XML attribute has a default value, setting it to that default removes the
+            # -- attribute from the element (when it is present)
+            if 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)
+            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 _getter(self) -> Callable[[BaseOxmlElement], Any]:
+        """Callable suitable for the "get" side of the attribute property descriptor."""
+
+        def get_attr_value(obj: BaseOxmlElement) -> Any:
+            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 _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 _setter(self) -> Callable[[BaseOxmlElement, Any], None]:
+        """Callable suitable for the "set" side of the attribute property descriptor."""
+
+        def set_attr_value(obj: BaseOxmlElement, value: Any) -> None:
+            str_value = self._simple_type.to_xml(value)
+            obj.set(self._clark_name, str_value)
+
+        return set_attr_value
+
+
+class _BaseChildElement:
+    """Base class for the child element classes corresponding to varying cardinalities.
+
+    Subclasses include ZeroOrOne and ZeroOrMore.
+    """
+
+    def __init__(self, nsptagname: str, successors: Sequence[str] = ()):
+        super(_BaseChildElement, self).__init__()
+        self._nsptagname = nsptagname
+        self._successors = successors
+
+    def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
+        """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.
+
+        This method 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 parent element class.
+
+        The property locates and returns this child element or `None` if not present.
+        """
+        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 = f"{self._prop_name}_lst"
+        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_to_class(self, name: str, method: Callable[..., Any]):
+        """Add `method` to the target class as `name`, unless `name` is already defined there."""
+        if hasattr(self._element_cls, name):
+            return
+        setattr(self._element_cls, name, method)
+
+    @property
+    def _creator(self) -> Callable[[BaseOxmlElement], BaseOxmlElement]:
+        """Callable that creates a new, empty element of the child type, having no attributes."""
+
+        def new_child_element(obj: BaseOxmlElement):
+            return OxmlElement(self._nsptagname)
+
+        return new_child_element
+
+    @property
+    def _getter(self) -> Callable[[BaseOxmlElement], BaseOxmlElement | None]:
+        """Callable 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) -> BaseOxmlElement | None:
+            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) -> Callable[[BaseOxmlElement], list[BaseOxmlElement]]:
+        """Callable suitable for the "get" side of a list property descriptor."""
+
+        def get_child_element_list(obj: BaseOxmlElement) -> list[BaseOxmlElement]:
+            return cast("list[BaseOxmlElement]", 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 _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: Type[BaseOxmlElement], group_prop_name: str, successors: Sequence[str]
+    ):
+        """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) -> None:
+        """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):
+        """
+        Calculate property name from tag name, e.g. a:schemeClr -> schemeClr.
+        """
+        if ":" in self._nsptagname:
+            start = self._nsptagname.index(":") + 1
+        else:
+            start = 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: Type[BaseOxmlElement], prop_name: str):
+        """
+        Add the appropriate methods to *element_cls*.
+        """
+        super(OneAndOnlyOne, self).populate_class_members(element_cls, prop_name)
+        self._add_getter()
+
+    @property
+    def _getter(self) -> Callable[[BaseOxmlElement], BaseOxmlElement]:
+        """Callable suitable for the "get" side of the property descriptor."""
+
+        def get_child_element(obj: BaseOxmlElement) -> 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: Type[BaseOxmlElement], prop_name: str):
+        """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)
+
+    def _add_public_adder(self) -> None:
+        """Add a public `.add_x()` method to the parent element class."""
+
+        def add_child(obj: BaseOxmlElement) -> 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)
+
+    @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
+
+
+class ZeroOrMore(_BaseChildElement):
+    """
+    Defines an optional repeating child element for MetaOxmlElement.
+    """
+
+    def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
+        """
+        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()
+        delattr(element_cls, prop_name)
+
+
+class ZeroOrOne(_BaseChildElement):
+    """Defines an optional child element for MetaOxmlElement."""
+
+    def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
+        """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) -> 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) -> None:
+            obj.remove_all(self._nsptagname)
+
+        _remove_child.__doc__ = f"Remove all `{self._nsptagname}` child elements."
+        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):
+    """An `EG_*` element group where at most one of its members may appear as a child."""
+
+    def __init__(self, choices: Iterable[Choice], successors: Iterable[str] = ()):
+        self._choices = tuple(choices)
+        self._successors = tuple(successors)
+
+    def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
+        """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.
+
+        The property 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) -> None:
+            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) -> BaseOxmlElement | None:
+            return cast(
+                "BaseOxmlElement | None", 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) -> list[str]:
+        """Sequence of namespace-prefixed tagnames, one for each member element of choice group."""
+        return [choice.nsptagname for choice in self._choices]
+
+    @lazyproperty
+    def _remove_choice_group_method_name(self):
+        """Function-name for choice remover."""
+        return f"_remove_{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)
diff --git a/.venv/lib/python3.12/site-packages/pptx/package.py b/.venv/lib/python3.12/site-packages/pptx/package.py
new file mode 100644
index 00000000..79703cd6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/package.py
@@ -0,0 +1,222 @@
+"""Overall .pptx package."""
+
+from __future__ import annotations
+
+from typing import IO, Iterator
+
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.opc.package import OpcPackage
+from pptx.opc.packuri import PackURI
+from pptx.parts.coreprops import CorePropertiesPart
+from pptx.parts.image import Image, ImagePart
+from pptx.parts.media import MediaPart
+from pptx.util import lazyproperty
+
+
+class Package(OpcPackage):
+    """An overall .pptx package."""
+
+    @lazyproperty
+    def core_properties(self) -> CorePropertiesPart:
+        """Instance of |CoreProperties| holding read/write Dublin Core doc properties.
+
+        Creates a default core properties part if one is not present (not common).
+        """
+        try:
+            return self.part_related_by(RT.CORE_PROPERTIES)
+        except KeyError:
+            core_props = CorePropertiesPart.default(self)
+            self.relate_to(core_props, RT.CORE_PROPERTIES)
+            return core_props
+
+    def get_or_add_image_part(self, image_file: str | IO[bytes]):
+        """
+        Return an |ImagePart| object containing the image in *image_file*. If
+        the image part already exists in this package, it is reused,
+        otherwise a new one is created.
+        """
+        return self._image_parts.get_or_add_image_part(image_file)
+
+    def get_or_add_media_part(self, media):
+        """Return a |MediaPart| object containing the media in *media*.
+
+        If a media part for this media bytestream ("file") is already present
+        in this package, it is reused, otherwise a new one is created.
+        """
+        return self._media_parts.get_or_add_media_part(media)
+
+    def next_image_partname(self, ext: str) -> PackURI:
+        """Return a |PackURI| instance representing the next available image partname.
+
+        Partname uses the next available sequence number. *ext* is used as the extention on the
+        returned partname.
+        """
+
+        def first_available_image_idx():
+            image_idxs = sorted(
+                [
+                    part.partname.idx
+                    for part in self.iter_parts()
+                    if (
+                        part.partname.startswith("/ppt/media/image")
+                        and part.partname.idx is not None
+                    )
+                ]
+            )
+            for i, image_idx in enumerate(image_idxs):
+                idx = i + 1
+                if idx < image_idx:
+                    return idx
+            return len(image_idxs) + 1
+
+        idx = first_available_image_idx()
+        return PackURI("/ppt/media/image%d.%s" % (idx, ext))
+
+    def next_media_partname(self, ext):
+        """Return |PackURI| instance for next available media partname.
+
+        Partname is first available, starting at sequence number 1. Empty
+        sequence numbers are reused. *ext* is used as the extension on the
+        returned partname.
+        """
+
+        def first_available_media_idx():
+            media_idxs = sorted(
+                [
+                    part.partname.idx
+                    for part in self.iter_parts()
+                    if part.partname.startswith("/ppt/media/media")
+                ]
+            )
+            for i, media_idx in enumerate(media_idxs):
+                idx = i + 1
+                if idx < media_idx:
+                    return idx
+            return len(media_idxs) + 1
+
+        idx = first_available_media_idx()
+        return PackURI("/ppt/media/media%d.%s" % (idx, ext))
+
+    @property
+    def presentation_part(self):
+        """
+        Reference to the |Presentation| instance contained in this package.
+        """
+        return self.main_document_part
+
+    @lazyproperty
+    def _image_parts(self):
+        """
+        |_ImageParts| object providing access to the image parts in this
+        package.
+        """
+        return _ImageParts(self)
+
+    @lazyproperty
+    def _media_parts(self):
+        """Return |_MediaParts| object for this package.
+
+        The media parts object provides access to all the media parts in this
+        package.
+        """
+        return _MediaParts(self)
+
+
+class _ImageParts(object):
+    """Provides access to the image parts in a package."""
+
+    def __init__(self, package):
+        super(_ImageParts, self).__init__()
+        self._package = package
+
+    def __iter__(self) -> Iterator[ImagePart]:
+        """Generate a reference to each |ImagePart| object in the package."""
+        image_parts = []
+        for rel in self._package.iter_rels():
+            if rel.is_external:
+                continue
+            if rel.reltype != RT.IMAGE:
+                continue
+            image_part = rel.target_part
+            if image_part in image_parts:
+                continue
+            image_parts.append(image_part)
+            yield image_part
+
+    def get_or_add_image_part(self, image_file: str | IO[bytes]) -> ImagePart:
+        """Return |ImagePart| object containing the image in `image_file`.
+
+        `image_file` can be either a path to an image file or a file-like object
+        containing an image. If an image part containing this same image already exists,
+        that instance is returned, otherwise a new image part is created.
+        """
+        image = Image.from_file(image_file)
+        image_part = self._find_by_sha1(image.sha1)
+        return image_part if image_part else ImagePart.new(self._package, image)
+
+    def _find_by_sha1(self, sha1: str) -> ImagePart | None:
+        """
+        Return an |ImagePart| object belonging to this package or |None| if
+        no matching image part is found. The image part is identified by the
+        SHA1 hash digest of the image binary it contains.
+        """
+        for image_part in self:
+            # ---skip unknown/unsupported image types, like SVG---
+            if not hasattr(image_part, "sha1"):
+                continue
+            if image_part.sha1 == sha1:
+                return image_part
+        return None
+
+
+class _MediaParts(object):
+    """Provides access to the media parts in a package.
+
+    Supports iteration and :meth:`get()` using the media object SHA1 hash as
+    its key.
+    """
+
+    def __init__(self, package):
+        super(_MediaParts, self).__init__()
+        self._package = package
+
+    def __iter__(self):
+        """Generate a reference to each |MediaPart| object in the package."""
+        # A media part can appear in more than one relationship (and commonly
+        # does in the case of video). Use media_parts to keep track of those
+        # that have been "yielded"; they can be skipped if they occur again.
+        media_parts = []
+        for rel in self._package.iter_rels():
+            if rel.is_external:
+                continue
+            if rel.reltype not in (RT.MEDIA, RT.VIDEO):
+                continue
+            media_part = rel.target_part
+            if media_part in media_parts:
+                continue
+            media_parts.append(media_part)
+            yield media_part
+
+    def get_or_add_media_part(self, media):
+        """Return a |MediaPart| object containing the media in *media*.
+
+        If this package already contains a media part for the same
+        bytestream, that instance is returned, otherwise a new media part is
+        created.
+        """
+        media_part = self._find_by_sha1(media.sha1)
+        if media_part is None:
+            media_part = MediaPart.new(self._package, media)
+        return media_part
+
+    def _find_by_sha1(self, sha1):
+        """Return |MediaPart| object having *sha1* hash or None if not found.
+
+        All media parts belonging to this package are considered. A media
+        part is identified by the SHA1 hash digest of its bytestream
+        ("file").
+        """
+        for media_part in self:
+            if media_part.sha1 == sha1:
+                return media_part
+        return None
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/__init__.py b/.venv/lib/python3.12/site-packages/pptx/parts/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/chart.py b/.venv/lib/python3.12/site-packages/pptx/parts/chart.py
new file mode 100644
index 00000000..7208071b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/chart.py
@@ -0,0 +1,95 @@
+"""Chart part objects, including Chart and Charts."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pptx.chart.chart import Chart
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.opc.package import XmlPart
+from pptx.parts.embeddedpackage import EmbeddedXlsxPart
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.chart.data import ChartData
+    from pptx.enum.chart import XL_CHART_TYPE
+    from pptx.package import Package
+
+
+class ChartPart(XmlPart):
+    """A chart part.
+
+    Corresponds to parts having partnames matching ppt/charts/chart[1-9][0-9]*.xml
+    """
+
+    partname_template = "/ppt/charts/chart%d.xml"
+
+    @classmethod
+    def new(cls, chart_type: XL_CHART_TYPE, chart_data: ChartData, package: Package):
+        """Return new |ChartPart| instance added to `package`.
+
+        Returned chart-part contains a chart of `chart_type` depicting `chart_data`.
+        """
+        chart_part = cls.load(
+            package.next_partname(cls.partname_template),
+            CT.DML_CHART,
+            package,
+            chart_data.xml_bytes(chart_type),
+        )
+        chart_part.chart_workbook.update_from_xlsx_blob(chart_data.xlsx_blob)
+        return chart_part
+
+    @lazyproperty
+    def chart(self):
+        """|Chart| object representing the chart in this part."""
+        return Chart(self._element, self)
+
+    @lazyproperty
+    def chart_workbook(self):
+        """
+        The |ChartWorkbook| object providing access to the external chart
+        data in a linked or embedded Excel workbook.
+        """
+        return ChartWorkbook(self._element, self)
+
+
+class ChartWorkbook(object):
+    """Provides access to external chart data in a linked or embedded Excel workbook."""
+
+    def __init__(self, chartSpace, chart_part):
+        super(ChartWorkbook, self).__init__()
+        self._chartSpace = chartSpace
+        self._chart_part = chart_part
+
+    def update_from_xlsx_blob(self, xlsx_blob):
+        """
+        Replace the Excel spreadsheet in the related |EmbeddedXlsxPart| with
+        the Excel binary in *xlsx_blob*, adding a new |EmbeddedXlsxPart| if
+        there isn't one.
+        """
+        xlsx_part = self.xlsx_part
+        if xlsx_part is None:
+            self.xlsx_part = EmbeddedXlsxPart.new(xlsx_blob, self._chart_part.package)
+            return
+        xlsx_part.blob = xlsx_blob
+
+    @property
+    def xlsx_part(self):
+        """Optional |EmbeddedXlsxPart| object containing data for this chart.
+
+        This related part has its rId at `c:chartSpace/c:externalData/@rId`. This value
+        is |None| if there is no `<c:externalData>` element.
+        """
+        xlsx_part_rId = self._chartSpace.xlsx_part_rId
+        return None if xlsx_part_rId is None else self._chart_part.related_part(xlsx_part_rId)
+
+    @xlsx_part.setter
+    def xlsx_part(self, xlsx_part):
+        """
+        Set the related |EmbeddedXlsxPart| to *xlsx_part*. Assume one does
+        not already exist.
+        """
+        rId = self._chart_part.relate_to(xlsx_part, RT.PACKAGE)
+        externalData = self._chartSpace.get_or_add_externalData()
+        externalData.rId = rId
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/coreprops.py b/.venv/lib/python3.12/site-packages/pptx/parts/coreprops.py
new file mode 100644
index 00000000..8471cc8e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/coreprops.py
@@ -0,0 +1,167 @@
+"""Core properties part, corresponds to ``/docProps/core.xml`` part in package."""
+
+from __future__ import annotations
+
+import datetime as dt
+from typing import TYPE_CHECKING
+
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.package import XmlPart
+from pptx.opc.packuri import PackURI
+from pptx.oxml.coreprops import CT_CoreProperties
+
+if TYPE_CHECKING:
+    from pptx.package import Package
+
+
+class CorePropertiesPart(XmlPart):
+    """Corresponds to part named `/docProps/core.xml`.
+
+    Contains the core document properties for this document package.
+    """
+
+    _element: CT_CoreProperties
+
+    @classmethod
+    def default(cls, package: Package):
+        """Return default new |CorePropertiesPart| instance suitable as starting point.
+
+        This provides a base for adding core-properties to a package that doesn't yet
+        have any.
+        """
+        core_props = cls._new(package)
+        core_props.title = "PowerPoint Presentation"
+        core_props.last_modified_by = "python-pptx"
+        core_props.revision = 1
+        core_props.modified = dt.datetime.now(dt.timezone.utc).replace(tzinfo=None)
+        return core_props
+
+    @property
+    def author(self) -> str:
+        return self._element.author_text
+
+    @author.setter
+    def author(self, value: str):
+        self._element.author_text = value
+
+    @property
+    def category(self) -> str:
+        return self._element.category_text
+
+    @category.setter
+    def category(self, value: str):
+        self._element.category_text = value
+
+    @property
+    def comments(self) -> str:
+        return self._element.comments_text
+
+    @comments.setter
+    def comments(self, value: str):
+        self._element.comments_text = value
+
+    @property
+    def content_status(self) -> str:
+        return self._element.contentStatus_text
+
+    @content_status.setter
+    def content_status(self, value: str):
+        self._element.contentStatus_text = value
+
+    @property
+    def created(self):
+        return self._element.created_datetime
+
+    @created.setter
+    def created(self, value: dt.datetime):
+        self._element.created_datetime = value
+
+    @property
+    def identifier(self) -> str:
+        return self._element.identifier_text
+
+    @identifier.setter
+    def identifier(self, value: str):
+        self._element.identifier_text = value
+
+    @property
+    def keywords(self) -> str:
+        return self._element.keywords_text
+
+    @keywords.setter
+    def keywords(self, value: str):
+        self._element.keywords_text = value
+
+    @property
+    def language(self) -> str:
+        return self._element.language_text
+
+    @language.setter
+    def language(self, value: str):
+        self._element.language_text = value
+
+    @property
+    def last_modified_by(self) -> str:
+        return self._element.lastModifiedBy_text
+
+    @last_modified_by.setter
+    def last_modified_by(self, value: str):
+        self._element.lastModifiedBy_text = value
+
+    @property
+    def last_printed(self):
+        return self._element.lastPrinted_datetime
+
+    @last_printed.setter
+    def last_printed(self, value: dt.datetime):
+        self._element.lastPrinted_datetime = value
+
+    @property
+    def modified(self):
+        return self._element.modified_datetime
+
+    @modified.setter
+    def modified(self, value: dt.datetime):
+        self._element.modified_datetime = value
+
+    @property
+    def revision(self):
+        return self._element.revision_number
+
+    @revision.setter
+    def revision(self, value: int):
+        self._element.revision_number = value
+
+    @property
+    def subject(self) -> str:
+        return self._element.subject_text
+
+    @subject.setter
+    def subject(self, value: str):
+        self._element.subject_text = value
+
+    @property
+    def title(self) -> str:
+        return self._element.title_text
+
+    @title.setter
+    def title(self, value: str):
+        self._element.title_text = value
+
+    @property
+    def version(self) -> str:
+        return self._element.version_text
+
+    @version.setter
+    def version(self, value: str):
+        self._element.version_text = value
+
+    @classmethod
+    def _new(cls, package: Package) -> CorePropertiesPart:
+        """Return new empty |CorePropertiesPart| instance."""
+        return CorePropertiesPart(
+            PackURI("/docProps/core.xml"),
+            CT.OPC_CORE_PROPERTIES,
+            package,
+            CT_CoreProperties.new_coreProperties(),
+        )
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/embeddedpackage.py b/.venv/lib/python3.12/site-packages/pptx/parts/embeddedpackage.py
new file mode 100644
index 00000000..7aa2cf40
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/embeddedpackage.py
@@ -0,0 +1,93 @@
+"""Embedded Package part objects.
+
+"Package" in this context means another OPC package, i.e. a DOCX, PPTX, or XLSX "file".
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pptx.enum.shapes import PROG_ID
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.package import Part
+
+if TYPE_CHECKING:
+    from pptx.package import Package
+
+
+class EmbeddedPackagePart(Part):
+    """A distinct OPC package, e.g. an Excel file, embedded in this PPTX package.
+
+    Has a partname like: `ppt/embeddings/Microsoft_Excel_Sheet1.xlsx`.
+    """
+
+    @classmethod
+    def factory(cls, prog_id: PROG_ID | str, object_blob: bytes, package: Package):
+        """Return a new |EmbeddedPackagePart| subclass instance added to *package*.
+
+        The subclass is determined by `prog_id` which corresponds to the "application"
+        used to open the "file-type" of `object_blob`. The returned part contains the
+        bytes of `object_blob` and has the content-type also determined by `prog_id`.
+        """
+        # --- a generic OLE object has no subclass ---
+        if not isinstance(prog_id, PROG_ID):
+            return cls(
+                package.next_partname("/ppt/embeddings/oleObject%d.bin"),
+                CT.OFC_OLE_OBJECT,
+                package,
+                object_blob,
+            )
+
+        # --- A Microsoft Office file-type is a distinguished package object ---
+        EmbeddedPartCls = {
+            PROG_ID.DOCX: EmbeddedDocxPart,
+            PROG_ID.PPTX: EmbeddedPptxPart,
+            PROG_ID.XLSX: EmbeddedXlsxPart,
+        }[prog_id]
+
+        return EmbeddedPartCls.new(object_blob, package)
+
+    @classmethod
+    def new(cls, blob: bytes, package: Package):
+        """Return new |EmbeddedPackagePart| subclass object.
+
+        The returned part object contains `blob` and is added to `package`.
+        """
+        return cls(
+            package.next_partname(cls.partname_template),
+            cls.content_type,
+            package,
+            blob,
+        )
+
+
+class EmbeddedDocxPart(EmbeddedPackagePart):
+    """A Word .docx file stored in a part.
+
+    This part-type arises when a Word document appears as an embedded OLE-object shape.
+    """
+
+    partname_template = "/ppt/embeddings/Microsoft_Word_Document%d.docx"
+    content_type = CT.WML_DOCUMENT
+
+
+class EmbeddedPptxPart(EmbeddedPackagePart):
+    """A PowerPoint file stored in a part.
+
+    This part-type arises when a PowerPoint presentation (.pptx file) appears as an
+    embedded OLE-object shape.
+    """
+
+    partname_template = "/ppt/embeddings/Microsoft_PowerPoint_Presentation%d.pptx"
+    content_type = CT.PML_PRESENTATION
+
+
+class EmbeddedXlsxPart(EmbeddedPackagePart):
+    """An Excel file stored in a part.
+
+    This part-type arises as the data source for a chart, but may also be the OLE-object
+    for an embedded object shape.
+    """
+
+    partname_template = "/ppt/embeddings/Microsoft_Excel_Sheet%d.xlsx"
+    content_type = CT.SML_SHEET
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/image.py b/.venv/lib/python3.12/site-packages/pptx/parts/image.py
new file mode 100644
index 00000000..9be5d02d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/image.py
@@ -0,0 +1,275 @@
+"""ImagePart and related objects."""
+
+from __future__ import annotations
+
+import hashlib
+import io
+import os
+from typing import IO, TYPE_CHECKING, Any, cast
+
+from PIL import Image as PIL_Image
+
+from pptx.opc.package import Part
+from pptx.opc.spec import image_content_types
+from pptx.util import Emu, lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.opc.packuri import PackURI
+    from pptx.package import Package
+    from pptx.util import Length
+
+
+class ImagePart(Part):
+    """An image part.
+
+    An image part generally has a partname matching the regex `ppt/media/image[1-9][0-9]*.*`.
+    """
+
+    def __init__(
+        self,
+        partname: PackURI,
+        content_type: str,
+        package: Package,
+        blob: bytes,
+        filename: str | None = None,
+    ):
+        super(ImagePart, self).__init__(partname, content_type, package, blob)
+        self._blob = blob
+        self._filename = filename
+
+    @classmethod
+    def new(cls, package: Package, image: Image) -> ImagePart:
+        """Return new |ImagePart| instance containing `image`.
+
+        `image` is an |Image| object.
+        """
+        return cls(
+            package.next_image_partname(image.ext),
+            image.content_type,
+            package,
+            image.blob,
+            image.filename,
+        )
+
+    @property
+    def desc(self) -> str:
+        """The filename associated with this image.
+
+        Either the filename of the original image or a generic name of the form `image.ext` where
+        `ext` is appropriate to the image file format, e.g. `'jpg'`. An image created using a path
+        will have that filename; one created with a file-like object will have a generic name.
+        """
+        # -- return generic filename if original filename is unknown --
+        if self._filename is None:
+            return f"image.{self.ext}"
+        return self._filename
+
+    @property
+    def ext(self) -> str:
+        """File-name extension for this image e.g. `'png'`."""
+        return self.partname.ext
+
+    @property
+    def image(self) -> Image:
+        """An |Image| object containing the image in this image part.
+
+        Note this is a `pptx.image.Image` object, not a PIL Image.
+        """
+        return Image(self._blob, self.desc)
+
+    def scale(self, scaled_cx: int | None, scaled_cy: int | None) -> tuple[int, int]:
+        """Return scaled image dimensions in EMU based on the combination of parameters supplied.
+
+        If `scaled_cx` and `scaled_cy` are both |None|, the native image size is returned. If
+        neither `scaled_cx` nor `scaled_cy` is |None|, their values are returned unchanged. If a
+        value is provided for either `scaled_cx` or `scaled_cy` and the other is |None|, the
+        missing value is calculated such that the image's aspect ratio is preserved.
+        """
+        image_cx, image_cy = self._native_size
+
+        if scaled_cx and scaled_cy:
+            return scaled_cx, scaled_cy
+
+        if scaled_cx and not scaled_cy:
+            scaling_factor = float(scaled_cx) / float(image_cx)
+            scaled_cy = int(round(image_cy * scaling_factor))
+            return scaled_cx, scaled_cy
+
+        if not scaled_cx and scaled_cy:
+            scaling_factor = float(scaled_cy) / float(image_cy)
+            scaled_cx = int(round(image_cx * scaling_factor))
+            return scaled_cx, scaled_cy
+
+        # -- only remaining case is both `scaled_cx` and `scaled_cy` are `None` --
+        return image_cx, image_cy
+
+    @lazyproperty
+    def sha1(self) -> str:
+        """The 40-character SHA1 hash digest for the image binary of this image part.
+
+        like: `"1be010ea47803b00e140b852765cdf84f491da47"`.
+        """
+        return hashlib.sha1(self._blob).hexdigest()
+
+    @property
+    def _dpi(self) -> tuple[int, int]:
+        """(horz_dpi, vert_dpi) pair representing the dots-per-inch resolution of this image."""
+        image = Image.from_blob(self._blob)
+        return image.dpi
+
+    @property
+    def _native_size(self) -> tuple[Length, Length]:
+        """A (width, height) 2-tuple representing the native dimensions of the image in EMU.
+
+        Calculated based on the image DPI value, if present, assuming 72 dpi as a default.
+        """
+        EMU_PER_INCH = 914400
+        horz_dpi, vert_dpi = self._dpi
+        width_px, height_px = self._px_size
+
+        width = EMU_PER_INCH * width_px / horz_dpi
+        height = EMU_PER_INCH * height_px / vert_dpi
+
+        return Emu(int(width)), Emu(int(height))
+
+    @property
+    def _px_size(self) -> tuple[int, int]:
+        """A (width, height) 2-tuple representing the dimensions of this image in pixels."""
+        image = Image.from_blob(self._blob)
+        return image.size
+
+
+class Image(object):
+    """Immutable value object representing an image such as a JPEG, PNG, or GIF."""
+
+    def __init__(self, blob: bytes, filename: str | None):
+        super(Image, self).__init__()
+        self._blob = blob
+        self._filename = filename
+
+    @classmethod
+    def from_blob(cls, blob: bytes, filename: str | None = None) -> Image:
+        """Return a new |Image| object loaded from the image binary in `blob`."""
+        return cls(blob, filename)
+
+    @classmethod
+    def from_file(cls, image_file: str | IO[bytes]) -> Image:
+        """Return a new |Image| object loaded from `image_file`.
+
+        `image_file` can be either a path (str) or a file-like object.
+        """
+        if isinstance(image_file, str):
+            # treat image_file as a path
+            with open(image_file, "rb") as f:
+                blob = f.read()
+            filename = os.path.basename(image_file)
+        else:
+            # assume image_file is a file-like object
+            # ---reposition file cursor if it has one---
+            if callable(getattr(image_file, "seek")):
+                image_file.seek(0)
+            blob = image_file.read()
+            filename = None
+
+        return cls.from_blob(blob, filename)
+
+    @property
+    def blob(self) -> bytes:
+        """The binary image bytestream of this image."""
+        return self._blob
+
+    @lazyproperty
+    def content_type(self) -> str:
+        """MIME-type of this image, e.g. `"image/jpeg"`."""
+        return image_content_types[self.ext]
+
+    @lazyproperty
+    def dpi(self) -> tuple[int, int]:
+        """A (horz_dpi, vert_dpi) 2-tuple specifying the dots-per-inch resolution of this image.
+
+        A default value of (72, 72) is used if the dpi is not specified in the image file.
+        """
+
+        def int_dpi(dpi: Any):
+            """Return an integer dots-per-inch value corresponding to `dpi`.
+
+            If `dpi` is |None|, a non-numeric type, less than 1 or greater than 2048, 72 is
+            returned.
+            """
+            try:
+                int_dpi = int(round(float(dpi)))
+                if int_dpi < 1 or int_dpi > 2048:
+                    int_dpi = 72
+            except (TypeError, ValueError):
+                int_dpi = 72
+            return int_dpi
+
+        def normalize_pil_dpi(pil_dpi: tuple[int, int] | None):
+            """Return a (horz_dpi, vert_dpi) 2-tuple corresponding to `pil_dpi`.
+
+            The value for the 'dpi' key in the `info` dict of a PIL image. If the 'dpi' key is not
+            present or contains an invalid value, `(72, 72)` is returned.
+            """
+            if isinstance(pil_dpi, tuple):
+                return (int_dpi(pil_dpi[0]), int_dpi(pil_dpi[1]))
+            return (72, 72)
+
+        return normalize_pil_dpi(self._pil_props[2])
+
+    @lazyproperty
+    def ext(self) -> str:
+        """Canonical file extension for this image e.g. `'png'`.
+
+        The returned extension is all lowercase and is the canonical extension for the content type
+        of this image, regardless of what extension may have been used in its filename, if any.
+        """
+        ext_map = {
+            "BMP": "bmp",
+            "GIF": "gif",
+            "JPEG": "jpg",
+            "PNG": "png",
+            "TIFF": "tiff",
+            "WMF": "wmf",
+        }
+        format = self._format
+        if format not in ext_map:
+            tmpl = "unsupported image format, expected one of: %s, got '%s'"
+            raise ValueError(tmpl % (ext_map.keys(), format))
+        return ext_map[format]
+
+    @property
+    def filename(self) -> str | None:
+        """Filename from path used to load this image, if loaded from the filesystem.
+
+        |None| if no filename was used in loading, such as when loaded from an in-memory stream.
+        """
+        return self._filename
+
+    @lazyproperty
+    def sha1(self) -> str:
+        """SHA1 hash digest of the image blob."""
+        return hashlib.sha1(self._blob).hexdigest()
+
+    @lazyproperty
+    def size(self) -> tuple[int, int]:
+        """A (width, height) 2-tuple specifying the dimensions of this image in pixels."""
+        return self._pil_props[1]
+
+    @property
+    def _format(self) -> str | None:
+        """The PIL Image format of this image, e.g. 'PNG'."""
+        return self._pil_props[0]
+
+    @lazyproperty
+    def _pil_props(self) -> tuple[str | None, tuple[int, int], tuple[int, int] | None]:
+        """tuple of image properties extracted from this image using Pillow."""
+        stream = io.BytesIO(self._blob)
+        pil_image = PIL_Image.open(stream)  # pyright: ignore[reportUnknownMemberType]
+        format = pil_image.format
+        width_px, height_px = pil_image.size
+        dpi = cast(
+            "tuple[int, int] | None",
+            pil_image.info.get("dpi"),  # pyright: ignore[reportUnknownMemberType]
+        )
+        stream.close()
+        return (format, (width_px, height_px), dpi)
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/media.py b/.venv/lib/python3.12/site-packages/pptx/parts/media.py
new file mode 100644
index 00000000..7e8bc2f2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/media.py
@@ -0,0 +1,37 @@
+"""MediaPart and related objects."""
+
+from __future__ import annotations
+
+import hashlib
+
+from pptx.opc.package import Part
+from pptx.util import lazyproperty
+
+
+class MediaPart(Part):
+    """A media part, containing an audio or video resource.
+
+    A media part generally has a partname matching the regex
+    `ppt/media/media[1-9][0-9]*.*`.
+    """
+
+    @classmethod
+    def new(cls, package, media):
+        """Return new |MediaPart| instance containing `media`.
+
+        `media` must be a |Media| object.
+        """
+        return cls(
+            package.next_media_partname(media.ext),
+            media.content_type,
+            package,
+            media.blob,
+        )
+
+    @lazyproperty
+    def sha1(self):
+        """The SHA1 hash digest for the media binary of this media part.
+
+        Example: `'1be010ea47803b00e140b852765cdf84f491da47'`
+        """
+        return hashlib.sha1(self._blob).hexdigest()
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/presentation.py b/.venv/lib/python3.12/site-packages/pptx/parts/presentation.py
new file mode 100644
index 00000000..1413de45
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/presentation.py
@@ -0,0 +1,126 @@
+"""Presentation part, the main part in a .pptx package."""
+
+from __future__ import annotations
+
+from typing import IO, TYPE_CHECKING, Iterable
+
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.opc.package import XmlPart
+from pptx.opc.packuri import PackURI
+from pptx.parts.slide import NotesMasterPart, SlidePart
+from pptx.presentation import Presentation
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.parts.coreprops import CorePropertiesPart
+    from pptx.slide import NotesMaster, Slide, SlideLayout, SlideMaster
+
+
+class PresentationPart(XmlPart):
+    """Top level class in object model.
+
+    Represents the contents of the /ppt directory of a .pptx file.
+    """
+
+    def add_slide(self, slide_layout: SlideLayout):
+        """Return (rId, slide) pair of a newly created blank slide.
+
+        New slide inherits appearance from `slide_layout`.
+        """
+        partname = self._next_slide_partname
+        slide_layout_part = slide_layout.part
+        slide_part = SlidePart.new(partname, self.package, slide_layout_part)
+        rId = self.relate_to(slide_part, RT.SLIDE)
+        return rId, slide_part.slide
+
+    @property
+    def core_properties(self) -> CorePropertiesPart:
+        """A |CoreProperties| object for the presentation.
+
+        Provides read/write access to the Dublin Core properties of this presentation.
+        """
+        return self.package.core_properties
+
+    def get_slide(self, slide_id: int) -> Slide | None:
+        """Return optional related |Slide| object identified by `slide_id`.
+
+        Returns |None| if no slide with `slide_id` is related to this presentation.
+        """
+        for sldId in self._element.sldIdLst:
+            if sldId.id == slide_id:
+                return self.related_part(sldId.rId).slide
+        return None
+
+    @lazyproperty
+    def notes_master(self) -> NotesMaster:
+        """
+        Return the |NotesMaster| object for this presentation. If the
+        presentation does not have a notes master, one is created from
+        a default template. The same single instance is returned on each
+        call.
+        """
+        return self.notes_master_part.notes_master
+
+    @lazyproperty
+    def notes_master_part(self) -> NotesMasterPart:
+        """Return the |NotesMasterPart| object for this presentation.
+
+        If the presentation does not have a notes master, one is created from a default template.
+        The same single instance is returned on each call.
+        """
+        try:
+            return self.part_related_by(RT.NOTES_MASTER)
+        except KeyError:
+            notes_master_part = NotesMasterPart.create_default(self.package)
+            self.relate_to(notes_master_part, RT.NOTES_MASTER)
+            return notes_master_part
+
+    @lazyproperty
+    def presentation(self):
+        """
+        A |Presentation| object providing access to the content of this
+        presentation.
+        """
+        return Presentation(self._element, self)
+
+    def related_slide(self, rId: str) -> Slide:
+        """Return |Slide| object for related |SlidePart| related by `rId`."""
+        return self.related_part(rId).slide
+
+    def related_slide_master(self, rId: str) -> SlideMaster:
+        """Return |SlideMaster| object for |SlideMasterPart| related by `rId`."""
+        return self.related_part(rId).slide_master
+
+    def rename_slide_parts(self, rIds: Iterable[str]):
+        """Assign incrementing partnames to the slide parts identified by `rIds`.
+
+        Partnames are like `/ppt/slides/slide9.xml` and are assigned in the order their id appears
+        in the `rIds` sequence. The name portion is always `slide`. The number part forms a
+        continuous sequence starting at 1 (e.g. 1, 2, ... 10, ...). The extension is always
+        `.xml`.
+        """
+        for idx, rId in enumerate(rIds):
+            slide_part = self.related_part(rId)
+            slide_part.partname = PackURI("/ppt/slides/slide%d.xml" % (idx + 1))
+
+    def save(self, path_or_stream: str | IO[bytes]):
+        """Save this presentation package to `path_or_stream`.
+
+        `path_or_stream` can be either a path to a filesystem location (a string) or a
+        file-like object.
+        """
+        self.package.save(path_or_stream)
+
+    def slide_id(self, slide_part):
+        """Return the slide-id associated with `slide_part`."""
+        for sldId in self._element.sldIdLst:
+            if self.related_part(sldId.rId) is slide_part:
+                return sldId.id
+        raise ValueError("matching slide_part not found")
+
+    @property
+    def _next_slide_partname(self):
+        """Return |PackURI| instance containing next available slide partname."""
+        sldIdLst = self._element.get_or_add_sldIdLst()
+        partname_str = "/ppt/slides/slide%d.xml" % (len(sldIdLst) + 1)
+        return PackURI(partname_str)
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/slide.py b/.venv/lib/python3.12/site-packages/pptx/parts/slide.py
new file mode 100644
index 00000000..6650564a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/slide.py
@@ -0,0 +1,297 @@
+"""Slide and related objects."""
+
+from __future__ import annotations
+
+from typing import IO, TYPE_CHECKING, cast
+
+from pptx.enum.shapes import PROG_ID
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.opc.package import XmlPart
+from pptx.opc.packuri import PackURI
+from pptx.oxml.slide import CT_NotesMaster, CT_NotesSlide, CT_Slide
+from pptx.oxml.theme import CT_OfficeStyleSheet
+from pptx.parts.chart import ChartPart
+from pptx.parts.embeddedpackage import EmbeddedPackagePart
+from pptx.slide import NotesMaster, NotesSlide, Slide, SlideLayout, SlideMaster
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.chart.data import ChartData
+    from pptx.enum.chart import XL_CHART_TYPE
+    from pptx.media import Video
+    from pptx.parts.image import Image, ImagePart
+
+
+class BaseSlidePart(XmlPart):
+    """Base class for slide parts.
+
+    This includes slide, slide-layout, and slide-master parts, but also notes-slide,
+    notes-master, and handout-master parts.
+    """
+
+    _element: CT_Slide
+
+    def get_image(self, rId: str) -> Image:
+        """Return an |Image| object containing the image related to this slide by *rId*.
+
+        Raises |KeyError| if no image is related by that id, which would generally indicate a
+        corrupted .pptx file.
+        """
+        return cast("ImagePart", self.related_part(rId)).image
+
+    def get_or_add_image_part(self, image_file: str | IO[bytes]):
+        """Return `(image_part, rId)` pair corresponding to `image_file`.
+
+        The returned |ImagePart| object contains the image in `image_file` and is
+        related to this slide with the key `rId`. If either the image part or
+        relationship already exists, they are reused, otherwise they are newly created.
+        """
+        image_part = self._package.get_or_add_image_part(image_file)
+        rId = self.relate_to(image_part, RT.IMAGE)
+        return image_part, rId
+
+    @property
+    def name(self) -> str:
+        """Internal name of this slide."""
+        return self._element.cSld.name
+
+
+class NotesMasterPart(BaseSlidePart):
+    """Notes master part.
+
+    Corresponds to package file `ppt/notesMasters/notesMaster1.xml`.
+    """
+
+    @classmethod
+    def create_default(cls, package):
+        """
+        Create and return a default notes master part, including creating the
+        new theme it requires.
+        """
+        notes_master_part = cls._new(package)
+        theme_part = cls._new_theme_part(package)
+        notes_master_part.relate_to(theme_part, RT.THEME)
+        return notes_master_part
+
+    @lazyproperty
+    def notes_master(self):
+        """
+        Return the |NotesMaster| object that proxies this notes master part.
+        """
+        return NotesMaster(self._element, self)
+
+    @classmethod
+    def _new(cls, package):
+        """
+        Create and return a standalone, default notes master part based on
+        the built-in template (without any related parts, such as theme).
+        """
+        return NotesMasterPart(
+            PackURI("/ppt/notesMasters/notesMaster1.xml"),
+            CT.PML_NOTES_MASTER,
+            package,
+            CT_NotesMaster.new_default(),
+        )
+
+    @classmethod
+    def _new_theme_part(cls, package):
+        """Return new default theme-part suitable for use with a notes master."""
+        return XmlPart(
+            package.next_partname("/ppt/theme/theme%d.xml"),
+            CT.OFC_THEME,
+            package,
+            CT_OfficeStyleSheet.new_default(),
+        )
+
+
+class NotesSlidePart(BaseSlidePart):
+    """Notes slide part.
+
+    Contains the slide notes content and the layout for the slide handout page.
+    Corresponds to package file `ppt/notesSlides/notesSlide[1-9][0-9]*.xml`.
+    """
+
+    @classmethod
+    def new(cls, package, slide_part):
+        """Return new |NotesSlidePart| for the slide in `slide_part`.
+
+        The new notes-slide part is based on the (singleton) notes master and related to
+        both the notes-master part and `slide_part`. If no notes-master is present,
+        one is created based on the default template.
+        """
+        notes_master_part = package.presentation_part.notes_master_part
+        notes_slide_part = cls._add_notes_slide_part(package, slide_part, notes_master_part)
+        notes_slide = notes_slide_part.notes_slide
+        notes_slide.clone_master_placeholders(notes_master_part.notes_master)
+        return notes_slide_part
+
+    @lazyproperty
+    def notes_master(self):
+        """Return the |NotesMaster| object this notes slide inherits from."""
+        notes_master_part = self.part_related_by(RT.NOTES_MASTER)
+        return notes_master_part.notes_master
+
+    @lazyproperty
+    def notes_slide(self):
+        """Return the |NotesSlide| object that proxies this notes slide part."""
+        return NotesSlide(self._element, self)
+
+    @classmethod
+    def _add_notes_slide_part(cls, package, slide_part, notes_master_part):
+        """Create and return a new notes-slide part.
+
+        The return part is fully related, but has no shape content (i.e. placeholders
+        not cloned).
+        """
+        notes_slide_part = NotesSlidePart(
+            package.next_partname("/ppt/notesSlides/notesSlide%d.xml"),
+            CT.PML_NOTES_SLIDE,
+            package,
+            CT_NotesSlide.new(),
+        )
+        notes_slide_part.relate_to(notes_master_part, RT.NOTES_MASTER)
+        notes_slide_part.relate_to(slide_part, RT.SLIDE)
+        return notes_slide_part
+
+
+class SlidePart(BaseSlidePart):
+    """Slide part. Corresponds to package files ppt/slides/slide[1-9][0-9]*.xml."""
+
+    @classmethod
+    def new(cls, partname, package, slide_layout_part):
+        """Return newly-created blank slide part.
+
+        The new slide-part has `partname` and a relationship to `slide_layout_part`.
+        """
+        slide_part = cls(partname, CT.PML_SLIDE, package, CT_Slide.new())
+        slide_part.relate_to(slide_layout_part, RT.SLIDE_LAYOUT)
+        return slide_part
+
+    def add_chart_part(self, chart_type: XL_CHART_TYPE, chart_data: ChartData):
+        """Return str rId of new |ChartPart| object containing chart of `chart_type`.
+
+        The chart depicts `chart_data` and is related to the slide contained in this
+        part by `rId`.
+        """
+        return self.relate_to(ChartPart.new(chart_type, chart_data, self._package), RT.CHART)
+
+    def add_embedded_ole_object_part(
+        self, prog_id: PROG_ID | str, ole_object_file: str | IO[bytes]
+    ):
+        """Return rId of newly-added OLE-object part formed from `ole_object_file`."""
+        relationship_type = RT.PACKAGE if isinstance(prog_id, PROG_ID) else RT.OLE_OBJECT
+        return self.relate_to(
+            EmbeddedPackagePart.factory(
+                prog_id, self._blob_from_file(ole_object_file), self._package
+            ),
+            relationship_type,
+        )
+
+    def get_or_add_video_media_part(self, video: Video) -> tuple[str, str]:
+        """Return rIds for media and video relationships to media part.
+
+        A new |MediaPart| object is created if it does not already exist
+        (such as would occur if the same video appeared more than once in
+         a presentation). Two relationships to the media part are created,
+        one each with MEDIA and VIDEO relationship types. The need for two
+        appears to be for legacy support for an earlier (pre-Office 2010)
+        PowerPoint media embedding strategy.
+        """
+        media_part = self._package.get_or_add_media_part(video)
+        media_rId = self.relate_to(media_part, RT.MEDIA)
+        video_rId = self.relate_to(media_part, RT.VIDEO)
+        return media_rId, video_rId
+
+    @property
+    def has_notes_slide(self):
+        """
+        Return True if this slide has a notes slide, False otherwise. A notes
+        slide is created by the :attr:`notes_slide` property when one doesn't
+        exist; use this property to test for a notes slide without the
+        possible side-effect of creating one.
+        """
+        try:
+            self.part_related_by(RT.NOTES_SLIDE)
+        except KeyError:
+            return False
+        return True
+
+    @lazyproperty
+    def notes_slide(self) -> NotesSlide:
+        """The |NotesSlide| instance associated with this slide.
+
+        If the slide does not have a notes slide, a new one is created. The same single instance
+        is returned on each call.
+        """
+        try:
+            notes_slide_part = self.part_related_by(RT.NOTES_SLIDE)
+        except KeyError:
+            notes_slide_part = self._add_notes_slide_part()
+        return notes_slide_part.notes_slide
+
+    @lazyproperty
+    def slide(self):
+        """
+        The |Slide| object representing this slide part.
+        """
+        return Slide(self._element, self)
+
+    @property
+    def slide_id(self) -> int:
+        """Return the slide identifier stored in the presentation part for this slide part."""
+        presentation_part = self.package.presentation_part
+        return presentation_part.slide_id(self)
+
+    @property
+    def slide_layout(self) -> SlideLayout:
+        """|SlideLayout| object the slide in this part inherits appearance from."""
+        slide_layout_part = self.part_related_by(RT.SLIDE_LAYOUT)
+        return slide_layout_part.slide_layout
+
+    def _add_notes_slide_part(self):
+        """
+        Return a newly created |NotesSlidePart| object related to this slide
+        part. Caller is responsible for ensuring this slide doesn't already
+        have a notes slide part.
+        """
+        notes_slide_part = NotesSlidePart.new(self.package, self)
+        self.relate_to(notes_slide_part, RT.NOTES_SLIDE)
+        return notes_slide_part
+
+
+class SlideLayoutPart(BaseSlidePart):
+    """Slide layout part.
+
+    Corresponds to package files ``ppt/slideLayouts/slideLayout[1-9][0-9]*.xml``.
+    """
+
+    @lazyproperty
+    def slide_layout(self):
+        """
+        The |SlideLayout| object representing this part.
+        """
+        return SlideLayout(self._element, self)
+
+    @property
+    def slide_master(self) -> SlideMaster:
+        """Slide master from which this slide layout inherits properties."""
+        return self.part_related_by(RT.SLIDE_MASTER).slide_master
+
+
+class SlideMasterPart(BaseSlidePart):
+    """Slide master part.
+
+    Corresponds to package files ppt/slideMasters/slideMaster[1-9][0-9]*.xml.
+    """
+
+    def related_slide_layout(self, rId: str) -> SlideLayout:
+        """Return |SlideLayout| related to this slide-master by key `rId`."""
+        return self.related_part(rId).slide_layout
+
+    @lazyproperty
+    def slide_master(self):
+        """
+        The |SlideMaster| object representing this part.
+        """
+        return SlideMaster(self._element, self)
diff --git a/.venv/lib/python3.12/site-packages/pptx/presentation.py b/.venv/lib/python3.12/site-packages/pptx/presentation.py
new file mode 100644
index 00000000..a41bfd59
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/presentation.py
@@ -0,0 +1,113 @@
+"""Main presentation object."""
+
+from __future__ import annotations
+
+from typing import IO, TYPE_CHECKING, cast
+
+from pptx.shared import PartElementProxy
+from pptx.slide import SlideMasters, Slides
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.oxml.presentation import CT_Presentation, CT_SlideId
+    from pptx.parts.presentation import PresentationPart
+    from pptx.slide import NotesMaster, SlideLayouts
+    from pptx.util import Length
+
+
+class Presentation(PartElementProxy):
+    """PresentationML (PML) presentation.
+
+    Not intended to be constructed directly. Use :func:`pptx.Presentation` to open or
+    create a presentation.
+    """
+
+    _element: CT_Presentation
+    part: PresentationPart  # pyright: ignore[reportIncompatibleMethodOverride]
+
+    @property
+    def core_properties(self):
+        """|CoreProperties| instance for this presentation.
+
+        Provides read/write access to the Dublin Core document properties for the presentation.
+        """
+        return self.part.core_properties
+
+    @property
+    def notes_master(self) -> NotesMaster:
+        """Instance of |NotesMaster| for this presentation.
+
+        If the presentation does not have a notes master, one is created from a default template
+        and returned. The same single instance is returned on each call.
+        """
+        return self.part.notes_master
+
+    def save(self, file: str | IO[bytes]):
+        """Writes this presentation to `file`.
+
+        `file` can be either a file-path or a file-like object open for writing bytes.
+        """
+        self.part.save(file)
+
+    @property
+    def slide_height(self) -> Length | None:
+        """Height of slides in this presentation, in English Metric Units (EMU).
+
+        Returns |None| if no slide width is defined. Read/write.
+        """
+        sldSz = self._element.sldSz
+        if sldSz is None:
+            return None
+        return sldSz.cy
+
+    @slide_height.setter
+    def slide_height(self, height: Length):
+        sldSz = self._element.get_or_add_sldSz()
+        sldSz.cy = height
+
+    @property
+    def slide_layouts(self) -> SlideLayouts:
+        """|SlideLayouts| collection belonging to the first |SlideMaster| of this presentation.
+
+        A presentation can have more than one slide master and each master will have its own set
+        of layouts. This property is a convenience for the common case where the presentation has
+        only a single slide master.
+        """
+        return self.slide_masters[0].slide_layouts
+
+    @property
+    def slide_master(self):
+        """
+        First |SlideMaster| object belonging to this presentation. Typically,
+        presentations have only a single slide master. This property provides
+        simpler access in that common case.
+        """
+        return self.slide_masters[0]
+
+    @lazyproperty
+    def slide_masters(self) -> SlideMasters:
+        """|SlideMasters| collection of slide-masters belonging to this presentation."""
+        return SlideMasters(self._element.get_or_add_sldMasterIdLst(), self)
+
+    @property
+    def slide_width(self):
+        """
+        Width of slides in this presentation, in English Metric Units (EMU).
+        Returns |None| if no slide width is defined. Read/write.
+        """
+        sldSz = self._element.sldSz
+        if sldSz is None:
+            return None
+        return sldSz.cx
+
+    @slide_width.setter
+    def slide_width(self, width: Length):
+        sldSz = self._element.get_or_add_sldSz()
+        sldSz.cx = width
+
+    @lazyproperty
+    def slides(self):
+        """|Slides| object containing the slides in this presentation."""
+        sldIdLst = self._element.get_or_add_sldIdLst()
+        self.part.rename_slide_parts([cast("CT_SlideId", sldId).rId for sldId in sldIdLst])
+        return Slides(sldIdLst, self)
diff --git a/.venv/lib/python3.12/site-packages/pptx/py.typed b/.venv/lib/python3.12/site-packages/pptx/py.typed
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/py.typed
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/__init__.py b/.venv/lib/python3.12/site-packages/pptx/shapes/__init__.py
new file mode 100644
index 00000000..332109a3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/__init__.py
@@ -0,0 +1,26 @@
+"""Objects used across sub-package."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from pptx.opc.package import XmlPart
+    from pptx.types import ProvidesPart
+
+
+class Subshape(object):
+    """Provides access to the containing part for drawing elements that occur below a shape.
+
+    Access to the part is required for example to add or drop a relationship. Provides
+    `self._parent` attribute to subclasses.
+    """
+
+    def __init__(self, parent: ProvidesPart):
+        super(Subshape, self).__init__()
+        self._parent = parent
+
+    @property
+    def part(self) -> XmlPart:
+        """The package part containing this object."""
+        return self._parent.part
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/autoshape.py b/.venv/lib/python3.12/site-packages/pptx/shapes/autoshape.py
new file mode 100644
index 00000000..c7f8cd93
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/autoshape.py
@@ -0,0 +1,355 @@
+"""Autoshape-related objects such as Shape and Adjustment."""
+
+from __future__ import annotations
+
+from numbers import Number
+from typing import TYPE_CHECKING, Iterable
+from xml.sax import saxutils
+
+from pptx.dml.fill import FillFormat
+from pptx.dml.line import LineFormat
+from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, MSO_SHAPE_TYPE
+from pptx.shapes.base import BaseShape
+from pptx.spec import autoshape_types
+from pptx.text.text import TextFrame
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.oxml.shapes.autoshape import CT_GeomGuide, CT_PresetGeometry2D, CT_Shape
+    from pptx.spec import AdjustmentValue
+    from pptx.types import ProvidesPart
+
+
+class Adjustment:
+    """An adjustment value for an autoshape.
+
+    An adjustment value corresponds to the position of an adjustment handle on an auto shape.
+    Adjustment handles are the small yellow diamond-shaped handles that appear on certain auto
+    shapes and allow the outline of the shape to be adjusted. For example, a rounded rectangle has
+    an adjustment handle that allows the radius of its corner rounding to be adjusted.
+
+    Values are |float| and generally range from 0.0 to 1.0, although the value can be negative or
+    greater than 1.0 in certain circumstances.
+    """
+
+    def __init__(self, name: str, def_val: int, actual: int | None = None):
+        super(Adjustment, self).__init__()
+        self.name = name
+        self.def_val = def_val
+        self.actual = actual
+
+    @property
+    def effective_value(self) -> float:
+        """Read/write |float| representing normalized adjustment value for this adjustment.
+
+        Actual values are a large-ish integer expressed in shape coordinates, nominally between 0
+        and 100,000. The effective value is normalized to a corresponding value nominally between
+        0.0 and 1.0. Intuitively this represents the proportion of the width or height of the shape
+        at which the adjustment value is located from its starting point. For simple shapes such as
+        a rounded rectangle, this intuitive correspondence holds. For more complicated shapes and
+        at more extreme shape proportions (e.g. width is much greater than height), the value can
+        become negative or greater than 1.0.
+        """
+        raw_value = self.actual if self.actual is not None else self.def_val
+        return self._normalize(raw_value)
+
+    @effective_value.setter
+    def effective_value(self, value: float):
+        if not isinstance(value, Number):
+            raise ValueError(f"adjustment value must be numeric, got {repr(value)}")
+        self.actual = self._denormalize(value)
+
+    @staticmethod
+    def _denormalize(value: float) -> int:
+        """Return integer corresponding to normalized `raw_value` on unit basis of 100,000.
+
+        See Adjustment.normalize for additional details.
+        """
+        return int(value * 100000.0)
+
+    @staticmethod
+    def _normalize(raw_value: int) -> float:
+        """Return normalized value for `raw_value`.
+
+        A normalized value is a |float| between 0.0 and 1.0 for nominal raw values between 0 and
+        100,000. Raw values less than 0 and greater than 100,000 are valid and return values
+        calculated on the same unit basis of 100,000.
+        """
+        return raw_value / 100000.0
+
+    @property
+    def val(self) -> int:
+        """Denormalized effective value.
+
+        Expressed in shape coordinates, this is suitable for using in the XML.
+        """
+        return self.actual if self.actual is not None else self.def_val
+
+
+class AdjustmentCollection:
+    """Sequence of |Adjustment| instances for an auto shape.
+
+    Each represents an available adjustment for a shape of its type. Supports `len()` and indexed
+    access, e.g. `shape.adjustments[1] = 0.15`.
+    """
+
+    def __init__(self, prstGeom: CT_PresetGeometry2D):
+        super(AdjustmentCollection, self).__init__()
+        self._adjustments_ = self._initialized_adjustments(prstGeom)
+        self._prstGeom = prstGeom
+
+    def __getitem__(self, idx: int) -> float:
+        """Provides indexed access, (e.g. 'adjustments[9]')."""
+        return self._adjustments_[idx].effective_value
+
+    def __setitem__(self, idx: int, value: float):
+        """Provides item assignment via an indexed expression, e.g. `adjustments[9] = 999.9`.
+
+        Causes all adjustment values in collection to be written to the XML.
+        """
+        self._adjustments_[idx].effective_value = value
+        self._rewrite_guides()
+
+    def _initialized_adjustments(self, prstGeom: CT_PresetGeometry2D | None) -> list[Adjustment]:
+        """Return an initialized list of adjustment values based on the contents of `prstGeom`."""
+        if prstGeom is None:
+            return []
+        davs = AutoShapeType.default_adjustment_values(prstGeom.prst)
+        adjustments = [Adjustment(name, def_val) for name, def_val in davs]
+        self._update_adjustments_with_actuals(adjustments, prstGeom.gd_lst)
+        return adjustments
+
+    def _rewrite_guides(self):
+        """Write `a:gd` elements to the XML, one for each adjustment value.
+
+        Any existing guide elements are overwritten.
+        """
+        guides = [(adj.name, adj.val) for adj in self._adjustments_]
+        self._prstGeom.rewrite_guides(guides)
+
+    @staticmethod
+    def _update_adjustments_with_actuals(
+        adjustments: Iterable[Adjustment], guides: Iterable[CT_GeomGuide]
+    ):
+        """Update |Adjustment| instances in `adjustments` with actual values held in `guides`.
+
+        `guides` is a list of `a:gd` elements. Guides with a name that does not match an adjustment
+        object are skipped.
+        """
+        adjustments_by_name = dict((adj.name, adj) for adj in adjustments)
+        for gd in guides:
+            name = gd.name
+            actual = int(gd.fmla[4:])
+            try:
+                adjustment = adjustments_by_name[name]
+            except KeyError:
+                continue
+            adjustment.actual = actual
+        return
+
+    @property
+    def _adjustments(self) -> tuple[Adjustment, ...]:
+        """Sequence of |Adjustment| objects contained in collection."""
+        return tuple(self._adjustments_)
+
+    def __len__(self):
+        """Implement built-in function len()"""
+        return len(self._adjustments_)
+
+
+class AutoShapeType:
+    """Provides access to metadata for an auto-shape of type identified by `autoshape_type_id`.
+
+    Instances are cached, so no more than one instance for a particular auto shape type is in
+    memory.
+
+    Instances provide the following attributes:
+
+    .. attribute:: autoshape_type_id
+
+       Integer uniquely identifying this auto shape type. Corresponds to a
+       value in `pptx.constants.MSO` like `MSO_SHAPE.ROUNDED_RECTANGLE`.
+
+    .. attribute:: basename
+
+       Base part of shape name for auto shapes of this type, e.g. `Rounded
+       Rectangle` becomes `Rounded Rectangle 99` when the distinguishing
+       integer is added to the shape name.
+
+    .. attribute:: prst
+
+       String identifier for this auto shape type used in the `a:prstGeom`
+       element.
+
+    """
+
+    _instances: dict[MSO_AUTO_SHAPE_TYPE, AutoShapeType] = {}
+
+    def __new__(cls, autoshape_type_id: MSO_AUTO_SHAPE_TYPE) -> AutoShapeType:
+        """Only create new instance on first call for content_type.
+
+        After that, use cached instance.
+        """
+        # -- if there's not a matching instance in the cache, create one --
+        if autoshape_type_id not in cls._instances:
+            inst = super(AutoShapeType, cls).__new__(cls)
+            cls._instances[autoshape_type_id] = inst
+        # -- return the instance; note that __init__() gets called either way --
+        return cls._instances[autoshape_type_id]
+
+    def __init__(self, autoshape_type_id: MSO_AUTO_SHAPE_TYPE):
+        """Initialize attributes from constant values in `pptx.spec`."""
+        # -- skip loading if this instance is from the cache --
+        if hasattr(self, "_loaded"):
+            return
+        # -- raise on bad autoshape_type_id --
+        if autoshape_type_id not in autoshape_types:
+            raise KeyError(
+                "no autoshape type with id '%s' in pptx.spec.autoshape_types" % autoshape_type_id
+            )
+        # -- otherwise initialize new instance --
+        autoshape_type = autoshape_types[autoshape_type_id]
+        self._autoshape_type_id = autoshape_type_id
+        self._basename = autoshape_type["basename"]
+        self._loaded = True
+
+    @property
+    def autoshape_type_id(self) -> MSO_AUTO_SHAPE_TYPE:
+        """MSO_AUTO_SHAPE_TYPE enumeration member identifying this auto shape type."""
+        return self._autoshape_type_id
+
+    @property
+    def basename(self) -> str:
+        """Base of shape name for this auto shape type.
+
+        A shape name is like "Rounded Rectangle 7" and appears as an XML attribute for example at
+        `p:sp/p:nvSpPr/p:cNvPr{name}`. This basename value is the name less the distinguishing
+        integer. This value is escaped because at least one autoshape-type name includes double
+        quotes ('"No" Symbol').
+        """
+        return saxutils.escape(self._basename, {'"': "&quot;"})
+
+    @classmethod
+    def default_adjustment_values(cls, prst: MSO_AUTO_SHAPE_TYPE) -> tuple[AdjustmentValue, ...]:
+        """Sequence of (name, value) pair adjustment value defaults for `prst` autoshape-type."""
+        return autoshape_types[prst]["avLst"]
+
+    @classmethod
+    def id_from_prst(cls, prst: str) -> MSO_AUTO_SHAPE_TYPE:
+        """Select auto shape type with matching `prst`.
+
+        e.g. `MSO_SHAPE.RECTANGLE` corresponding to preset geometry keyword `"rect"`.
+        """
+        return MSO_AUTO_SHAPE_TYPE.from_xml(prst)
+
+    @property
+    def prst(self):
+        """
+        Preset geometry identifier string for this auto shape. Used in the
+        `prst` attribute of `a:prstGeom` element to specify the geometry
+        to be used in rendering the shape, for example `'roundRect'`.
+        """
+        return MSO_AUTO_SHAPE_TYPE.to_xml(self._autoshape_type_id)
+
+
+class Shape(BaseShape):
+    """A shape that can appear on a slide.
+
+    Corresponds to the `p:sp` element that can appear in any of the slide-type parts
+    (slide, slideLayout, slideMaster, notesPage, notesMaster, handoutMaster).
+    """
+
+    def __init__(self, sp: CT_Shape, parent: ProvidesPart):
+        super(Shape, self).__init__(sp, parent)
+        self._sp = sp
+
+    @lazyproperty
+    def adjustments(self) -> AdjustmentCollection:
+        """Read-only reference to |AdjustmentCollection| instance for this shape."""
+        return AdjustmentCollection(self._sp.prstGeom)
+
+    @property
+    def auto_shape_type(self):
+        """Enumeration value identifying the type of this auto shape.
+
+        Like `MSO_SHAPE.ROUNDED_RECTANGLE`. Raises |ValueError| if this shape is not an auto shape.
+        """
+        if not self._sp.is_autoshape:
+            raise ValueError("shape is not an auto shape")
+        return self._sp.prst
+
+    @lazyproperty
+    def fill(self):
+        """|FillFormat| instance for this shape.
+
+        Provides access to fill properties such as fill color.
+        """
+        return FillFormat.from_fill_parent(self._sp.spPr)
+
+    def get_or_add_ln(self):
+        """Return the `a:ln` element containing the line format properties XML for this shape."""
+        return self._sp.get_or_add_ln()
+
+    @property
+    def has_text_frame(self) -> bool:
+        """|True| if this shape can contain text. Always |True| for an AutoShape."""
+        return True
+
+    @lazyproperty
+    def line(self):
+        """|LineFormat| instance for this shape.
+
+        Provides access to line properties such as line color.
+        """
+        return LineFormat(self)
+
+    @property
+    def ln(self):
+        """The `a:ln` element containing the line format properties such as line color and width.
+
+        |None| if no `a:ln` element is present.
+        """
+        return self._sp.ln
+
+    @property
+    def shape_type(self) -> MSO_SHAPE_TYPE:
+        """Unique integer identifying the type of this shape, like `MSO_SHAPE_TYPE.TEXT_BOX`."""
+        if self.is_placeholder:
+            return MSO_SHAPE_TYPE.PLACEHOLDER
+        if self._sp.has_custom_geometry:
+            return MSO_SHAPE_TYPE.FREEFORM
+        if self._sp.is_autoshape:
+            return MSO_SHAPE_TYPE.AUTO_SHAPE
+        if self._sp.is_textbox:
+            return MSO_SHAPE_TYPE.TEXT_BOX
+        raise NotImplementedError("Shape instance of unrecognized shape type")
+
+    @property
+    def text(self) -> str:
+        """Read/write. Text in shape as a single string.
+
+        The returned string will contain a newline character (`"\\n"`) separating each paragraph
+        and a vertical-tab (`"\\v"`) character for each line break (soft carriage return) in the
+        shape's text.
+
+        Assignment to `text` replaces any text previously contained in the shape, along with any
+        paragraph or font formatting applied to it. A newline character (`"\\n"`) in the assigned
+        text causes a new paragraph to be started. A vertical-tab (`"\\v"`) character in the
+        assigned text causes a line-break (soft carriage-return) to be inserted. (The vertical-tab
+        character appears in clipboard text copied from PowerPoint as its str encoding of
+        line-breaks.)
+        """
+        return self.text_frame.text
+
+    @text.setter
+    def text(self, text: str):
+        self.text_frame.text = text
+
+    @property
+    def text_frame(self):
+        """|TextFrame| instance for this shape.
+
+        Contains the text of the shape and provides access to text formatting properties.
+        """
+        txBody = self._sp.get_or_add_txBody()
+        return TextFrame(txBody, self)
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/base.py b/.venv/lib/python3.12/site-packages/pptx/shapes/base.py
new file mode 100644
index 00000000..75123502
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/base.py
@@ -0,0 +1,244 @@
+"""Base shape-related objects such as BaseShape."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+from pptx.action import ActionSetting
+from pptx.dml.effect import ShadowFormat
+from pptx.shared import ElementProxy
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER
+    from pptx.oxml.shapes import ShapeElement
+    from pptx.oxml.shapes.shared import CT_Placeholder
+    from pptx.parts.slide import BaseSlidePart
+    from pptx.types import ProvidesPart
+    from pptx.util import Length
+
+
+class BaseShape(object):
+    """Base class for shape objects.
+
+    Subclasses include |Shape|, |Picture|, and |GraphicFrame|.
+    """
+
+    def __init__(self, shape_elm: ShapeElement, parent: ProvidesPart):
+        super().__init__()
+        self._element = shape_elm
+        self._parent = parent
+
+    def __eq__(self, other: object) -> bool:
+        """|True| if this shape object proxies the same element as *other*.
+
+        Equality for proxy objects is defined as referring to the same XML element, whether or not
+        they are the same proxy object instance.
+        """
+        if not isinstance(other, BaseShape):
+            return False
+        return self._element is other._element
+
+    def __ne__(self, other: object) -> bool:
+        if not isinstance(other, BaseShape):
+            return True
+        return self._element is not other._element
+
+    @lazyproperty
+    def click_action(self) -> ActionSetting:
+        """|ActionSetting| instance providing access to click behaviors.
+
+        Click behaviors are hyperlink-like behaviors including jumping to a hyperlink (web page)
+        or to another slide in the presentation. The click action is that defined on the overall
+        shape, not a run of text within the shape. An |ActionSetting| object is always returned,
+        even when no click behavior is defined on the shape.
+        """
+        cNvPr = self._element._nvXxPr.cNvPr  # pyright: ignore[reportPrivateUsage]
+        return ActionSetting(cNvPr, self)
+
+    @property
+    def element(self) -> ShapeElement:
+        """`lxml` element for this shape, e.g. a CT_Shape instance.
+
+        Note that manipulating this element improperly can produce an invalid presentation file.
+        Make sure you know what you're doing if you use this to change the underlying XML.
+        """
+        return self._element
+
+    @property
+    def has_chart(self) -> bool:
+        """|True| if this shape is a graphic frame containing a chart object.
+
+        |False| otherwise. When |True|, the chart object can be accessed using the ``.chart``
+        property.
+        """
+        # This implementation is unconditionally False, the True version is
+        # on GraphicFrame subclass.
+        return False
+
+    @property
+    def has_table(self) -> bool:
+        """|True| if this shape is a graphic frame containing a table object.
+
+        |False| otherwise. When |True|, the table object can be accessed using the ``.table``
+        property.
+        """
+        # This implementation is unconditionally False, the True version is
+        # on GraphicFrame subclass.
+        return False
+
+    @property
+    def has_text_frame(self) -> bool:
+        """|True| if this shape can contain text."""
+        # overridden on Shape to return True. Only <p:sp> has text frame
+        return False
+
+    @property
+    def height(self) -> Length:
+        """Read/write. Integer distance between top and bottom extents of shape in EMUs."""
+        return self._element.cy
+
+    @height.setter
+    def height(self, value: Length):
+        self._element.cy = value
+
+    @property
+    def is_placeholder(self) -> bool:
+        """True if this shape is a placeholder.
+
+        A shape is a placeholder if it has a <p:ph> element.
+        """
+        return self._element.has_ph_elm
+
+    @property
+    def left(self) -> Length:
+        """Integer distance of the left edge of this shape from the left edge of the slide.
+
+        Read/write. Expressed in English Metric Units (EMU)
+        """
+        return self._element.x
+
+    @left.setter
+    def left(self, value: Length):
+        self._element.x = value
+
+    @property
+    def name(self) -> str:
+        """Name of this shape, e.g. 'Picture 7'."""
+        return self._element.shape_name
+
+    @name.setter
+    def name(self, value: str):
+        self._element._nvXxPr.cNvPr.name = value  # pyright: ignore[reportPrivateUsage]
+
+    @property
+    def part(self) -> BaseSlidePart:
+        """The package part containing this shape.
+
+        A |BaseSlidePart| subclass in this case. Access to a slide part should only be required if
+        you are extending the behavior of |pp| API objects.
+        """
+        return cast("BaseSlidePart", self._parent.part)
+
+    @property
+    def placeholder_format(self) -> _PlaceholderFormat:
+        """Provides access to placeholder-specific properties such as placeholder type.
+
+        Raises |ValueError| on access if the shape is not a placeholder.
+        """
+        ph = self._element.ph
+        if ph is None:
+            raise ValueError("shape is not a placeholder")
+        return _PlaceholderFormat(ph)
+
+    @property
+    def rotation(self) -> float:
+        """Degrees of clockwise rotation.
+
+        Read/write float. Negative values can be assigned to indicate counter-clockwise rotation,
+        e.g. assigning -45.0 will change setting to 315.0.
+        """
+        return self._element.rot
+
+    @rotation.setter
+    def rotation(self, value: float):
+        self._element.rot = value
+
+    @lazyproperty
+    def shadow(self) -> ShadowFormat:
+        """|ShadowFormat| object providing access to shadow for this shape.
+
+        A |ShadowFormat| object is always returned, even when no shadow is
+        explicitly defined on this shape (i.e. it inherits its shadow
+        behavior).
+        """
+        return ShadowFormat(self._element.spPr)
+
+    @property
+    def shape_id(self) -> int:
+        """Read-only positive integer identifying this shape.
+
+        The id of a shape is unique among all shapes on a slide.
+        """
+        return self._element.shape_id
+
+    @property
+    def shape_type(self) -> MSO_SHAPE_TYPE:
+        """A member of MSO_SHAPE_TYPE classifying this shape by type.
+
+        Like ``MSO_SHAPE_TYPE.CHART``. Must be implemented by subclasses.
+        """
+        raise NotImplementedError(f"{type(self).__name__} does not implement `.shape_type`")
+
+    @property
+    def top(self) -> Length:
+        """Distance from the top edge of the slide to the top edge of this shape.
+
+        Read/write. Expressed in English Metric Units (EMU)
+        """
+        return self._element.y
+
+    @top.setter
+    def top(self, value: Length):
+        self._element.y = value
+
+    @property
+    def width(self) -> Length:
+        """Distance between left and right extents of this shape.
+
+        Read/write. Expressed in English Metric Units (EMU).
+        """
+        return self._element.cx
+
+    @width.setter
+    def width(self, value: Length):
+        self._element.cx = value
+
+
+class _PlaceholderFormat(ElementProxy):
+    """Provides properties specific to placeholders, such as the placeholder type.
+
+    Accessed via the :attr:`~.BaseShape.placeholder_format` property of a placeholder shape,
+    """
+
+    def __init__(self, element: CT_Placeholder):
+        super().__init__(element)
+        self._ph = element
+
+    @property
+    def element(self) -> CT_Placeholder:
+        """The `p:ph` element proxied by this object."""
+        return self._ph
+
+    @property
+    def idx(self) -> int:
+        """Integer placeholder 'idx' attribute."""
+        return self._ph.idx
+
+    @property
+    def type(self) -> PP_PLACEHOLDER:
+        """Placeholder type.
+
+        A member of the :ref:`PpPlaceholderType` enumeration, e.g. PP_PLACEHOLDER.CHART
+        """
+        return self._ph.type
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/connector.py b/.venv/lib/python3.12/site-packages/pptx/shapes/connector.py
new file mode 100644
index 00000000..070b080d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/connector.py
@@ -0,0 +1,297 @@
+"""Connector (line) shape and related objects.
+
+A connector is a line shape having end-points that can be connected to other
+objects (but not to other connectors). A connector can be straight, have
+elbows, or can be curved.
+"""
+
+from __future__ import annotations
+
+from pptx.dml.line import LineFormat
+from pptx.enum.shapes import MSO_SHAPE_TYPE
+from pptx.shapes.base import BaseShape
+from pptx.util import Emu, lazyproperty
+
+
+class Connector(BaseShape):
+    """Connector (line) shape.
+
+    A connector is a linear shape having end-points that can be connected to
+    other objects (but not to other connectors). A connector can be straight,
+    have elbows, or can be curved.
+    """
+
+    def begin_connect(self, shape, cxn_pt_idx):
+        """
+        **EXPERIMENTAL** - *The current implementation only works properly
+        with rectangular shapes, such as pictures and rectangles. Use with
+        other shape types may cause unexpected visual alignment of the
+        connected end-point and could lead to a load error if cxn_pt_idx
+        exceeds the connection point count available on the connected shape.
+        That said, a quick test should reveal what to expect when using this
+        method with other shape types.*
+
+        Connect the beginning of this connector to *shape* at the connection
+        point specified by *cxn_pt_idx*. Each shape has zero or more
+        connection points and they are identified by index, starting with 0.
+        Generally, the first connection point of a shape is at the top center
+        of its bounding box and numbering proceeds counter-clockwise from
+        there. However this is only a convention and may vary, especially
+        with non built-in shapes.
+        """
+        self._connect_begin_to(shape, cxn_pt_idx)
+        self._move_begin_to_cxn(shape, cxn_pt_idx)
+
+    @property
+    def begin_x(self):
+        """
+        Return the X-position of the begin point of this connector, in
+        English Metric Units (as a |Length| object).
+        """
+        cxnSp = self._element
+        x, cx, flipH = cxnSp.x, cxnSp.cx, cxnSp.flipH
+        begin_x = x + cx if flipH else x
+        return Emu(begin_x)
+
+    @begin_x.setter
+    def begin_x(self, value):
+        cxnSp = self._element
+        x, cx, flipH, new_x = cxnSp.x, cxnSp.cx, cxnSp.flipH, int(value)
+
+        if flipH:
+            old_x = x + cx
+            dx = abs(new_x - old_x)
+            if new_x >= old_x:
+                cxnSp.cx = cx + dx
+            elif dx <= cx:
+                cxnSp.cx = cx - dx
+            else:
+                cxnSp.flipH = False
+                cxnSp.x = new_x
+                cxnSp.cx = dx - cx
+        else:
+            dx = abs(new_x - x)
+            if new_x <= x:
+                cxnSp.x = new_x
+                cxnSp.cx = cx + dx
+            elif dx <= cx:
+                cxnSp.x = new_x
+                cxnSp.cx = cx - dx
+            else:
+                cxnSp.flipH = True
+                cxnSp.x = x + cx
+                cxnSp.cx = dx - cx
+
+    @property
+    def begin_y(self):
+        """
+        Return the Y-position of the begin point of this connector, in
+        English Metric Units (as a |Length| object).
+        """
+        cxnSp = self._element
+        y, cy, flipV = cxnSp.y, cxnSp.cy, cxnSp.flipV
+        begin_y = y + cy if flipV else y
+        return Emu(begin_y)
+
+    @begin_y.setter
+    def begin_y(self, value):
+        cxnSp = self._element
+        y, cy, flipV, new_y = cxnSp.y, cxnSp.cy, cxnSp.flipV, int(value)
+
+        if flipV:
+            old_y = y + cy
+            dy = abs(new_y - old_y)
+            if new_y >= old_y:
+                cxnSp.cy = cy + dy
+            elif dy <= cy:
+                cxnSp.cy = cy - dy
+            else:
+                cxnSp.flipV = False
+                cxnSp.y = new_y
+                cxnSp.cy = dy - cy
+        else:
+            dy = abs(new_y - y)
+            if new_y <= y:
+                cxnSp.y = new_y
+                cxnSp.cy = cy + dy
+            elif dy <= cy:
+                cxnSp.y = new_y
+                cxnSp.cy = cy - dy
+            else:
+                cxnSp.flipV = True
+                cxnSp.y = y + cy
+                cxnSp.cy = dy - cy
+
+    def end_connect(self, shape, cxn_pt_idx):
+        """
+        **EXPERIMENTAL** - *The current implementation only works properly
+        with rectangular shapes, such as pictures and rectangles. Use with
+        other shape types may cause unexpected visual alignment of the
+        connected end-point and could lead to a load error if cxn_pt_idx
+        exceeds the connection point count available on the connected shape.
+        That said, a quick test should reveal what to expect when using this
+        method with other shape types.*
+
+        Connect the ending of this connector to *shape* at the connection
+        point specified by *cxn_pt_idx*.
+        """
+        self._connect_end_to(shape, cxn_pt_idx)
+        self._move_end_to_cxn(shape, cxn_pt_idx)
+
+    @property
+    def end_x(self):
+        """
+        Return the X-position of the end point of this connector, in English
+        Metric Units (as a |Length| object).
+        """
+        cxnSp = self._element
+        x, cx, flipH = cxnSp.x, cxnSp.cx, cxnSp.flipH
+        end_x = x if flipH else x + cx
+        return Emu(end_x)
+
+    @end_x.setter
+    def end_x(self, value):
+        cxnSp = self._element
+        x, cx, flipH, new_x = cxnSp.x, cxnSp.cx, cxnSp.flipH, int(value)
+
+        if flipH:
+            dx = abs(new_x - x)
+            if new_x <= x:
+                cxnSp.x = new_x
+                cxnSp.cx = cx + dx
+            elif dx <= cx:
+                cxnSp.x = new_x
+                cxnSp.cx = cx - dx
+            else:
+                cxnSp.flipH = False
+                cxnSp.x = x + cx
+                cxnSp.cx = dx - cx
+        else:
+            old_x = x + cx
+            dx = abs(new_x - old_x)
+            if new_x >= old_x:
+                cxnSp.cx = cx + dx
+            elif dx <= cx:
+                cxnSp.cx = cx - dx
+            else:
+                cxnSp.flipH = True
+                cxnSp.x = new_x
+                cxnSp.cx = dx - cx
+
+    @property
+    def end_y(self):
+        """
+        Return the Y-position of the end point of this connector, in English
+        Metric Units (as a |Length| object).
+        """
+        cxnSp = self._element
+        y, cy, flipV = cxnSp.y, cxnSp.cy, cxnSp.flipV
+        end_y = y if flipV else y + cy
+        return Emu(end_y)
+
+    @end_y.setter
+    def end_y(self, value):
+        cxnSp = self._element
+        y, cy, flipV, new_y = cxnSp.y, cxnSp.cy, cxnSp.flipV, int(value)
+
+        if flipV:
+            dy = abs(new_y - y)
+            if new_y <= y:
+                cxnSp.y = new_y
+                cxnSp.cy = cy + dy
+            elif dy <= cy:
+                cxnSp.y = new_y
+                cxnSp.cy = cy - dy
+            else:
+                cxnSp.flipV = False
+                cxnSp.y = y + cy
+                cxnSp.cy = dy - cy
+        else:
+            old_y = y + cy
+            dy = abs(new_y - old_y)
+            if new_y >= old_y:
+                cxnSp.cy = cy + dy
+            elif dy <= cy:
+                cxnSp.cy = cy - dy
+            else:
+                cxnSp.flipV = True
+                cxnSp.y = new_y
+                cxnSp.cy = dy - cy
+
+    def get_or_add_ln(self):
+        """Helper method required by |LineFormat|."""
+        return self._element.spPr.get_or_add_ln()
+
+    @lazyproperty
+    def line(self):
+        """|LineFormat| instance for this connector.
+
+        Provides access to line properties such as line color, width, and
+        line style.
+        """
+        return LineFormat(self)
+
+    @property
+    def ln(self):
+        """Helper method required by |LineFormat|.
+
+        The ``<a:ln>`` element containing the line format properties such as
+        line color and width. |None| if no `<a:ln>` element is present.
+        """
+        return self._element.spPr.ln
+
+    @property
+    def shape_type(self):
+        """Member of `MSO_SHAPE_TYPE` identifying the type of this shape.
+
+        Unconditionally `MSO_SHAPE_TYPE.LINE` for a `Connector` object.
+        """
+        return MSO_SHAPE_TYPE.LINE
+
+    def _connect_begin_to(self, shape, cxn_pt_idx):
+        """
+        Add or update a stCxn element for this connector that connects its
+        begin point to the connection point of *shape* specified by
+        *cxn_pt_idx*.
+        """
+        cNvCxnSpPr = self._element.nvCxnSpPr.cNvCxnSpPr
+        stCxn = cNvCxnSpPr.get_or_add_stCxn()
+        stCxn.id = shape.shape_id
+        stCxn.idx = cxn_pt_idx
+
+    def _connect_end_to(self, shape, cxn_pt_idx):
+        """
+        Add or update an endCxn element for this connector that connects its
+        end point to the connection point of *shape* specified by
+        *cxn_pt_idx*.
+        """
+        cNvCxnSpPr = self._element.nvCxnSpPr.cNvCxnSpPr
+        endCxn = cNvCxnSpPr.get_or_add_endCxn()
+        endCxn.id = shape.shape_id
+        endCxn.idx = cxn_pt_idx
+
+    def _move_begin_to_cxn(self, shape, cxn_pt_idx):
+        """
+        Move the begin point of this connector to coordinates of the
+        connection point of *shape* specified by *cxn_pt_idx*.
+        """
+        x, y, cx, cy = shape.left, shape.top, shape.width, shape.height
+        self.begin_x, self.begin_y = {
+            0: (int(x + cx / 2), y),
+            1: (x, int(y + cy / 2)),
+            2: (int(x + cx / 2), y + cy),
+            3: (x + cx, int(y + cy / 2)),
+        }[cxn_pt_idx]
+
+    def _move_end_to_cxn(self, shape, cxn_pt_idx):
+        """
+        Move the end point of this connector to the coordinates of the
+        connection point of *shape* specified by *cxn_pt_idx*.
+        """
+        x, y, cx, cy = shape.left, shape.top, shape.width, shape.height
+        self.end_x, self.end_y = {
+            0: (int(x + cx / 2), y),
+            1: (x, int(y + cy / 2)),
+            2: (int(x + cx / 2), y + cy),
+            3: (x + cx, int(y + cy / 2)),
+        }[cxn_pt_idx]
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/freeform.py b/.venv/lib/python3.12/site-packages/pptx/shapes/freeform.py
new file mode 100644
index 00000000..afe87385
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/freeform.py
@@ -0,0 +1,337 @@
+"""Objects related to construction of freeform shapes."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Iterable, Iterator, Sequence
+
+from pptx.util import Emu, lazyproperty
+
+if TYPE_CHECKING:
+    from typing_extensions import TypeAlias
+
+    from pptx.oxml.shapes.autoshape import (
+        CT_Path2D,
+        CT_Path2DClose,
+        CT_Path2DLineTo,
+        CT_Path2DMoveTo,
+        CT_Shape,
+    )
+    from pptx.shapes.shapetree import _BaseGroupShapes  # pyright: ignore[reportPrivateUsage]
+    from pptx.util import Length
+
+CT_DrawingOperation: TypeAlias = "CT_Path2DClose | CT_Path2DLineTo | CT_Path2DMoveTo"
+DrawingOperation: TypeAlias = "_LineSegment | _MoveTo | _Close"
+
+
+class FreeformBuilder(Sequence[DrawingOperation]):
+    """Allows a freeform shape to be specified and created.
+
+    The initial pen position is provided on construction. From there, drawing proceeds using
+    successive calls to draw line segments. The freeform shape may be closed by calling the
+    :meth:`close` method.
+
+    A shape may have more than one contour, in which case overlapping areas are "subtracted". A
+    contour is a sequence of line segments beginning with a "move-to" operation. A move-to
+    operation is automatically inserted in each new freeform; additional move-to ops can be
+    inserted with the `.move_to()` method.
+    """
+
+    def __init__(
+        self,
+        shapes: _BaseGroupShapes,
+        start_x: Length,
+        start_y: Length,
+        x_scale: float,
+        y_scale: float,
+    ):
+        super(FreeformBuilder, self).__init__()
+        self._shapes = shapes
+        self._start_x = start_x
+        self._start_y = start_y
+        self._x_scale = x_scale
+        self._y_scale = y_scale
+
+    def __getitem__(  # pyright: ignore[reportIncompatibleMethodOverride]
+        self, idx: int
+    ) -> DrawingOperation:
+        return self._drawing_operations.__getitem__(idx)
+
+    def __iter__(self) -> Iterator[DrawingOperation]:
+        return self._drawing_operations.__iter__()
+
+    def __len__(self):
+        return self._drawing_operations.__len__()
+
+    @classmethod
+    def new(
+        cls,
+        shapes: _BaseGroupShapes,
+        start_x: float,
+        start_y: float,
+        x_scale: float,
+        y_scale: float,
+    ):
+        """Return a new |FreeformBuilder| object.
+
+        The initial pen location is specified (in local coordinates) by
+        (`start_x`, `start_y`).
+        """
+        return cls(shapes, Emu(int(round(start_x))), Emu(int(round(start_y))), x_scale, y_scale)
+
+    def add_line_segments(self, vertices: Iterable[tuple[float, float]], close: bool = True):
+        """Add a straight line segment to each point in `vertices`.
+
+        `vertices` must be an iterable of (x, y) pairs (2-tuples). Each x and y value is rounded
+        to the nearest integer before use. The optional `close` parameter determines whether the
+        resulting contour is `closed` or left `open`.
+
+        Returns this |FreeformBuilder| object so it can be used in chained calls.
+        """
+        for x, y in vertices:
+            self._add_line_segment(x, y)
+        if close:
+            self._add_close()
+        return self
+
+    def convert_to_shape(self, origin_x: Length = Emu(0), origin_y: Length = Emu(0)):
+        """Return new freeform shape positioned relative to specified offset.
+
+        `origin_x` and `origin_y` locate the origin of the local coordinate system in slide
+        coordinates (EMU), perhaps most conveniently by use of a |Length| object.
+
+        Note that this method may be called more than once to add multiple shapes of the same
+        geometry in different locations on the slide.
+        """
+        sp = self._add_freeform_sp(origin_x, origin_y)
+        path = self._start_path(sp)
+        for drawing_operation in self:
+            drawing_operation.apply_operation_to(path)
+        return self._shapes._shape_factory(sp)  # pyright: ignore[reportPrivateUsage]
+
+    def move_to(self, x: float, y: float):
+        """Move pen to (x, y) (local coordinates) without drawing line.
+
+        Returns this |FreeformBuilder| object so it can be used in chained calls.
+        """
+        self._drawing_operations.append(_MoveTo.new(self, x, y))
+        return self
+
+    @property
+    def shape_offset_x(self) -> Length:
+        """Return x distance of shape origin from local coordinate origin.
+
+        The returned integer represents the leftmost extent of the freeform shape, in local
+        coordinates. Note that the bounding box of the shape need not start at the local origin.
+        """
+        min_x = self._start_x
+        for drawing_operation in self:
+            if isinstance(drawing_operation, _Close):
+                continue
+            min_x = min(min_x, drawing_operation.x)
+        return Emu(min_x)
+
+    @property
+    def shape_offset_y(self) -> Length:
+        """Return y distance of shape origin from local coordinate origin.
+
+        The returned integer represents the topmost extent of the freeform shape, in local
+        coordinates. Note that the bounding box of the shape need not start at the local origin.
+        """
+        min_y = self._start_y
+        for drawing_operation in self:
+            if isinstance(drawing_operation, _Close):
+                continue
+            min_y = min(min_y, drawing_operation.y)
+        return Emu(min_y)
+
+    def _add_close(self):
+        """Add a close |_Close| operation to the drawing sequence."""
+        self._drawing_operations.append(_Close.new())
+
+    def _add_freeform_sp(self, origin_x: Length, origin_y: Length):
+        """Add a freeform `p:sp` element having no drawing elements.
+
+        `origin_x` and `origin_y` are specified in slide coordinates, and represent the location
+        of the local coordinates origin on the slide.
+        """
+        spTree = self._shapes._spTree  # pyright: ignore[reportPrivateUsage]
+        return spTree.add_freeform_sp(
+            origin_x + self._left, origin_y + self._top, self._width, self._height
+        )
+
+    def _add_line_segment(self, x: float, y: float) -> None:
+        """Add a |_LineSegment| operation to the drawing sequence."""
+        self._drawing_operations.append(_LineSegment.new(self, x, y))
+
+    @lazyproperty
+    def _drawing_operations(self) -> list[DrawingOperation]:
+        """Return the sequence of drawing operation objects for freeform."""
+        return []
+
+    @property
+    def _dx(self) -> Length:
+        """Return width of this shape's path in local units."""
+        min_x = max_x = self._start_x
+        for drawing_operation in self:
+            if isinstance(drawing_operation, _Close):
+                continue
+            min_x = min(min_x, drawing_operation.x)
+            max_x = max(max_x, drawing_operation.x)
+        return Emu(max_x - min_x)
+
+    @property
+    def _dy(self) -> Length:
+        """Return integer height of this shape's path in local units."""
+        min_y = max_y = self._start_y
+        for drawing_operation in self:
+            if isinstance(drawing_operation, _Close):
+                continue
+            min_y = min(min_y, drawing_operation.y)
+            max_y = max(max_y, drawing_operation.y)
+        return Emu(max_y - min_y)
+
+    @property
+    def _height(self):
+        """Return vertical size of this shape's path in slide coordinates.
+
+        This value is based on the actual extents of the shape and does not include any
+        positioning offset.
+        """
+        return int(round(self._dy * self._y_scale))
+
+    @property
+    def _left(self):
+        """Return leftmost extent of this shape's path in slide coordinates.
+
+        Note that this value does not include any positioning offset; it assumes the drawing
+        (local) coordinate origin is at (0, 0) on the slide.
+        """
+        return int(round(self.shape_offset_x * self._x_scale))
+
+    def _local_to_shape(self, local_x: Length, local_y: Length) -> tuple[Length, Length]:
+        """Translate local coordinates point to shape coordinates.
+
+        Shape coordinates have the same unit as local coordinates, but are offset such that the
+        origin of the shape coordinate system (0, 0) is located at the top-left corner of the
+        shape bounding box.
+        """
+        return Emu(local_x - self.shape_offset_x), Emu(local_y - self.shape_offset_y)
+
+    def _start_path(self, sp: CT_Shape) -> CT_Path2D:
+        """Return a newly created `a:path` element added to `sp`.
+
+        The returned `a:path` element has an `a:moveTo` element representing the shape starting
+        point as its only child.
+        """
+        path = sp.add_path(w=self._dx, h=self._dy)
+        path.add_moveTo(*self._local_to_shape(self._start_x, self._start_y))
+        return path
+
+    @property
+    def _top(self):
+        """Return topmost extent of this shape's path in slide coordinates.
+
+        Note that this value does not include any positioning offset; it assumes the drawing
+        (local) coordinate origin is located at slide coordinates (0, 0) (top-left corner of
+        slide).
+        """
+        return int(round(self.shape_offset_y * self._y_scale))
+
+    @property
+    def _width(self):
+        """Return width of this shape's path in slide coordinates.
+
+        This value is based on the actual extents of the shape path and does not include any
+        positioning offset.
+        """
+        return int(round(self._dx * self._x_scale))
+
+
+class _BaseDrawingOperation(object):
+    """Base class for freeform drawing operations.
+
+    A drawing operation has at least one location (x, y) in local coordinates.
+    """
+
+    def __init__(self, freeform_builder: FreeformBuilder, x: Length, y: Length):
+        super(_BaseDrawingOperation, self).__init__()
+        self._freeform_builder = freeform_builder
+        self._x = x
+        self._y = y
+
+    def apply_operation_to(self, path: CT_Path2D) -> CT_DrawingOperation:
+        """Add the XML element(s) implementing this operation to `path`.
+
+        Must be implemented by each subclass.
+        """
+        raise NotImplementedError("must be implemented by each subclass")
+
+    @property
+    def x(self) -> Length:
+        """Return the horizontal (x) target location of this operation.
+
+        The returned value is an integer in local coordinates.
+        """
+        return self._x
+
+    @property
+    def y(self) -> Length:
+        """Return the vertical (y) target location of this operation.
+
+        The returned value is an integer in local coordinates.
+        """
+        return self._y
+
+
+class _Close(object):
+    """Specifies adding a `<a:close/>` element to the current contour."""
+
+    @classmethod
+    def new(cls) -> _Close:
+        """Return a new _Close object."""
+        return cls()
+
+    def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DClose:
+        """Add `a:close` element to `path`."""
+        return path.add_close()
+
+
+class _LineSegment(_BaseDrawingOperation):
+    """Specifies a straight line segment ending at the specified point."""
+
+    @classmethod
+    def new(cls, freeform_builder: FreeformBuilder, x: float, y: float) -> _LineSegment:
+        """Return a new _LineSegment object ending at point *(x, y)*.
+
+        Both `x` and `y` are rounded to the nearest integer before use.
+        """
+        return cls(freeform_builder, Emu(int(round(x))), Emu(int(round(y))))
+
+    def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DLineTo:
+        """Add `a:lnTo` element to `path` for this line segment.
+
+        Returns the `a:lnTo` element newly added to the path.
+        """
+        return path.add_lnTo(
+            Emu(self._x - self._freeform_builder.shape_offset_x),
+            Emu(self._y - self._freeform_builder.shape_offset_y),
+        )
+
+
+class _MoveTo(_BaseDrawingOperation):
+    """Specifies a new pen position."""
+
+    @classmethod
+    def new(cls, freeform_builder: FreeformBuilder, x: float, y: float) -> _MoveTo:
+        """Return a new _MoveTo object for move to point `(x, y)`.
+
+        Both `x` and `y` are rounded to the nearest integer before use.
+        """
+        return cls(freeform_builder, Emu(int(round(x))), Emu(int(round(y))))
+
+    def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DMoveTo:
+        """Add `a:moveTo` element to `path` for this line segment."""
+        return path.add_moveTo(
+            Emu(self._x - self._freeform_builder.shape_offset_x),
+            Emu(self._y - self._freeform_builder.shape_offset_y),
+        )
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/graphfrm.py b/.venv/lib/python3.12/site-packages/pptx/shapes/graphfrm.py
new file mode 100644
index 00000000..c0ed2bba
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/graphfrm.py
@@ -0,0 +1,166 @@
+"""Graphic Frame shape and related objects.
+
+A graphic frame is a common container for table, chart, smart art, and media
+objects.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+from pptx.enum.shapes import MSO_SHAPE_TYPE
+from pptx.shapes.base import BaseShape
+from pptx.shared import ParentedElementProxy
+from pptx.spec import (
+    GRAPHIC_DATA_URI_CHART,
+    GRAPHIC_DATA_URI_OLEOBJ,
+    GRAPHIC_DATA_URI_TABLE,
+)
+from pptx.table import Table
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.chart.chart import Chart
+    from pptx.dml.effect import ShadowFormat
+    from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectData, CT_GraphicalObjectFrame
+    from pptx.parts.chart import ChartPart
+    from pptx.parts.slide import BaseSlidePart
+    from pptx.types import ProvidesPart
+
+
+class GraphicFrame(BaseShape):
+    """Container shape for table, chart, smart art, and media objects.
+
+    Corresponds to a `p:graphicFrame` element in the shape tree.
+    """
+
+    def __init__(self, graphicFrame: CT_GraphicalObjectFrame, parent: ProvidesPart):
+        super().__init__(graphicFrame, parent)
+        self._graphicFrame = graphicFrame
+
+    @property
+    def chart(self) -> Chart:
+        """The |Chart| object containing the chart in this graphic frame.
+
+        Raises |ValueError| if this graphic frame does not contain a chart.
+        """
+        if not self.has_chart:
+            raise ValueError("shape does not contain a chart")
+        return self.chart_part.chart
+
+    @property
+    def chart_part(self) -> ChartPart:
+        """The |ChartPart| object containing the chart in this graphic frame."""
+        chart_rId = self._graphicFrame.chart_rId
+        if chart_rId is None:
+            raise ValueError("this graphic frame does not contain a chart")
+        return cast("ChartPart", self.part.related_part(chart_rId))
+
+    @property
+    def has_chart(self) -> bool:
+        """|True| if this graphic frame contains a chart object. |False| otherwise.
+
+        When |True|, the chart object can be accessed using the `.chart` property.
+        """
+        return self._graphicFrame.graphicData_uri == GRAPHIC_DATA_URI_CHART
+
+    @property
+    def has_table(self) -> bool:
+        """|True| if this graphic frame contains a table object, |False| otherwise.
+
+        When |True|, the table object can be accessed using the `.table` property.
+        """
+        return self._graphicFrame.graphicData_uri == GRAPHIC_DATA_URI_TABLE
+
+    @property
+    def ole_format(self) -> _OleFormat:
+        """_OleFormat object for this graphic-frame shape.
+
+        Raises `ValueError` on a GraphicFrame instance that does not contain an OLE object.
+
+        An shape that contains an OLE object will have `.shape_type` of either
+        `EMBEDDED_OLE_OBJECT` or `LINKED_OLE_OBJECT`.
+        """
+        if not self._graphicFrame.has_oleobj:
+            raise ValueError("not an OLE-object shape")
+        return _OleFormat(self._graphicFrame.graphicData, self._parent)
+
+    @lazyproperty
+    def shadow(self) -> ShadowFormat:
+        """Unconditionally raises |NotImplementedError|.
+
+        Access to the shadow effect for graphic-frame objects is content-specific (i.e. different
+        for charts, tables, etc.) and has not yet been implemented.
+        """
+        raise NotImplementedError("shadow property on GraphicFrame not yet supported")
+
+    @property
+    def shape_type(self) -> MSO_SHAPE_TYPE:
+        """Optional member of `MSO_SHAPE_TYPE` identifying the type of this shape.
+
+        Possible values are `MSO_SHAPE_TYPE.CHART`, `MSO_SHAPE_TYPE.TABLE`,
+        `MSO_SHAPE_TYPE.EMBEDDED_OLE_OBJECT`, `MSO_SHAPE_TYPE.LINKED_OLE_OBJECT`.
+
+        This value is `None` when none of these four types apply, for example when the shape
+        contains SmartArt.
+        """
+        graphicData_uri = self._graphicFrame.graphicData_uri
+        if graphicData_uri == GRAPHIC_DATA_URI_CHART:
+            return MSO_SHAPE_TYPE.CHART
+        elif graphicData_uri == GRAPHIC_DATA_URI_TABLE:
+            return MSO_SHAPE_TYPE.TABLE
+        elif graphicData_uri == GRAPHIC_DATA_URI_OLEOBJ:
+            return (
+                MSO_SHAPE_TYPE.EMBEDDED_OLE_OBJECT
+                if self._graphicFrame.is_embedded_ole_obj
+                else MSO_SHAPE_TYPE.LINKED_OLE_OBJECT
+            )
+        else:
+            return None  # pyright: ignore[reportReturnType]
+
+    @property
+    def table(self) -> Table:
+        """The |Table| object contained in this graphic frame.
+
+        Raises |ValueError| if this graphic frame does not contain a table.
+        """
+        if not self.has_table:
+            raise ValueError("shape does not contain a table")
+        tbl = self._graphicFrame.graphic.graphicData.tbl
+        return Table(tbl, self)
+
+
+class _OleFormat(ParentedElementProxy):
+    """Provides attributes on an embedded OLE object."""
+
+    part: BaseSlidePart  # pyright: ignore[reportIncompatibleMethodOverride]
+
+    def __init__(self, graphicData: CT_GraphicalObjectData, parent: ProvidesPart):
+        super().__init__(graphicData, parent)
+        self._graphicData = graphicData
+
+    @property
+    def blob(self) -> bytes | None:
+        """Optional bytes of OLE object, suitable for loading or saving as a file.
+
+        This value is `None` if the embedded object does not represent a "file".
+        """
+        blob_rId = self._graphicData.blob_rId
+        if blob_rId is None:
+            return None
+        return self.part.related_part(blob_rId).blob
+
+    @property
+    def prog_id(self) -> str | None:
+        """str "progId" attribute of this embedded OLE object.
+
+        The progId is a str like "Excel.Sheet.12" that identifies the "file-type" of the embedded
+        object, or perhaps more precisely, the application (aka. "server" in OLE parlance) to be
+        used to open this object.
+        """
+        return self._graphicData.progId
+
+    @property
+    def show_as_icon(self) -> bool | None:
+        """True when OLE object should appear as an icon (rather than preview)."""
+        return self._graphicData.showAsIcon
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/group.py b/.venv/lib/python3.12/site-packages/pptx/shapes/group.py
new file mode 100644
index 00000000..71737585
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/group.py
@@ -0,0 +1,69 @@
+"""GroupShape and related objects."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pptx.dml.effect import ShadowFormat
+from pptx.enum.shapes import MSO_SHAPE_TYPE
+from pptx.shapes.base import BaseShape
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.action import ActionSetting
+    from pptx.oxml.shapes.groupshape import CT_GroupShape
+    from pptx.shapes.shapetree import GroupShapes
+    from pptx.types import ProvidesPart
+
+
+class GroupShape(BaseShape):
+    """A shape that acts as a container for other shapes."""
+
+    def __init__(self, grpSp: CT_GroupShape, parent: ProvidesPart):
+        super().__init__(grpSp, parent)
+        self._grpSp = grpSp
+
+    @lazyproperty
+    def click_action(self) -> ActionSetting:
+        """Unconditionally raises `TypeError`.
+
+        A group shape cannot have a click action or hover action.
+        """
+        raise TypeError("a group shape cannot have a click action")
+
+    @property
+    def has_text_frame(self) -> bool:
+        """Unconditionally |False|.
+
+        A group shape does not have a textframe and cannot itself contain text. This does not
+        impact the ability of shapes contained by the group to each have their own text.
+        """
+        return False
+
+    @lazyproperty
+    def shadow(self) -> ShadowFormat:
+        """|ShadowFormat| object representing shadow effect for this group.
+
+        A |ShadowFormat| object is always returned, even when no shadow is explicitly defined on
+        this group shape (i.e. when the group inherits its shadow behavior).
+        """
+        return ShadowFormat(self._grpSp.grpSpPr)
+
+    @property
+    def shape_type(self) -> MSO_SHAPE_TYPE:
+        """Member of :ref:`MsoShapeType` identifying the type of this shape.
+
+        Unconditionally `MSO_SHAPE_TYPE.GROUP` in this case
+        """
+        return MSO_SHAPE_TYPE.GROUP
+
+    @lazyproperty
+    def shapes(self) -> GroupShapes:
+        """|GroupShapes| object for this group.
+
+        The |GroupShapes| object provides access to the group's member shapes and provides methods
+        for adding new ones.
+        """
+        from pptx.shapes.shapetree import GroupShapes
+
+        return GroupShapes(self._element, self)
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/picture.py b/.venv/lib/python3.12/site-packages/pptx/shapes/picture.py
new file mode 100644
index 00000000..59182860
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/picture.py
@@ -0,0 +1,203 @@
+"""Shapes based on the `p:pic` element, including Picture and Movie."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pptx.dml.line import LineFormat
+from pptx.enum.shapes import MSO_SHAPE, MSO_SHAPE_TYPE, PP_MEDIA_TYPE
+from pptx.shapes.base import BaseShape
+from pptx.shared import ParentedElementProxy
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.oxml.shapes.picture import CT_Picture
+    from pptx.oxml.shapes.shared import CT_LineProperties
+    from pptx.types import ProvidesPart
+
+
+class _BasePicture(BaseShape):
+    """Base class for shapes based on a `p:pic` element."""
+
+    def __init__(self, pic: CT_Picture, parent: ProvidesPart):
+        super(_BasePicture, self).__init__(pic, parent)
+        self._pic = pic
+
+    @property
+    def crop_bottom(self) -> float:
+        """|float| representing relative portion cropped from shape bottom.
+
+        Read/write. 1.0 represents 100%. For example, 25% is represented by 0.25. Negative values
+        are valid as are values greater than 1.0.
+        """
+        return self._pic.srcRect_b
+
+    @crop_bottom.setter
+    def crop_bottom(self, value: float):
+        self._pic.srcRect_b = value
+
+    @property
+    def crop_left(self) -> float:
+        """|float| representing relative portion cropped from left of shape.
+
+        Read/write. 1.0 represents 100%. A negative value extends the side beyond the image
+        boundary.
+        """
+        return self._pic.srcRect_l
+
+    @crop_left.setter
+    def crop_left(self, value: float):
+        self._pic.srcRect_l = value
+
+    @property
+    def crop_right(self) -> float:
+        """|float| representing relative portion cropped from right of shape.
+
+        Read/write. 1.0 represents 100%.
+        """
+        return self._pic.srcRect_r
+
+    @crop_right.setter
+    def crop_right(self, value: float):
+        self._pic.srcRect_r = value
+
+    @property
+    def crop_top(self) -> float:
+        """|float| representing relative portion cropped from shape top.
+
+        Read/write. 1.0 represents 100%.
+        """
+        return self._pic.srcRect_t
+
+    @crop_top.setter
+    def crop_top(self, value: float):
+        self._pic.srcRect_t = value
+
+    def get_or_add_ln(self):
+        """Return the `a:ln` element for this `p:pic`-based image.
+
+        The `a:ln` element contains the line format properties XML.
+        """
+        return self._pic.get_or_add_ln()
+
+    @lazyproperty
+    def line(self) -> LineFormat:
+        """Provides access to properties of the picture outline, such as its color and width."""
+        return LineFormat(self)
+
+    @property
+    def ln(self) -> CT_LineProperties | None:
+        """The `a:ln` element for this `p:pic`.
+
+        Contains the line format properties such as line color and width. |None| if no `a:ln`
+        element is present.
+        """
+        return self._pic.ln
+
+
+class Movie(_BasePicture):
+    """A movie shape, one that places a video on a slide.
+
+    Like |Picture|, a movie shape is based on the `p:pic` element. A movie is composed of a video
+    and a *poster frame*, the placeholder image that represents the video before it is played.
+    """
+
+    @lazyproperty
+    def media_format(self) -> _MediaFormat:
+        """The |_MediaFormat| object for this movie.
+
+        The |_MediaFormat| object provides access to formatting properties for the movie.
+        """
+        return _MediaFormat(self._pic, self)
+
+    @property
+    def media_type(self) -> PP_MEDIA_TYPE:
+        """Member of :ref:`PpMediaType` describing this shape.
+
+        The return value is unconditionally `PP_MEDIA_TYPE.MOVIE` in this case.
+        """
+        return PP_MEDIA_TYPE.MOVIE
+
+    @property
+    def poster_frame(self):
+        """Return |Image| object containing poster frame for this movie.
+
+        Returns |None| if this movie has no poster frame (uncommon).
+        """
+        slide_part, rId = self.part, self._pic.blip_rId
+        if rId is None:
+            return None
+        return slide_part.get_image(rId)
+
+    @property
+    def shape_type(self) -> MSO_SHAPE_TYPE:
+        """Return member of :ref:`MsoShapeType` describing this shape.
+
+        The return value is unconditionally `MSO_SHAPE_TYPE.MEDIA` in this
+        case.
+        """
+        return MSO_SHAPE_TYPE.MEDIA
+
+
+class Picture(_BasePicture):
+    """A picture shape, one that places an image on a slide.
+
+    Based on the `p:pic` element.
+    """
+
+    @property
+    def auto_shape_type(self) -> MSO_SHAPE | None:
+        """Member of MSO_SHAPE indicating masking shape.
+
+        A picture can be masked by any of the so-called "auto-shapes" available in PowerPoint,
+        such as an ellipse or triangle. When a picture is masked by a shape, the shape assumes the
+        same dimensions as the picture and the portion of the picture outside the shape boundaries
+        does not appear. Note the default value for a newly-inserted picture is
+        `MSO_AUTO_SHAPE_TYPE.RECTANGLE`, which performs no cropping because the extents of the
+        rectangle exactly correspond to the extents of the picture.
+
+        The available shapes correspond to the members of :ref:`MsoAutoShapeType`.
+
+        The return value can also be |None|, indicating the picture either has no geometry (not
+        expected) or has custom geometry, like a freeform shape. A picture with no geometry will
+        have no visible representation on the slide, although it can be selected. This is because
+        without geometry, there is no "inside-the-shape" for it to appear in.
+        """
+        prstGeom = self._pic.spPr.prstGeom
+        if prstGeom is None:  # ---generally means cropped with freeform---
+            return None
+        return prstGeom.prst
+
+    @auto_shape_type.setter
+    def auto_shape_type(self, member: MSO_SHAPE):
+        MSO_SHAPE.validate(member)
+        spPr = self._pic.spPr
+        prstGeom = spPr.prstGeom
+        if prstGeom is None:
+            spPr._remove_custGeom()  # pyright: ignore[reportPrivateUsage]
+            prstGeom = spPr._add_prstGeom()  # pyright: ignore[reportPrivateUsage]
+        prstGeom.prst = member
+
+    @property
+    def image(self):
+        """The |Image| object for this picture.
+
+        Provides access to the properties and bytes of the image in this picture shape.
+        """
+        slide_part, rId = self.part, self._pic.blip_rId
+        if rId is None:
+            raise ValueError("no embedded image")
+        return slide_part.get_image(rId)
+
+    @property
+    def shape_type(self) -> MSO_SHAPE_TYPE:
+        """Unconditionally `MSO_SHAPE_TYPE.PICTURE` in this case."""
+        return MSO_SHAPE_TYPE.PICTURE
+
+
+class _MediaFormat(ParentedElementProxy):
+    """Provides access to formatting properties for a Media object.
+
+    Media format properties are things like start point, volume, and
+    compression type.
+    """
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/placeholder.py b/.venv/lib/python3.12/site-packages/pptx/shapes/placeholder.py
new file mode 100644
index 00000000..c44837be
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/placeholder.py
@@ -0,0 +1,407 @@
+"""Placeholder-related objects.
+
+Specific to shapes having a `p:ph` element. A placeholder has distinct behaviors
+depending on whether it appears on a slide, layout, or master. Hence there is a
+non-trivial class inheritance structure.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER
+from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
+from pptx.oxml.shapes.picture import CT_Picture
+from pptx.shapes.autoshape import Shape
+from pptx.shapes.graphfrm import GraphicFrame
+from pptx.shapes.picture import Picture
+from pptx.util import Emu
+
+if TYPE_CHECKING:
+    from pptx.oxml.shapes.autoshape import CT_Shape
+
+
+class _InheritsDimensions(object):
+    """
+    Mixin class that provides inherited dimension behavior. Specifically,
+    left, top, width, and height report the value from the layout placeholder
+    where they would have otherwise reported |None|. This behavior is
+    distinctive to placeholders. :meth:`_base_placeholder` must be overridden
+    by all subclasses to provide lookup of the appropriate base placeholder
+    to inherit from.
+    """
+
+    @property
+    def height(self):
+        """
+        The effective height of this placeholder shape; its directly-applied
+        height if it has one, otherwise the height of its parent layout
+        placeholder.
+        """
+        return self._effective_value("height")
+
+    @height.setter
+    def height(self, value):
+        self._element.cy = value
+
+    @property
+    def left(self):
+        """
+        The effective left of this placeholder shape; its directly-applied
+        left if it has one, otherwise the left of its parent layout
+        placeholder.
+        """
+        return self._effective_value("left")
+
+    @left.setter
+    def left(self, value):
+        self._element.x = value
+
+    @property
+    def shape_type(self):
+        """
+        Member of :ref:`MsoShapeType` specifying the type of this shape.
+        Unconditionally ``MSO_SHAPE_TYPE.PLACEHOLDER`` in this case.
+        Read-only.
+        """
+        return MSO_SHAPE_TYPE.PLACEHOLDER
+
+    @property
+    def top(self):
+        """
+        The effective top of this placeholder shape; its directly-applied
+        top if it has one, otherwise the top of its parent layout
+        placeholder.
+        """
+        return self._effective_value("top")
+
+    @top.setter
+    def top(self, value):
+        self._element.y = value
+
+    @property
+    def width(self):
+        """
+        The effective width of this placeholder shape; its directly-applied
+        width if it has one, otherwise the width of its parent layout
+        placeholder.
+        """
+        return self._effective_value("width")
+
+    @width.setter
+    def width(self, value):
+        self._element.cx = value
+
+    @property
+    def _base_placeholder(self):
+        """
+        Return the layout or master placeholder shape this placeholder
+        inherits from. Not to be confused with an instance of
+        |BasePlaceholder| (necessarily).
+        """
+        raise NotImplementedError("Must be implemented by all subclasses.")
+
+    def _effective_value(self, attr_name):
+        """
+        The effective value of *attr_name* on this placeholder shape; its
+        directly-applied value if it has one, otherwise the value on the
+        layout placeholder it inherits from.
+        """
+        directly_applied_value = getattr(super(_InheritsDimensions, self), attr_name)
+        if directly_applied_value is not None:
+            return directly_applied_value
+        return self._inherited_value(attr_name)
+
+    def _inherited_value(self, attr_name):
+        """
+        Return the attribute value, e.g. 'width' of the base placeholder this
+        placeholder inherits from.
+        """
+        base_placeholder = self._base_placeholder
+        if base_placeholder is None:
+            return None
+        inherited_value = getattr(base_placeholder, attr_name)
+        return inherited_value
+
+
+class _BaseSlidePlaceholder(_InheritsDimensions, Shape):
+    """Base class for placeholders on slides.
+
+    Provides common behaviors such as inherited dimensions.
+    """
+
+    @property
+    def is_placeholder(self):
+        """
+        Boolean indicating whether this shape is a placeholder.
+        Unconditionally |True| in this case.
+        """
+        return True
+
+    @property
+    def shape_type(self):
+        """
+        Member of :ref:`MsoShapeType` specifying the type of this shape.
+        Unconditionally ``MSO_SHAPE_TYPE.PLACEHOLDER`` in this case.
+        Read-only.
+        """
+        return MSO_SHAPE_TYPE.PLACEHOLDER
+
+    @property
+    def _base_placeholder(self):
+        """
+        Return the layout placeholder this slide placeholder inherits from.
+        Not to be confused with an instance of |BasePlaceholder|
+        (necessarily).
+        """
+        layout, idx = self.part.slide_layout, self._element.ph_idx
+        return layout.placeholders.get(idx=idx)
+
+    def _replace_placeholder_with(self, element):
+        """
+        Substitute *element* for this placeholder element in the shapetree.
+        This placeholder's `._element` attribute is set to |None| and its
+        original element is free for garbage collection. Any attribute access
+        (including a method call) on this placeholder after this call raises
+        |AttributeError|.
+        """
+        element._nvXxPr.nvPr._insert_ph(self._element.ph)
+        self._element.addprevious(element)
+        self._element.getparent().remove(self._element)
+        self._element = None
+
+
+class BasePlaceholder(Shape):
+    """
+    NOTE: This class is deprecated and will be removed from a future release
+    along with the properties *idx*, *orient*, *ph_type*, and *sz*. The *idx*
+    property will be available via the .placeholder_format property. The
+    others will be accessed directly from the oxml layer as they are only
+    used for internal purposes.
+
+    Base class for placeholder subclasses that differentiate the varying
+    behaviors of placeholders on a master, layout, and slide.
+    """
+
+    @property
+    def idx(self):
+        """
+        Integer placeholder 'idx' attribute, e.g. 0
+        """
+        return self._sp.ph_idx
+
+    @property
+    def orient(self):
+        """
+        Placeholder orientation, e.g. ST_Direction.HORZ
+        """
+        return self._sp.ph_orient
+
+    @property
+    def ph_type(self):
+        """
+        Placeholder type, e.g. PP_PLACEHOLDER.CENTER_TITLE
+        """
+        return self._sp.ph_type
+
+    @property
+    def sz(self):
+        """
+        Placeholder 'sz' attribute, e.g. ST_PlaceholderSize.FULL
+        """
+        return self._sp.ph_sz
+
+
+class LayoutPlaceholder(_InheritsDimensions, Shape):
+    """Placeholder shape on a slide layout.
+
+    Provides differentiated behavior for slide layout placeholders, in particular, inheriting
+    shape properties from the master placeholder having the same type, when a matching one exists.
+    """
+
+    element: CT_Shape  # pyright: ignore[reportIncompatibleMethodOverride]
+
+    @property
+    def _base_placeholder(self):
+        """
+        Return the master placeholder this layout placeholder inherits from.
+        """
+        base_ph_type = {
+            PP_PLACEHOLDER.BODY: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.CHART: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.BITMAP: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.CENTER_TITLE: PP_PLACEHOLDER.TITLE,
+            PP_PLACEHOLDER.ORG_CHART: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.DATE: PP_PLACEHOLDER.DATE,
+            PP_PLACEHOLDER.FOOTER: PP_PLACEHOLDER.FOOTER,
+            PP_PLACEHOLDER.MEDIA_CLIP: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.OBJECT: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.PICTURE: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.SLIDE_NUMBER: PP_PLACEHOLDER.SLIDE_NUMBER,
+            PP_PLACEHOLDER.SUBTITLE: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.TABLE: PP_PLACEHOLDER.BODY,
+            PP_PLACEHOLDER.TITLE: PP_PLACEHOLDER.TITLE,
+        }[self._element.ph_type]
+        slide_master = self.part.slide_master
+        return slide_master.placeholders.get(base_ph_type, None)
+
+
+class MasterPlaceholder(BasePlaceholder):
+    """Placeholder shape on a slide master."""
+
+    element: CT_Shape  # pyright: ignore[reportIncompatibleMethodOverride]
+
+
+class NotesSlidePlaceholder(_InheritsDimensions, Shape):
+    """
+    Placeholder shape on a notes slide. Inherits shape properties from the
+    placeholder on the notes master that has the same type (e.g. 'body').
+    """
+
+    @property
+    def _base_placeholder(self):
+        """
+        Return the notes master placeholder this notes slide placeholder
+        inherits from, or |None| if no placeholder of the matching type is
+        present.
+        """
+        notes_master = self.part.notes_master
+        ph_type = self.element.ph_type
+        return notes_master.placeholders.get(ph_type=ph_type)
+
+
+class SlidePlaceholder(_BaseSlidePlaceholder):
+    """
+    Placeholder shape on a slide. Inherits shape properties from its
+    corresponding slide layout placeholder.
+    """
+
+
+class ChartPlaceholder(_BaseSlidePlaceholder):
+    """Placeholder shape that can only accept a chart."""
+
+    def insert_chart(self, chart_type, chart_data):
+        """
+        Return a |PlaceholderGraphicFrame| object containing a new chart of
+        *chart_type* depicting *chart_data* and having the same position and
+        size as this placeholder. *chart_type* is one of the
+        :ref:`XlChartType` enumeration values. *chart_data* is a |ChartData|
+        object populated with the categories and series values for the chart.
+        Note that the new |Chart| object is not returned directly. The chart
+        object may be accessed using the
+        :attr:`~.PlaceholderGraphicFrame.chart` property of the returned
+        |PlaceholderGraphicFrame| object.
+        """
+        rId = self.part.add_chart_part(chart_type, chart_data)
+        graphicFrame = self._new_chart_graphicFrame(
+            rId, self.left, self.top, self.width, self.height
+        )
+        self._replace_placeholder_with(graphicFrame)
+        return PlaceholderGraphicFrame(graphicFrame, self._parent)
+
+    def _new_chart_graphicFrame(self, rId, x, y, cx, cy):
+        """
+        Return a newly created `p:graphicFrame` element having the specified
+        position and size and containing the chart identified by *rId*.
+        """
+        id_, name = self.shape_id, self.name
+        return CT_GraphicalObjectFrame.new_chart_graphicFrame(id_, name, rId, x, y, cx, cy)
+
+
+class PicturePlaceholder(_BaseSlidePlaceholder):
+    """Placeholder shape that can only accept a picture."""
+
+    def insert_picture(self, image_file):
+        """Return a |PlaceholderPicture| object depicting the image in `image_file`.
+
+        `image_file` may be either a path (string) or a file-like object. The image is
+        cropped to fill the entire space of the placeholder. A |PlaceholderPicture|
+        object has all the properties and methods of a |Picture| shape except that the
+        value of its :attr:`~._BaseSlidePlaceholder.shape_type` property is
+        `MSO_SHAPE_TYPE.PLACEHOLDER` instead of `MSO_SHAPE_TYPE.PICTURE`.
+        """
+        pic = self._new_placeholder_pic(image_file)
+        self._replace_placeholder_with(pic)
+        return PlaceholderPicture(pic, self._parent)
+
+    def _new_placeholder_pic(self, image_file):
+        """
+        Return a new `p:pic` element depicting the image in *image_file*,
+        suitable for use as a placeholder. In particular this means not
+        having an `a:xfrm` element, allowing its extents to be inherited from
+        its layout placeholder.
+        """
+        rId, desc, image_size = self._get_or_add_image(image_file)
+        shape_id, name = self.shape_id, self.name
+        pic = CT_Picture.new_ph_pic(shape_id, name, desc, rId)
+        pic.crop_to_fit(image_size, (self.width, self.height))
+        return pic
+
+    def _get_or_add_image(self, image_file):
+        """
+        Return an (rId, description, image_size) 3-tuple identifying the
+        related image part containing *image_file* and describing the image.
+        """
+        image_part, rId = self.part.get_or_add_image_part(image_file)
+        desc, image_size = image_part.desc, image_part._px_size
+        return rId, desc, image_size
+
+
+class PlaceholderGraphicFrame(GraphicFrame):
+    """
+    Placeholder shape populated with a table, chart, or smart art.
+    """
+
+    @property
+    def is_placeholder(self):
+        """
+        Boolean indicating whether this shape is a placeholder.
+        Unconditionally |True| in this case.
+        """
+        return True
+
+
+class PlaceholderPicture(_InheritsDimensions, Picture):
+    """
+    Placeholder shape populated with a picture.
+    """
+
+    @property
+    def _base_placeholder(self):
+        """
+        Return the layout placeholder this picture placeholder inherits from.
+        """
+        layout, idx = self.part.slide_layout, self._element.ph_idx
+        return layout.placeholders.get(idx=idx)
+
+
+class TablePlaceholder(_BaseSlidePlaceholder):
+    """Placeholder shape that can only accept a table."""
+
+    def insert_table(self, rows, cols):
+        """Return |PlaceholderGraphicFrame| object containing a `rows` by `cols` table.
+
+        The position and width of the table are those of the placeholder and its height
+        is proportional to the number of rows. A |PlaceholderGraphicFrame| object has
+        all the properties and methods of a |GraphicFrame| shape except that the value
+        of its :attr:`~._BaseSlidePlaceholder.shape_type` property is unconditionally
+        `MSO_SHAPE_TYPE.PLACEHOLDER`. Note that the return value is not the new table
+        but rather *contains* the new table. The table can be accessed using the
+        :attr:`~.PlaceholderGraphicFrame.table` property of the returned
+        |PlaceholderGraphicFrame| object.
+        """
+        graphicFrame = self._new_placeholder_table(rows, cols)
+        self._replace_placeholder_with(graphicFrame)
+        return PlaceholderGraphicFrame(graphicFrame, self._parent)
+
+    def _new_placeholder_table(self, rows, cols):
+        """
+        Return a newly added `p:graphicFrame` element containing an empty
+        table with *rows* rows and *cols* columns, positioned at the location
+        of this placeholder and having its same width. The table's height is
+        determined by the number of rows.
+        """
+        shape_id, name, height = self.shape_id, self.name, Emu(rows * 370840)
+        return CT_GraphicalObjectFrame.new_table_graphicFrame(
+            shape_id, name, rows, cols, self.left, self.top, self.width, height
+        )
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/shapetree.py b/.venv/lib/python3.12/site-packages/pptx/shapes/shapetree.py
new file mode 100644
index 00000000..29623f1f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/shapetree.py
@@ -0,0 +1,1190 @@
+"""The shape tree, the structure that holds a slide's shapes."""
+
+from __future__ import annotations
+
+import io
+import os
+from typing import IO, TYPE_CHECKING, Callable, Iterable, Iterator, cast
+
+from pptx.enum.shapes import PP_PLACEHOLDER, PROG_ID
+from pptx.media import SPEAKER_IMAGE_BYTES, Video
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.oxml.ns import qn
+from pptx.oxml.shapes.autoshape import CT_Shape
+from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
+from pptx.oxml.shapes.picture import CT_Picture
+from pptx.oxml.simpletypes import ST_Direction
+from pptx.shapes.autoshape import AutoShapeType, Shape
+from pptx.shapes.base import BaseShape
+from pptx.shapes.connector import Connector
+from pptx.shapes.freeform import FreeformBuilder
+from pptx.shapes.graphfrm import GraphicFrame
+from pptx.shapes.group import GroupShape
+from pptx.shapes.picture import Movie, Picture
+from pptx.shapes.placeholder import (
+    ChartPlaceholder,
+    LayoutPlaceholder,
+    MasterPlaceholder,
+    NotesSlidePlaceholder,
+    PicturePlaceholder,
+    PlaceholderGraphicFrame,
+    PlaceholderPicture,
+    SlidePlaceholder,
+    TablePlaceholder,
+)
+from pptx.shared import ParentedElementProxy
+from pptx.util import Emu, lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.chart.chart import Chart
+    from pptx.chart.data import ChartData
+    from pptx.enum.chart import XL_CHART_TYPE
+    from pptx.enum.shapes import MSO_CONNECTOR_TYPE, MSO_SHAPE
+    from pptx.oxml.shapes import ShapeElement
+    from pptx.oxml.shapes.connector import CT_Connector
+    from pptx.oxml.shapes.groupshape import CT_GroupShape
+    from pptx.parts.image import ImagePart
+    from pptx.parts.slide import SlidePart
+    from pptx.slide import Slide, SlideLayout
+    from pptx.types import ProvidesPart
+    from pptx.util import Length
+
+# +-- _BaseShapes
+# |   |
+# |   +-- _BaseGroupShapes
+# |   |   |
+# |   |   +-- GroupShapes
+# |   |   |
+# |   |   +-- SlideShapes
+# |   |
+# |   +-- LayoutShapes
+# |   |
+# |   +-- MasterShapes
+# |   |
+# |   +-- NotesSlideShapes
+# |   |
+# |   +-- BasePlaceholders
+# |       |
+# |       +-- LayoutPlaceholders
+# |       |
+# |       +-- MasterPlaceholders
+# |           |
+# |           +-- NotesSlidePlaceholders
+# |
+# +-- SlidePlaceholders
+
+
+class _BaseShapes(ParentedElementProxy):
+    """Base class for a shape collection appearing in a slide-type object.
+
+    Subclasses include Slide, SlideLayout, and SlideMaster. Provides common methods.
+    """
+
+    def __init__(self, spTree: CT_GroupShape, parent: ProvidesPart):
+        super(_BaseShapes, self).__init__(spTree, parent)
+        self._spTree = spTree
+        self._cached_max_shape_id = None
+
+    def __getitem__(self, idx: int) -> BaseShape:
+        """Return shape at `idx` in sequence, e.g. `shapes[2]`."""
+        shape_elms = list(self._iter_member_elms())
+        try:
+            shape_elm = shape_elms[idx]
+        except IndexError:
+            raise IndexError("shape index out of range")
+        return self._shape_factory(shape_elm)
+
+    def __iter__(self) -> Iterator[BaseShape]:
+        """Generate a reference to each shape in the collection, in sequence."""
+        for shape_elm in self._iter_member_elms():
+            yield self._shape_factory(shape_elm)
+
+    def __len__(self) -> int:
+        """Return count of shapes in this shape tree.
+
+        A group shape contributes 1 to the total, without regard to the number of shapes contained
+        in the group.
+        """
+        shape_elms = list(self._iter_member_elms())
+        return len(shape_elms)
+
+    def clone_placeholder(self, placeholder: LayoutPlaceholder) -> None:
+        """Add a new placeholder shape based on `placeholder`."""
+        sp = placeholder.element
+        ph_type, orient, sz, idx = (sp.ph_type, sp.ph_orient, sp.ph_sz, sp.ph_idx)
+        id_ = self._next_shape_id
+        name = self._next_ph_name(ph_type, id_, orient)
+        self._spTree.add_placeholder(id_, name, ph_type, orient, sz, idx)
+
+    def ph_basename(self, ph_type: PP_PLACEHOLDER) -> str:
+        """Return the base name for a placeholder of `ph_type` in this shape collection.
+
+        There is some variance between slide types, for example a notes slide uses a different
+        name for the body placeholder, so this method can be overriden by subclasses.
+        """
+        return {
+            PP_PLACEHOLDER.BITMAP: "ClipArt Placeholder",
+            PP_PLACEHOLDER.BODY: "Text Placeholder",
+            PP_PLACEHOLDER.CENTER_TITLE: "Title",
+            PP_PLACEHOLDER.CHART: "Chart Placeholder",
+            PP_PLACEHOLDER.DATE: "Date Placeholder",
+            PP_PLACEHOLDER.FOOTER: "Footer Placeholder",
+            PP_PLACEHOLDER.HEADER: "Header Placeholder",
+            PP_PLACEHOLDER.MEDIA_CLIP: "Media Placeholder",
+            PP_PLACEHOLDER.OBJECT: "Content Placeholder",
+            PP_PLACEHOLDER.ORG_CHART: "SmartArt Placeholder",
+            PP_PLACEHOLDER.PICTURE: "Picture Placeholder",
+            PP_PLACEHOLDER.SLIDE_NUMBER: "Slide Number Placeholder",
+            PP_PLACEHOLDER.SUBTITLE: "Subtitle",
+            PP_PLACEHOLDER.TABLE: "Table Placeholder",
+            PP_PLACEHOLDER.TITLE: "Title",
+        }[ph_type]
+
+    @property
+    def turbo_add_enabled(self) -> bool:
+        """True if "turbo-add" mode is enabled. Read/Write.
+
+        EXPERIMENTAL: This feature can radically improve performance when adding large numbers
+        (hundreds of shapes) to a slide. It works by caching the last shape ID used and
+        incrementing that value to assign the next shape id. This avoids repeatedly searching all
+        shape ids in the slide each time a new ID is required.
+
+        Performance is not noticeably improved for a slide with a relatively small number of
+        shapes, but because the search time rises with the square of the shape count, this option
+        can be useful for optimizing generation of a slide composed of many shapes.
+
+        Shape-id collisions can occur (causing a repair error on load) if more than one |Slide|
+        object is used to interact with the same slide in the presentation. Note that the |Slides|
+        collection creates a new |Slide| object each time a slide is accessed (e.g. `slide =
+        prs.slides[0]`, so you must be careful to limit use to a single |Slide| object.
+        """
+        return self._cached_max_shape_id is not None
+
+    @turbo_add_enabled.setter
+    def turbo_add_enabled(self, value: bool):
+        enable = bool(value)
+        self._cached_max_shape_id = self._spTree.max_shape_id if enable else None
+
+    @staticmethod
+    def _is_member_elm(shape_elm: ShapeElement) -> bool:
+        """Return true if `shape_elm` represents a member of this collection, False otherwise."""
+        return True
+
+    def _iter_member_elms(self) -> Iterator[ShapeElement]:
+        """Generate each child of the `p:spTree` element that corresponds to a shape.
+
+        Items appear in XML document order.
+        """
+        for shape_elm in self._spTree.iter_shape_elms():
+            if self._is_member_elm(shape_elm):
+                yield shape_elm
+
+    def _next_ph_name(self, ph_type: PP_PLACEHOLDER, id: int, orient: str) -> str:
+        """Next unique placeholder name for placeholder shape of type `ph_type`.
+
+        Usually will be standard placeholder root name suffixed with id-1, e.g.
+        _next_ph_name(ST_PlaceholderType.TBL, 4, 'horz') ==> 'Table Placeholder 3'. The number is
+        incremented as necessary to make the name unique within the collection. If `orient` is
+        `'vert'`, the placeholder name is prefixed with `'Vertical '`.
+        """
+        basename = self.ph_basename(ph_type)
+
+        # prefix rootname with 'Vertical ' if orient is 'vert'
+        if orient == ST_Direction.VERT:
+            basename = "Vertical %s" % basename
+
+        # increment numpart as necessary to make name unique
+        numpart = id - 1
+        names = self._spTree.xpath("//p:cNvPr/@name")
+        while True:
+            name = "%s %d" % (basename, numpart)
+            if name not in names:
+                break
+            numpart += 1
+
+        return name
+
+    @property
+    def _next_shape_id(self) -> int:
+        """Return a unique shape id suitable for use with a new shape.
+
+        The returned id is 1 greater than the maximum shape id used so far. In practice, the
+        minimum id is 2 because the spTree element is always assigned id="1".
+        """
+        # ---presence of cached-max-shape-id indicates turbo mode is on---
+        if self._cached_max_shape_id is not None:
+            self._cached_max_shape_id += 1
+            return self._cached_max_shape_id
+
+        return self._spTree.max_shape_id + 1
+
+    def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+        """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+        return BaseShapeFactory(shape_elm, self)
+
+
+class _BaseGroupShapes(_BaseShapes):
+    """Base class for shape-trees that can add shapes."""
+
+    part: SlidePart  # pyright: ignore[reportIncompatibleMethodOverride]
+    _element: CT_GroupShape
+
+    def __init__(self, grpSp: CT_GroupShape, parent: ProvidesPart):
+        super(_BaseGroupShapes, self).__init__(grpSp, parent)
+        self._grpSp = grpSp
+
+    def add_chart(
+        self,
+        chart_type: XL_CHART_TYPE,
+        x: Length,
+        y: Length,
+        cx: Length,
+        cy: Length,
+        chart_data: ChartData,
+    ) -> Chart:
+        """Add a new chart of `chart_type` to the slide.
+
+        The chart is positioned at (`x`, `y`), has size (`cx`, `cy`), and depicts `chart_data`.
+        `chart_type` is one of the :ref:`XlChartType` enumeration values. `chart_data` is a
+        |ChartData| object populated with the categories and series values for the chart.
+
+        Note that a |GraphicFrame| shape object is returned, not the |Chart| object contained in
+        that graphic frame shape. The chart object may be accessed using the :attr:`chart`
+        property of the returned |GraphicFrame| object.
+        """
+        rId = self.part.add_chart_part(chart_type, chart_data)
+        graphicFrame = self._add_chart_graphicFrame(rId, x, y, cx, cy)
+        self._recalculate_extents()
+        return cast("Chart", self._shape_factory(graphicFrame))
+
+    def add_connector(
+        self,
+        connector_type: MSO_CONNECTOR_TYPE,
+        begin_x: Length,
+        begin_y: Length,
+        end_x: Length,
+        end_y: Length,
+    ) -> Connector:
+        """Add a newly created connector shape to the end of this shape tree.
+
+        `connector_type` is a member of the :ref:`MsoConnectorType` enumeration and the end-point
+        values are specified as EMU values. The returned connector is of type `connector_type` and
+        has begin and end points as specified.
+        """
+        cxnSp = self._add_cxnSp(connector_type, begin_x, begin_y, end_x, end_y)
+        self._recalculate_extents()
+        return cast(Connector, self._shape_factory(cxnSp))
+
+    def add_group_shape(self, shapes: Iterable[BaseShape] = ()) -> GroupShape:
+        """Return a |GroupShape| object newly appended to this shape tree.
+
+        The group shape is empty and must be populated with shapes using methods on its shape
+        tree, available on its `.shapes` property. The position and extents of the group shape are
+        determined by the shapes it contains; its position and extents are recalculated each time
+        a shape is added to it.
+        """
+        shapes = tuple(shapes)
+        grpSp = self._element.add_grpSp()
+        for shape in shapes:
+            grpSp.insert_element_before(
+                shape._element, "p:extLst"  # pyright: ignore[reportPrivateUsage]
+            )
+        if shapes:
+            grpSp.recalculate_extents()
+        return cast(GroupShape, self._shape_factory(grpSp))
+
+    def add_ole_object(
+        self,
+        object_file: str | IO[bytes],
+        prog_id: str,
+        left: Length,
+        top: Length,
+        width: Length | None = None,
+        height: Length | None = None,
+        icon_file: str | IO[bytes] | None = None,
+        icon_width: Length | None = None,
+        icon_height: Length | None = None,
+    ) -> GraphicFrame:
+        """Return newly-created GraphicFrame shape embedding `object_file`.
+
+        The returned graphic-frame shape contains `object_file` as an embedded OLE object. It is
+        displayed as an icon at `left`, `top` with size `width`, `height`. `width` and `height`
+        may be omitted when `prog_id` is a member of `PROG_ID`, in which case the default icon
+        size is used. This is advised for best appearance where applicable because it avoids an
+        icon with a "stretched" appearance.
+
+        `object_file` may either be a str path to a file or file-like object (such as
+        `io.BytesIO`) containing the bytes of the object to be embedded (such as an Excel file).
+
+        `prog_id` can be either a member of `pptx.enum.shapes.PROG_ID` or a str value like
+        `"Adobe.Exchange.7"` determined by inspecting the XML generated by PowerPoint for an
+        object of the desired type.
+
+        `icon_file` may either be a str path to an image file or a file-like object containing the
+        image. The image provided will be displayed in lieu of the OLE object; double-clicking on
+        the image opens the object (subject to operating-system limitations). The image file can
+        be any supported image file. Those produced by PowerPoint itself are generally EMF and can
+        be harvested from a PPTX package that embeds such an object. PNG and JPG also work fine.
+
+        `icon_width` and `icon_height` are `Length` values (e.g. Emu() or Inches()) that describe
+        the size of the icon image within the shape. These should be omitted unless a custom
+        `icon_file` is provided. The dimensions must be discovered by inspecting the XML.
+        Automatic resizing of the OLE-object shape can occur when the icon is double-clicked if
+        these values are not as set by PowerPoint. This behavior may only manifest in the Windows
+        version of PowerPoint.
+        """
+        graphicFrame = _OleObjectElementCreator.graphicFrame(
+            self,
+            self._next_shape_id,
+            object_file,
+            prog_id,
+            left,
+            top,
+            width,
+            height,
+            icon_file,
+            icon_width,
+            icon_height,
+        )
+        self._spTree.append(graphicFrame)
+        self._recalculate_extents()
+        return cast(GraphicFrame, self._shape_factory(graphicFrame))
+
+    def add_picture(
+        self,
+        image_file: str | IO[bytes],
+        left: Length,
+        top: Length,
+        width: Length | None = None,
+        height: Length | None = None,
+    ) -> Picture:
+        """Add picture shape displaying image in `image_file`.
+
+        `image_file` can be either a path to a file (a string) or a file-like object. The picture
+        is positioned with its top-left corner at (`top`, `left`). If `width` and `height` are
+        both |None|, the native size of the image is used. If only one of `width` or `height` is
+        used, the unspecified dimension is calculated to preserve the aspect ratio of the image.
+        If both are specified, the picture is stretched to fit, without regard to its native
+        aspect ratio.
+        """
+        image_part, rId = self.part.get_or_add_image_part(image_file)
+        pic = self._add_pic_from_image_part(image_part, rId, left, top, width, height)
+        self._recalculate_extents()
+        return cast(Picture, self._shape_factory(pic))
+
+    def add_shape(
+        self, autoshape_type_id: MSO_SHAPE, left: Length, top: Length, width: Length, height: Length
+    ) -> Shape:
+        """Return new |Shape| object appended to this shape tree.
+
+        `autoshape_type_id` is a member of :ref:`MsoAutoShapeType` e.g. `MSO_SHAPE.RECTANGLE`
+        specifying the type of shape to be added. The remaining arguments specify the new shape's
+        position and size.
+        """
+        autoshape_type = AutoShapeType(autoshape_type_id)
+        sp = self._add_sp(autoshape_type, left, top, width, height)
+        self._recalculate_extents()
+        return cast(Shape, self._shape_factory(sp))
+
+    def add_textbox(self, left: Length, top: Length, width: Length, height: Length) -> Shape:
+        """Return newly added text box shape appended to this shape tree.
+
+        The text box is of the specified size, located at the specified position on the slide.
+        """
+        sp = self._add_textbox_sp(left, top, width, height)
+        self._recalculate_extents()
+        return cast(Shape, self._shape_factory(sp))
+
+    def build_freeform(
+        self, start_x: float = 0, start_y: float = 0, scale: tuple[float, float] | float = 1.0
+    ) -> FreeformBuilder:
+        """Return |FreeformBuilder| object to specify a freeform shape.
+
+        The optional `start_x` and `start_y` arguments specify the starting pen position in local
+        coordinates. They will be rounded to the nearest integer before use and each default to
+        zero.
+
+        The optional `scale` argument specifies the size of local coordinates proportional to
+        slide coordinates (EMU). If the vertical scale is different than the horizontal scale
+        (local coordinate units are "rectangular"), a pair of numeric values can be provided as
+        the `scale` argument, e.g. `scale=(1.0, 2.0)`. In this case the first number is
+        interpreted as the horizontal (X) scale and the second as the vertical (Y) scale.
+
+        A convenient method for calculating scale is to divide a |Length| object by an equivalent
+        count of local coordinate units, e.g. `scale = Inches(1)/1000` for 1000 local units per
+        inch.
+        """
+        x_scale, y_scale = scale if isinstance(scale, tuple) else (scale, scale)
+
+        return FreeformBuilder.new(self, start_x, start_y, x_scale, y_scale)
+
+    def index(self, shape: BaseShape) -> int:
+        """Return the index of `shape` in this sequence.
+
+        Raises |ValueError| if `shape` is not in the collection.
+        """
+        shape_elms = list(self._element.iter_shape_elms())
+        return shape_elms.index(shape.element)
+
+    def _add_chart_graphicFrame(
+        self, rId: str, x: Length, y: Length, cx: Length, cy: Length
+    ) -> CT_GraphicalObjectFrame:
+        """Return new `p:graphicFrame` element appended to this shape tree.
+
+        The `p:graphicFrame` element has the specified position and size and refers to the chart
+        part identified by `rId`.
+        """
+        shape_id = self._next_shape_id
+        name = "Chart %d" % (shape_id - 1)
+        graphicFrame = CT_GraphicalObjectFrame.new_chart_graphicFrame(
+            shape_id, name, rId, x, y, cx, cy
+        )
+        self._spTree.append(graphicFrame)
+        return graphicFrame
+
+    def _add_cxnSp(
+        self,
+        connector_type: MSO_CONNECTOR_TYPE,
+        begin_x: Length,
+        begin_y: Length,
+        end_x: Length,
+        end_y: Length,
+    ) -> CT_Connector:
+        """Return a newly-added `p:cxnSp` element as specified.
+
+        The `p:cxnSp` element is for a connector of `connector_type` beginning at (`begin_x`,
+        `begin_y`) and extending to (`end_x`, `end_y`).
+        """
+        id_ = self._next_shape_id
+        name = "Connector %d" % (id_ - 1)
+
+        flipH, flipV = begin_x > end_x, begin_y > end_y
+        x, y = min(begin_x, end_x), min(begin_y, end_y)
+        cx, cy = abs(end_x - begin_x), abs(end_y - begin_y)
+
+        return self._element.add_cxnSp(id_, name, connector_type, x, y, cx, cy, flipH, flipV)
+
+    def _add_pic_from_image_part(
+        self,
+        image_part: ImagePart,
+        rId: str,
+        x: Length,
+        y: Length,
+        cx: Length | None,
+        cy: Length | None,
+    ) -> CT_Picture:
+        """Return a newly appended `p:pic` element as specified.
+
+        The `p:pic` element displays the image in `image_part` with size and position specified by
+        `x`, `y`, `cx`, and `cy`. The element is appended to the shape tree, causing it to be
+        displayed first in z-order on the slide.
+        """
+        id_ = self._next_shape_id
+        scaled_cx, scaled_cy = image_part.scale(cx, cy)
+        name = "Picture %d" % (id_ - 1)
+        desc = image_part.desc
+        pic = self._grpSp.add_pic(id_, name, desc, rId, x, y, scaled_cx, scaled_cy)
+        return pic
+
+    def _add_sp(
+        self, autoshape_type: AutoShapeType, x: Length, y: Length, cx: Length, cy: Length
+    ) -> CT_Shape:
+        """Return newly-added `p:sp` element as specified.
+
+        `p:sp` element is of `autoshape_type` at position (`x`, `y`) and of size (`cx`, `cy`).
+        """
+        id_ = self._next_shape_id
+        name = "%s %d" % (autoshape_type.basename, id_ - 1)
+        sp = self._grpSp.add_autoshape(id_, name, autoshape_type.prst, x, y, cx, cy)
+        return sp
+
+    def _add_textbox_sp(self, x: Length, y: Length, cx: Length, cy: Length) -> CT_Shape:
+        """Return newly-appended textbox `p:sp` element.
+
+        Element has position (`x`, `y`) and size (`cx`, `cy`).
+        """
+        id_ = self._next_shape_id
+        name = "TextBox %d" % (id_ - 1)
+        sp = self._spTree.add_textbox(id_, name, x, y, cx, cy)
+        return sp
+
+    def _recalculate_extents(self) -> None:
+        """Adjust position and size to incorporate all contained shapes.
+
+        This would typically be called when a contained shape is added, removed, or its position
+        or size updated.
+        """
+        # ---default behavior is to do nothing, GroupShapes overrides to
+        #    produce the distinctive behavior of groups and subgroups.---
+        pass
+
+
+class GroupShapes(_BaseGroupShapes):
+    """The sequence of child shapes belonging to a group shape.
+
+    Note that this collection can itself contain a group shape, making this part of a recursive,
+    tree data structure (acyclic graph).
+    """
+
+    def _recalculate_extents(self) -> None:
+        """Adjust position and size to incorporate all contained shapes.
+
+        This would typically be called when a contained shape is added, removed, or its position
+        or size updated.
+        """
+        self._grpSp.recalculate_extents()
+
+
+class SlideShapes(_BaseGroupShapes):
+    """Sequence of shapes appearing on a slide.
+
+    The first shape in the sequence is the backmost in z-order and the last shape is topmost.
+    Supports indexed access, len(), index(), and iteration.
+    """
+
+    parent: Slide  # pyright: ignore[reportIncompatibleMethodOverride]
+
+    def add_movie(
+        self,
+        movie_file: str | IO[bytes],
+        left: Length,
+        top: Length,
+        width: Length,
+        height: Length,
+        poster_frame_image: str | IO[bytes] | None = None,
+        mime_type: str = CT.VIDEO,
+    ) -> GraphicFrame:
+        """Return newly added movie shape displaying video in `movie_file`.
+
+        **EXPERIMENTAL.** This method has important limitations:
+
+        * The size must be specified; no auto-scaling such as that provided by :meth:`add_picture`
+          is performed.
+        * The MIME type of the video file should be specified, e.g. 'video/mp4'. The provided
+          video file is not interrogated for its type. The MIME type `video/unknown` is used by
+          default (and works fine in tests as of this writing).
+        * A poster frame image must be provided, it cannot be automatically extracted from the
+          video file. If no poster frame is provided, the default "media loudspeaker" image will
+          be used.
+
+        Return a newly added movie shape to the slide, positioned at (`left`, `top`), having size
+        (`width`, `height`), and containing `movie_file`. Before the video is started,
+        `poster_frame_image` is displayed as a placeholder for the video.
+        """
+        movie_pic = _MoviePicElementCreator.new_movie_pic(
+            self,
+            self._next_shape_id,
+            movie_file,
+            left,
+            top,
+            width,
+            height,
+            poster_frame_image,
+            mime_type,
+        )
+        self._spTree.append(movie_pic)
+        self._add_video_timing(movie_pic)
+        return cast(GraphicFrame, self._shape_factory(movie_pic))
+
+    def add_table(
+        self, rows: int, cols: int, left: Length, top: Length, width: Length, height: Length
+    ) -> GraphicFrame:
+        """Add a |GraphicFrame| object containing a table.
+
+        The table has the specified number of `rows` and `cols` and the specified position and
+        size. `width` is evenly distributed between the columns of the new table. Likewise,
+        `height` is evenly distributed between the rows. Note that the `.table` property on the
+        returned |GraphicFrame| shape must be used to access the enclosed |Table| object.
+        """
+        graphicFrame = self._add_graphicFrame_containing_table(rows, cols, left, top, width, height)
+        return cast(GraphicFrame, self._shape_factory(graphicFrame))
+
+    def clone_layout_placeholders(self, slide_layout: SlideLayout) -> None:
+        """Add placeholder shapes based on those in `slide_layout`.
+
+        Z-order of placeholders is preserved. Latent placeholders (date, slide number, and footer)
+        are not cloned.
+        """
+        for placeholder in slide_layout.iter_cloneable_placeholders():
+            self.clone_placeholder(placeholder)
+
+    @property
+    def placeholders(self) -> SlidePlaceholders:
+        """Sequence of placeholder shapes in this slide."""
+        return self.parent.placeholders
+
+    @property
+    def title(self) -> Shape | None:
+        """The title placeholder shape on the slide.
+
+        |None| if the slide has no title placeholder.
+        """
+        for elm in self._spTree.iter_ph_elms():
+            if elm.ph_idx == 0:
+                return cast(Shape, self._shape_factory(elm))
+        return None
+
+    def _add_graphicFrame_containing_table(
+        self, rows: int, cols: int, x: Length, y: Length, cx: Length, cy: Length
+    ) -> CT_GraphicalObjectFrame:
+        """Return a newly added `p:graphicFrame` element containing a table as specified."""
+        _id = self._next_shape_id
+        name = "Table %d" % (_id - 1)
+        graphicFrame = self._spTree.add_table(_id, name, rows, cols, x, y, cx, cy)
+        return graphicFrame
+
+    def _add_video_timing(self, pic: CT_Picture) -> None:
+        """Add a `p:video` element under `p:sld/p:timing`.
+
+        The element will refer to the specified `pic` element by its shape id, and cause the video
+        play controls to appear for that video.
+        """
+        sld = self._spTree.xpath("/p:sld")[0]
+        childTnLst = sld.get_or_add_childTnLst()
+        childTnLst.add_video(pic.shape_id)
+
+    def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+        """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+        return SlideShapeFactory(shape_elm, self)
+
+
+class LayoutShapes(_BaseShapes):
+    """Sequence of shapes appearing on a slide layout.
+
+    The first shape in the sequence is the backmost in z-order and the last shape is topmost.
+    Supports indexed access, len(), index(), and iteration.
+    """
+
+    def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+        """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+        return _LayoutShapeFactory(shape_elm, self)
+
+
+class MasterShapes(_BaseShapes):
+    """Sequence of shapes appearing on a slide master.
+
+    The first shape in the sequence is the backmost in z-order and the last shape is topmost.
+    Supports indexed access, len(), and iteration.
+    """
+
+    def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+        """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+        return _MasterShapeFactory(shape_elm, self)
+
+
+class NotesSlideShapes(_BaseShapes):
+    """Sequence of shapes appearing on a notes slide.
+
+    The first shape in the sequence is the backmost in z-order and the last shape is topmost.
+    Supports indexed access, len(), index(), and iteration.
+    """
+
+    def ph_basename(self, ph_type: PP_PLACEHOLDER) -> str:
+        """Return the base name for a placeholder of `ph_type` in this shape collection.
+
+        A notes slide uses a different name for the body placeholder and has some unique
+        placeholder types, so this method overrides the default in the base class.
+        """
+        return {
+            PP_PLACEHOLDER.BODY: "Notes Placeholder",
+            PP_PLACEHOLDER.DATE: "Date Placeholder",
+            PP_PLACEHOLDER.FOOTER: "Footer Placeholder",
+            PP_PLACEHOLDER.HEADER: "Header Placeholder",
+            PP_PLACEHOLDER.SLIDE_IMAGE: "Slide Image Placeholder",
+            PP_PLACEHOLDER.SLIDE_NUMBER: "Slide Number Placeholder",
+        }[ph_type]
+
+    def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+        """Return appropriate shape object for `shape_elm` appearing on a notes slide."""
+        return _NotesSlideShapeFactory(shape_elm, self)
+
+
+class BasePlaceholders(_BaseShapes):
+    """Base class for placeholder collections.
+
+    Subclasses differentiate behaviors for a master, layout, and slide. By default, placeholder
+    shapes are constructed using |BaseShapeFactory|. Subclasses should override
+    :method:`_shape_factory` to use custom placeholder classes.
+    """
+
+    @staticmethod
+    def _is_member_elm(shape_elm: ShapeElement) -> bool:
+        """True if `shape_elm` is a placeholder shape, False otherwise."""
+        return shape_elm.has_ph_elm
+
+
+class LayoutPlaceholders(BasePlaceholders):
+    """Sequence of |LayoutPlaceholder| instance for each placeholder shape on a slide layout."""
+
+    __iter__: Callable[  # pyright: ignore[reportIncompatibleMethodOverride]
+        [], Iterator[LayoutPlaceholder]
+    ]
+
+    def get(self, idx: int, default: LayoutPlaceholder | None = None) -> LayoutPlaceholder | None:
+        """The first placeholder shape with matching `idx` value, or `default` if not found."""
+        for placeholder in self:
+            if placeholder.element.ph_idx == idx:
+                return placeholder
+        return default
+
+    def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+        """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+        return _LayoutShapeFactory(shape_elm, self)
+
+
+class MasterPlaceholders(BasePlaceholders):
+    """Sequence of MasterPlaceholder representing the placeholder shapes on a slide master."""
+
+    __iter__: Callable[  # pyright: ignore[reportIncompatibleMethodOverride]
+        [], Iterator[MasterPlaceholder]
+    ]
+
+    def get(self, ph_type: PP_PLACEHOLDER, default: MasterPlaceholder | None = None):
+        """Return the first placeholder shape with type `ph_type` (e.g. 'body').
+
+        Returns `default` if no such placeholder shape is present in the collection.
+        """
+        for placeholder in self:
+            if placeholder.ph_type == ph_type:
+                return placeholder
+        return default
+
+    def _shape_factory(  # pyright: ignore[reportIncompatibleMethodOverride]
+        self, placeholder_elm: CT_Shape
+    ) -> MasterPlaceholder:
+        """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+        return cast(MasterPlaceholder, _MasterShapeFactory(placeholder_elm, self))
+
+
+class NotesSlidePlaceholders(MasterPlaceholders):
+    """Sequence of placeholder shapes on a notes slide."""
+
+    __iter__: Callable[  # pyright: ignore[reportIncompatibleMethodOverride]
+        [], Iterator[NotesSlidePlaceholder]
+    ]
+
+    def _shape_factory(  # pyright: ignore[reportIncompatibleMethodOverride]
+        self, placeholder_elm: CT_Shape
+    ) -> NotesSlidePlaceholder:
+        """Return an instance of the appropriate placeholder proxy class for `placeholder_elm`."""
+        return cast(NotesSlidePlaceholder, _NotesSlideShapeFactory(placeholder_elm, self))
+
+
+class SlidePlaceholders(ParentedElementProxy):
+    """Collection of placeholder shapes on a slide.
+
+    Supports iteration, :func:`len`, and dictionary-style lookup on the `idx` value of the
+    placeholders it contains.
+    """
+
+    _element: CT_GroupShape
+
+    def __getitem__(self, idx: int):
+        """Access placeholder shape having `idx`.
+
+        Note that while this looks like list access, idx is actually a dictionary key and will
+        raise |KeyError| if no placeholder with that idx value is in the collection.
+        """
+        for e in self._element.iter_ph_elms():
+            if e.ph_idx == idx:
+                return SlideShapeFactory(e, self)
+        raise KeyError("no placeholder on this slide with idx == %d" % idx)
+
+    def __iter__(self):
+        """Generate placeholder shapes in `idx` order."""
+        ph_elms = sorted([e for e in self._element.iter_ph_elms()], key=lambda e: e.ph_idx)
+        return (SlideShapeFactory(e, self) for e in ph_elms)
+
+    def __len__(self) -> int:
+        """Return count of placeholder shapes."""
+        return len(list(self._element.iter_ph_elms()))
+
+
+def BaseShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
+    """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+    tag = shape_elm.tag
+
+    if isinstance(shape_elm, CT_Picture):
+        videoFiles = shape_elm.xpath("./p:nvPicPr/p:nvPr/a:videoFile")
+        if videoFiles:
+            return Movie(shape_elm, parent)
+        return Picture(shape_elm, parent)
+
+    shape_cls = {
+        qn("p:cxnSp"): Connector,
+        qn("p:grpSp"): GroupShape,
+        qn("p:sp"): Shape,
+        qn("p:graphicFrame"): GraphicFrame,
+    }.get(tag, BaseShape)
+
+    return shape_cls(shape_elm, parent)  # pyright: ignore[reportArgumentType]
+
+
+def _LayoutShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
+    """Return appropriate shape object for `shape_elm` on a slide layout."""
+    if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm:
+        return LayoutPlaceholder(shape_elm, parent)
+    return BaseShapeFactory(shape_elm, parent)
+
+
+def _MasterShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
+    """Return appropriate shape object for `shape_elm` on a slide master."""
+    if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm:
+        return MasterPlaceholder(shape_elm, parent)
+    return BaseShapeFactory(shape_elm, parent)
+
+
+def _NotesSlideShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
+    """Return appropriate shape object for `shape_elm` on a notes slide."""
+    if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm:
+        return NotesSlidePlaceholder(shape_elm, parent)
+    return BaseShapeFactory(shape_elm, parent)
+
+
+def _SlidePlaceholderFactory(shape_elm: ShapeElement, parent: ProvidesPart):
+    """Return a placeholder shape of the appropriate type for `shape_elm`."""
+    tag = shape_elm.tag
+    if tag == qn("p:sp"):
+        Constructor = {
+            PP_PLACEHOLDER.BITMAP: PicturePlaceholder,
+            PP_PLACEHOLDER.CHART: ChartPlaceholder,
+            PP_PLACEHOLDER.PICTURE: PicturePlaceholder,
+            PP_PLACEHOLDER.TABLE: TablePlaceholder,
+        }.get(shape_elm.ph_type, SlidePlaceholder)
+    elif tag == qn("p:graphicFrame"):
+        Constructor = PlaceholderGraphicFrame
+    elif tag == qn("p:pic"):
+        Constructor = PlaceholderPicture
+    else:
+        Constructor = BaseShapeFactory
+    return Constructor(shape_elm, parent)  # pyright: ignore[reportArgumentType]
+
+
+def SlideShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
+    """Return appropriate shape object for `shape_elm` on a slide."""
+    if shape_elm.has_ph_elm:
+        return _SlidePlaceholderFactory(shape_elm, parent)
+    return BaseShapeFactory(shape_elm, parent)
+
+
+class _MoviePicElementCreator(object):
+    """Functional service object for creating a new movie p:pic element.
+
+    It's entire external interface is its :meth:`new_movie_pic` class method that returns a new
+    `p:pic` element containing the specified video. This class is not intended to be constructed
+    or an instance of it retained by the caller; it is a "one-shot" object, really a function
+    wrapped in a object such that its helper methods can be organized here.
+    """
+
+    def __init__(
+        self,
+        shapes: SlideShapes,
+        shape_id: int,
+        movie_file: str | IO[bytes],
+        x: Length,
+        y: Length,
+        cx: Length,
+        cy: Length,
+        poster_frame_file: str | IO[bytes] | None,
+        mime_type: str | None,
+    ):
+        super(_MoviePicElementCreator, self).__init__()
+        self._shapes = shapes
+        self._shape_id = shape_id
+        self._movie_file = movie_file
+        self._x, self._y, self._cx, self._cy = x, y, cx, cy
+        self._poster_frame_file = poster_frame_file
+        self._mime_type = mime_type
+
+    @classmethod
+    def new_movie_pic(
+        cls,
+        shapes: SlideShapes,
+        shape_id: int,
+        movie_file: str | IO[bytes],
+        x: Length,
+        y: Length,
+        cx: Length,
+        cy: Length,
+        poster_frame_image: str | IO[bytes] | None,
+        mime_type: str | None,
+    ) -> CT_Picture:
+        """Return a new `p:pic` element containing video in `movie_file`.
+
+        If `mime_type` is None, 'video/unknown' is used. If `poster_frame_file` is None, the
+        default "media loudspeaker" image is used.
+        """
+        return cls(shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_image, mime_type)._pic
+
+    @property
+    def _media_rId(self) -> str:
+        """Return the rId of RT.MEDIA relationship to video part.
+
+        For historical reasons, there are two relationships to the same part; one is the video rId
+        and the other is the media rId.
+        """
+        return self._video_part_rIds[0]
+
+    @lazyproperty
+    def _pic(self) -> CT_Picture:
+        """Return the new `p:pic` element referencing the video."""
+        return CT_Picture.new_video_pic(
+            self._shape_id,
+            self._shape_name,
+            self._video_rId,
+            self._media_rId,
+            self._poster_frame_rId,
+            self._x,
+            self._y,
+            self._cx,
+            self._cy,
+        )
+
+    @lazyproperty
+    def _poster_frame_image_file(self) -> str | IO[bytes]:
+        """Return the image file for video placeholder image.
+
+        If no poster frame file is provided, the default "media loudspeaker" image is used.
+        """
+        poster_frame_file = self._poster_frame_file
+        if poster_frame_file is None:
+            return io.BytesIO(SPEAKER_IMAGE_BYTES)
+        return poster_frame_file
+
+    @lazyproperty
+    def _poster_frame_rId(self) -> str:
+        """Return the rId of relationship to poster frame image.
+
+        The poster frame is the image used to represent the video before it's played.
+        """
+        _, poster_frame_rId = self._slide_part.get_or_add_image_part(self._poster_frame_image_file)
+        return poster_frame_rId
+
+    @property
+    def _shape_name(self) -> str:
+        """Return the appropriate shape name for the p:pic shape.
+
+        A movie shape is named with the base filename of the video.
+        """
+        return self._video.filename
+
+    @property
+    def _slide_part(self) -> SlidePart:
+        """Return SlidePart object for slide containing this movie."""
+        return self._shapes.part
+
+    @lazyproperty
+    def _video(self) -> Video:
+        """Return a |Video| object containing the movie file."""
+        return Video.from_path_or_file_like(self._movie_file, self._mime_type)
+
+    @lazyproperty
+    def _video_part_rIds(self) -> tuple[str, str]:
+        """Return the rIds for relationships to media part for video.
+
+        This is where the media part and its relationships to the slide are actually created.
+        """
+        media_rId, video_rId = self._slide_part.get_or_add_video_media_part(self._video)
+        return media_rId, video_rId
+
+    @property
+    def _video_rId(self) -> str:
+        """Return the rId of RT.VIDEO relationship to video part.
+
+        For historical reasons, there are two relationships to the same part; one is the video rId
+        and the other is the media rId.
+        """
+        return self._video_part_rIds[1]
+
+
+class _OleObjectElementCreator(object):
+    """Functional service object for creating a new OLE-object p:graphicFrame element.
+
+    It's entire external interface is its :meth:`graphicFrame` class method that returns a new
+    `p:graphicFrame` element containing the specified embedded OLE-object shape. This class is not
+    intended to be constructed or an instance of it retained by the caller; it is a "one-shot"
+    object, really a function wrapped in a object such that its helper methods can be organized
+    here.
+    """
+
+    def __init__(
+        self,
+        shapes: _BaseGroupShapes,
+        shape_id: int,
+        ole_object_file: str | IO[bytes],
+        prog_id: PROG_ID | str,
+        x: Length,
+        y: Length,
+        cx: Length | None,
+        cy: Length | None,
+        icon_file: str | IO[bytes] | None,
+        icon_width: Length | None,
+        icon_height: Length | None,
+    ):
+        self._shapes = shapes
+        self._shape_id = shape_id
+        self._ole_object_file = ole_object_file
+        self._prog_id_arg = prog_id
+        self._x = x
+        self._y = y
+        self._cx_arg = cx
+        self._cy_arg = cy
+        self._icon_file_arg = icon_file
+        self._icon_width_arg = icon_width
+        self._icon_height_arg = icon_height
+
+    @classmethod
+    def graphicFrame(
+        cls,
+        shapes: _BaseGroupShapes,
+        shape_id: int,
+        ole_object_file: str | IO[bytes],
+        prog_id: PROG_ID | str,
+        x: Length,
+        y: Length,
+        cx: Length | None,
+        cy: Length | None,
+        icon_file: str | IO[bytes] | None,
+        icon_width: Length | None,
+        icon_height: Length | None,
+    ) -> CT_GraphicalObjectFrame:
+        """Return new `p:graphicFrame` element containing embedded `ole_object_file`."""
+        return cls(
+            shapes,
+            shape_id,
+            ole_object_file,
+            prog_id,
+            x,
+            y,
+            cx,
+            cy,
+            icon_file,
+            icon_width,
+            icon_height,
+        )._graphicFrame
+
+    @lazyproperty
+    def _graphicFrame(self) -> CT_GraphicalObjectFrame:
+        """Newly-created `p:graphicFrame` element referencing embedded OLE-object."""
+        return CT_GraphicalObjectFrame.new_ole_object_graphicFrame(
+            self._shape_id,
+            self._shape_name,
+            self._ole_object_rId,
+            self._progId,
+            self._icon_rId,
+            self._x,
+            self._y,
+            self._cx,
+            self._cy,
+            self._icon_width,
+            self._icon_height,
+        )
+
+    @lazyproperty
+    def _cx(self) -> Length:
+        """Emu object specifying width of "show-as-icon" image for OLE shape."""
+        # --- a user-specified width overrides any default ---
+        if self._cx_arg is not None:
+            return self._cx_arg
+
+        # --- the default width is specified by the PROG_ID member if prog_id is one,
+        # --- otherwise it gets the default icon width.
+        return (
+            Emu(self._prog_id_arg.width) if isinstance(self._prog_id_arg, PROG_ID) else Emu(965200)
+        )
+
+    @lazyproperty
+    def _cy(self) -> Length:
+        """Emu object specifying height of "show-as-icon" image for OLE shape."""
+        # --- a user-specified width overrides any default ---
+        if self._cy_arg is not None:
+            return self._cy_arg
+
+        # --- the default height is specified by the PROG_ID member if prog_id is one,
+        # --- otherwise it gets the default icon height.
+        return (
+            Emu(self._prog_id_arg.height) if isinstance(self._prog_id_arg, PROG_ID) else Emu(609600)
+        )
+
+    @lazyproperty
+    def _icon_height(self) -> Length:
+        """Vertical size of enclosed EMF icon within the OLE graphic-frame.
+
+        This must be specified when a custom icon is used, to avoid stretching of the image and
+        possible undesired resizing by PowerPoint when the OLE shape is double-clicked to open it.
+
+        The correct size can be determined by creating an example PPTX using PowerPoint and then
+        inspecting the XML of the OLE graphics-frame (p:oleObj.imgH).
+        """
+        return self._icon_height_arg if self._icon_height_arg is not None else Emu(609600)
+
+    @lazyproperty
+    def _icon_image_file(self) -> str | IO[bytes]:
+        """Reference to image file containing icon to show in lieu of this object.
+
+        This can be either a str path or a file-like object (io.BytesIO typically).
+        """
+        # --- a user-specified icon overrides any default ---
+        if self._icon_file_arg is not None:
+            return self._icon_file_arg
+
+        # --- A prog_id belonging to PROG_ID gets its icon filename from there. A
+        # --- user-specified (str) prog_id gets the default icon.
+        icon_filename = (
+            self._prog_id_arg.icon_filename
+            if isinstance(self._prog_id_arg, PROG_ID)
+            else "generic-icon.emf"
+        )
+
+        _thisdir = os.path.split(__file__)[0]
+        return os.path.abspath(os.path.join(_thisdir, "..", "templates", icon_filename))
+
+    @lazyproperty
+    def _icon_rId(self) -> str:
+        """str rId like "rId7" of rel to icon (image) representing OLE-object part."""
+        _, rId = self._slide_part.get_or_add_image_part(self._icon_image_file)
+        return rId
+
+    @lazyproperty
+    def _icon_width(self) -> Length:
+        """Width of enclosed EMF icon within the OLE graphic-frame.
+
+        This must be specified when a custom icon is used, to avoid stretching of the image and
+        possible undesired resizing by PowerPoint when the OLE shape is double-clicked to open it.
+        """
+        return self._icon_width_arg if self._icon_width_arg is not None else Emu(965200)
+
+    @lazyproperty
+    def _ole_object_rId(self) -> str:
+        """str rId like "rId6" of relationship to embedded ole_object part.
+
+        This is where the ole_object part and its relationship to the slide are actually created.
+        """
+        return self._slide_part.add_embedded_ole_object_part(
+            self._prog_id_arg, self._ole_object_file
+        )
+
+    @lazyproperty
+    def _progId(self) -> str:
+        """str like "Excel.Sheet.12" identifying program used to open object.
+
+        This value appears in the `progId` attribute of the `p:oleObj` element for the object.
+        """
+        prog_id_arg = self._prog_id_arg
+
+        # --- member of PROG_ID enumeration knows its progId keyphrase, otherwise caller
+        # --- has specified it explicitly (as str)
+        return prog_id_arg.progId if isinstance(prog_id_arg, PROG_ID) else prog_id_arg
+
+    @lazyproperty
+    def _shape_name(self) -> str:
+        """str name like "Object 1" for the embedded ole_object shape.
+
+        The name is formed from the prefix "Object " and the shape-id decremented by 1.
+        """
+        return "Object %d" % (self._shape_id - 1)
+
+    @lazyproperty
+    def _slide_part(self) -> SlidePart:
+        """SlidePart object for this slide."""
+        return self._shapes.part
diff --git a/.venv/lib/python3.12/site-packages/pptx/shared.py b/.venv/lib/python3.12/site-packages/pptx/shared.py
new file mode 100644
index 00000000..da2a1718
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shared.py
@@ -0,0 +1,82 @@
+"""Objects shared by pptx modules."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from pptx.opc.package import XmlPart
+    from pptx.oxml.xmlchemy import BaseOxmlElement
+    from pptx.types import ProvidesPart
+
+
+class ElementProxy(object):
+    """Base class for lxml element proxy classes.
+
+    An element proxy class is one whose primary responsibilities are fulfilled by manipulating the
+    attributes and child elements of an XML element. They are the most common type of class in
+    python-pptx other than custom element (oxml) classes.
+    """
+
+    def __init__(self, element: BaseOxmlElement):
+        self._element = element
+
+    def __eq__(self, other: object) -> bool:
+        """Return |True| if this proxy object refers to the same oxml element as does *other*.
+
+        ElementProxy objects are value objects and should maintain no mutable local state.
+        Equality for proxy objects is defined as referring to the same XML element, whether or not
+        they are the same proxy object instance.
+        """
+        if not isinstance(other, ElementProxy):
+            return False
+        return self._element is other._element
+
+    def __ne__(self, other: object) -> bool:
+        if not isinstance(other, ElementProxy):
+            return True
+        return self._element is not other._element
+
+    @property
+    def element(self):
+        """The lxml element proxied by this object."""
+        return self._element
+
+
+class ParentedElementProxy(ElementProxy):
+    """Provides access to ancestor objects and part.
+
+    An ancestor may occasionally be required to provide a service, such as add or drop a
+    relationship. Provides the :attr:`_parent` attribute to subclasses and the public
+    :attr:`parent` read-only property.
+    """
+
+    def __init__(self, element: BaseOxmlElement, parent: ProvidesPart):
+        super(ParentedElementProxy, self).__init__(element)
+        self._parent = parent
+
+    @property
+    def parent(self):
+        """The ancestor proxy object to this one.
+
+        For example, the parent of a shape is generally the |SlideShapes| object that contains it.
+        """
+        return self._parent
+
+    @property
+    def part(self) -> XmlPart:
+        """The package part containing this object."""
+        return self._parent.part
+
+
+class PartElementProxy(ElementProxy):
+    """Provides common members for proxy-objects that wrap a part's root element, e.g. `p:sld`."""
+
+    def __init__(self, element: BaseOxmlElement, part: XmlPart):
+        super(PartElementProxy, self).__init__(element)
+        self._part = part
+
+    @property
+    def part(self) -> XmlPart:
+        """The package part containing this object."""
+        return self._part
diff --git a/.venv/lib/python3.12/site-packages/pptx/slide.py b/.venv/lib/python3.12/site-packages/pptx/slide.py
new file mode 100644
index 00000000..3b1b65d8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/slide.py
@@ -0,0 +1,498 @@
+"""Slide-related objects, including masters, layouts, and notes."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Iterator, cast
+
+from pptx.dml.fill import FillFormat
+from pptx.enum.shapes import PP_PLACEHOLDER
+from pptx.shapes.shapetree import (
+    LayoutPlaceholders,
+    LayoutShapes,
+    MasterPlaceholders,
+    MasterShapes,
+    NotesSlidePlaceholders,
+    NotesSlideShapes,
+    SlidePlaceholders,
+    SlideShapes,
+)
+from pptx.shared import ElementProxy, ParentedElementProxy, PartElementProxy
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.oxml.presentation import CT_SlideIdList, CT_SlideMasterIdList
+    from pptx.oxml.slide import (
+        CT_CommonSlideData,
+        CT_NotesSlide,
+        CT_Slide,
+        CT_SlideLayoutIdList,
+        CT_SlideMaster,
+    )
+    from pptx.parts.presentation import PresentationPart
+    from pptx.parts.slide import SlideLayoutPart, SlideMasterPart, SlidePart
+    from pptx.presentation import Presentation
+    from pptx.shapes.placeholder import LayoutPlaceholder, MasterPlaceholder
+    from pptx.shapes.shapetree import NotesSlidePlaceholder
+    from pptx.text.text import TextFrame
+
+
+class _BaseSlide(PartElementProxy):
+    """Base class for slide objects, including masters, layouts and notes."""
+
+    _element: CT_Slide
+
+    @lazyproperty
+    def background(self) -> _Background:
+        """|_Background| object providing slide background properties.
+
+        This property returns a |_Background| object whether or not the
+        slide, master, or layout has an explicitly defined background.
+
+        The same |_Background| object is returned on every call for the same
+        slide object.
+        """
+        return _Background(self._element.cSld)
+
+    @property
+    def name(self) -> str:
+        """String representing the internal name of this slide.
+
+        Returns an empty string (`''`) if no name is assigned. Assigning an empty string or |None|
+        to this property causes any name to be removed.
+        """
+        return self._element.cSld.name
+
+    @name.setter
+    def name(self, value: str | None):
+        new_value = "" if value is None else value
+        self._element.cSld.name = new_value
+
+
+class _BaseMaster(_BaseSlide):
+    """Base class for master objects such as |SlideMaster| and |NotesMaster|.
+
+    Provides access to placeholders and regular shapes.
+    """
+
+    @lazyproperty
+    def placeholders(self) -> MasterPlaceholders:
+        """|MasterPlaceholders| collection of placeholder shapes in this master.
+
+        Sequence sorted in `idx` order.
+        """
+        return MasterPlaceholders(self._element.spTree, self)
+
+    @lazyproperty
+    def shapes(self):
+        """
+        Instance of |MasterShapes| containing sequence of shape objects
+        appearing on this slide.
+        """
+        return MasterShapes(self._element.spTree, self)
+
+
+class NotesMaster(_BaseMaster):
+    """Proxy for the notes master XML document.
+
+    Provides access to shapes, the most commonly used of which are placeholders.
+    """
+
+
+class NotesSlide(_BaseSlide):
+    """Notes slide object.
+
+    Provides access to slide notes placeholder and other shapes on the notes handout
+    page.
+    """
+
+    element: CT_NotesSlide  # pyright: ignore[reportIncompatibleMethodOverride]
+
+    def clone_master_placeholders(self, notes_master: NotesMaster) -> None:
+        """Selectively add placeholder shape elements from `notes_master`.
+
+        Selected placeholder shape elements from `notes_master` are added to the shapes
+        collection of this notes slide. Z-order of placeholders is preserved. Certain
+        placeholders (header, date, footer) are not cloned.
+        """
+
+        def iter_cloneable_placeholders() -> Iterator[MasterPlaceholder]:
+            """Generate a reference to each cloneable placeholder in `notes_master`.
+
+            These are the placeholders that should be cloned to a notes slide when the a new notes
+            slide is created.
+            """
+            cloneable = (
+                PP_PLACEHOLDER.SLIDE_IMAGE,
+                PP_PLACEHOLDER.BODY,
+                PP_PLACEHOLDER.SLIDE_NUMBER,
+            )
+            for placeholder in notes_master.placeholders:
+                if placeholder.element.ph_type in cloneable:
+                    yield placeholder
+
+        shapes = self.shapes
+        for placeholder in iter_cloneable_placeholders():
+            shapes.clone_placeholder(cast("LayoutPlaceholder", placeholder))
+
+    @property
+    def notes_placeholder(self) -> NotesSlidePlaceholder | None:
+        """the notes placeholder on this notes slide, the shape that contains the actual notes text.
+
+        Return |None| if no notes placeholder is present; while this is probably uncommon, it can
+        happen if the notes master does not have a body placeholder, or if the notes placeholder
+        has been deleted from the notes slide.
+        """
+        for placeholder in self.placeholders:
+            if placeholder.placeholder_format.type == PP_PLACEHOLDER.BODY:
+                return placeholder
+        return None
+
+    @property
+    def notes_text_frame(self) -> TextFrame | None:
+        """The text frame of the notes placeholder on this notes slide.
+
+        |None| if there is no notes placeholder. This is a shortcut to accommodate the common case
+        of simply adding "notes" text to the notes "page".
+        """
+        notes_placeholder = self.notes_placeholder
+        if notes_placeholder is None:
+            return None
+        return notes_placeholder.text_frame
+
+    @lazyproperty
+    def placeholders(self) -> NotesSlidePlaceholders:
+        """Instance of |NotesSlidePlaceholders| for this notes-slide.
+
+        Contains the sequence of placeholder shapes in this notes slide.
+        """
+        return NotesSlidePlaceholders(self.element.spTree, self)
+
+    @lazyproperty
+    def shapes(self) -> NotesSlideShapes:
+        """Sequence of shape objects appearing on this notes slide."""
+        return NotesSlideShapes(self._element.spTree, self)
+
+
+class Slide(_BaseSlide):
+    """Slide object. Provides access to shapes and slide-level properties."""
+
+    part: SlidePart  # pyright: ignore[reportIncompatibleMethodOverride]
+
+    @property
+    def follow_master_background(self):
+        """|True| if this slide inherits the slide master background.
+
+        Assigning |False| causes background inheritance from the master to be
+        interrupted; if there is no custom background for this slide,
+        a default background is added. If a custom background already exists
+        for this slide, assigning |False| has no effect.
+
+        Assigning |True| causes any custom background for this slide to be
+        deleted and inheritance from the master restored.
+        """
+        return self._element.bg is None
+
+    @property
+    def has_notes_slide(self) -> bool:
+        """`True` if this slide has a notes slide, `False` otherwise.
+
+        A notes slide is created by :attr:`.notes_slide` when one doesn't exist; use this property
+        to test for a notes slide without the possible side effect of creating one.
+        """
+        return self.part.has_notes_slide
+
+    @property
+    def notes_slide(self) -> NotesSlide:
+        """The |NotesSlide| instance for this slide.
+
+        If the slide does not have a notes slide, one is created. The same single instance is
+        returned on each call.
+        """
+        return self.part.notes_slide
+
+    @lazyproperty
+    def placeholders(self) -> SlidePlaceholders:
+        """Sequence of placeholder shapes in this slide."""
+        return SlidePlaceholders(self._element.spTree, self)
+
+    @lazyproperty
+    def shapes(self) -> SlideShapes:
+        """Sequence of shape objects appearing on this slide."""
+        return SlideShapes(self._element.spTree, self)
+
+    @property
+    def slide_id(self) -> int:
+        """Integer value that uniquely identifies this slide within this presentation.
+
+        The slide id does not change if the position of this slide in the slide sequence is changed
+        by adding, rearranging, or deleting slides.
+        """
+        return self.part.slide_id
+
+    @property
+    def slide_layout(self) -> SlideLayout:
+        """|SlideLayout| object this slide inherits appearance from."""
+        return self.part.slide_layout
+
+
+class Slides(ParentedElementProxy):
+    """Sequence of slides belonging to an instance of |Presentation|.
+
+    Has list semantics for access to individual slides. Supports indexed access, len(), and
+    iteration.
+    """
+
+    part: PresentationPart  # pyright: ignore[reportIncompatibleMethodOverride]
+
+    def __init__(self, sldIdLst: CT_SlideIdList, prs: Presentation):
+        super(Slides, self).__init__(sldIdLst, prs)
+        self._sldIdLst = sldIdLst
+
+    def __getitem__(self, idx: int) -> Slide:
+        """Provide indexed access, (e.g. 'slides[0]')."""
+        try:
+            sldId = self._sldIdLst.sldId_lst[idx]
+        except IndexError:
+            raise IndexError("slide index out of range")
+        return self.part.related_slide(sldId.rId)
+
+    def __iter__(self) -> Iterator[Slide]:
+        """Support iteration, e.g. `for slide in slides:`."""
+        for sldId in self._sldIdLst.sldId_lst:
+            yield self.part.related_slide(sldId.rId)
+
+    def __len__(self) -> int:
+        """Support len() built-in function, e.g. `len(slides) == 4`."""
+        return len(self._sldIdLst)
+
+    def add_slide(self, slide_layout: SlideLayout) -> Slide:
+        """Return a newly added slide that inherits layout from `slide_layout`."""
+        rId, slide = self.part.add_slide(slide_layout)
+        slide.shapes.clone_layout_placeholders(slide_layout)
+        self._sldIdLst.add_sldId(rId)
+        return slide
+
+    def get(self, slide_id: int, default: Slide | None = None) -> Slide | None:
+        """Return the slide identified by int `slide_id` in this presentation.
+
+        Returns `default` if not found.
+        """
+        slide = self.part.get_slide(slide_id)
+        if slide is None:
+            return default
+        return slide
+
+    def index(self, slide: Slide) -> int:
+        """Map `slide` to its zero-based position in this slide sequence.
+
+        Raises |ValueError| on *slide* not present.
+        """
+        for idx, this_slide in enumerate(self):
+            if this_slide == slide:
+                return idx
+        raise ValueError("%s is not in slide collection" % slide)
+
+
+class SlideLayout(_BaseSlide):
+    """Slide layout object.
+
+    Provides access to placeholders, regular shapes, and slide layout-level properties.
+    """
+
+    part: SlideLayoutPart  # pyright: ignore[reportIncompatibleMethodOverride]
+
+    def iter_cloneable_placeholders(self) -> Iterator[LayoutPlaceholder]:
+        """Generate layout-placeholders on this slide-layout that should be cloned to a new slide.
+
+        Used when creating a new slide from this slide-layout.
+        """
+        latent_ph_types = (
+            PP_PLACEHOLDER.DATE,
+            PP_PLACEHOLDER.FOOTER,
+            PP_PLACEHOLDER.SLIDE_NUMBER,
+        )
+        for ph in self.placeholders:
+            if ph.element.ph_type not in latent_ph_types:
+                yield ph
+
+    @lazyproperty
+    def placeholders(self) -> LayoutPlaceholders:
+        """Sequence of placeholder shapes in this slide layout.
+
+        Placeholders appear in `idx` order.
+        """
+        return LayoutPlaceholders(self._element.spTree, self)
+
+    @lazyproperty
+    def shapes(self) -> LayoutShapes:
+        """Sequence of shapes appearing on this slide layout."""
+        return LayoutShapes(self._element.spTree, self)
+
+    @property
+    def slide_master(self) -> SlideMaster:
+        """Slide master from which this slide-layout inherits properties."""
+        return self.part.slide_master
+
+    @property
+    def used_by_slides(self):
+        """Tuple of slide objects based on this slide layout."""
+        # ---getting Slides collection requires going around the horn a bit---
+        slides = self.part.package.presentation_part.presentation.slides
+        return tuple(s for s in slides if s.slide_layout == self)
+
+
+class SlideLayouts(ParentedElementProxy):
+    """Sequence of slide layouts belonging to a slide-master.
+
+    Supports indexed access, len(), iteration, index() and remove().
+    """
+
+    part: SlideMasterPart  # pyright: ignore[reportIncompatibleMethodOverride]
+
+    def __init__(self, sldLayoutIdLst: CT_SlideLayoutIdList, parent: SlideMaster):
+        super(SlideLayouts, self).__init__(sldLayoutIdLst, parent)
+        self._sldLayoutIdLst = sldLayoutIdLst
+
+    def __getitem__(self, idx: int) -> SlideLayout:
+        """Provides indexed access, e.g. `slide_layouts[2]`."""
+        try:
+            sldLayoutId = self._sldLayoutIdLst.sldLayoutId_lst[idx]
+        except IndexError:
+            raise IndexError("slide layout index out of range")
+        return self.part.related_slide_layout(sldLayoutId.rId)
+
+    def __iter__(self) -> Iterator[SlideLayout]:
+        """Generate each |SlideLayout| in the collection, in sequence."""
+        for sldLayoutId in self._sldLayoutIdLst.sldLayoutId_lst:
+            yield self.part.related_slide_layout(sldLayoutId.rId)
+
+    def __len__(self) -> int:
+        """Support len() built-in function, e.g. `len(slides) == 4`."""
+        return len(self._sldLayoutIdLst)
+
+    def get_by_name(self, name: str, default: SlideLayout | None = None) -> SlideLayout | None:
+        """Return SlideLayout object having `name`, or `default` if not found."""
+        for slide_layout in self:
+            if slide_layout.name == name:
+                return slide_layout
+        return default
+
+    def index(self, slide_layout: SlideLayout) -> int:
+        """Return zero-based index of `slide_layout` in this collection.
+
+        Raises `ValueError` if `slide_layout` is not present in this collection.
+        """
+        for idx, this_layout in enumerate(self):
+            if slide_layout == this_layout:
+                return idx
+        raise ValueError("layout not in this SlideLayouts collection")
+
+    def remove(self, slide_layout: SlideLayout) -> None:
+        """Remove `slide_layout` from the collection.
+
+        Raises ValueError when `slide_layout` is in use; a slide layout which is the basis for one
+        or more slides cannot be removed.
+        """
+        # ---raise if layout is in use---
+        if slide_layout.used_by_slides:
+            raise ValueError("cannot remove slide-layout in use by one or more slides")
+
+        # ---target layout is identified by its index in this collection---
+        target_idx = self.index(slide_layout)
+
+        # --remove layout from p:sldLayoutIds of its master
+        # --this stops layout from showing up, but doesn't remove it from package
+        target_sldLayoutId = self._sldLayoutIdLst.sldLayoutId_lst[target_idx]
+        self._sldLayoutIdLst.remove(target_sldLayoutId)
+
+        # --drop relationship from master to layout
+        # --this removes layout from package, along with everything (only) it refers to,
+        # --including images (not used elsewhere) and hyperlinks
+        slide_layout.slide_master.part.drop_rel(target_sldLayoutId.rId)
+
+
+class SlideMaster(_BaseMaster):
+    """Slide master object.
+
+    Provides access to slide layouts. Access to placeholders, regular shapes, and slide master-level
+    properties is inherited from |_BaseMaster|.
+    """
+
+    _element: CT_SlideMaster  # pyright: ignore[reportIncompatibleVariableOverride]
+
+    @lazyproperty
+    def slide_layouts(self) -> SlideLayouts:
+        """|SlideLayouts| object providing access to this slide-master's layouts."""
+        return SlideLayouts(self._element.get_or_add_sldLayoutIdLst(), self)
+
+
+class SlideMasters(ParentedElementProxy):
+    """Sequence of |SlideMaster| objects belonging to a presentation.
+
+    Has list access semantics, supporting indexed access, len(), and iteration.
+    """
+
+    part: PresentationPart  # pyright: ignore[reportIncompatibleMethodOverride]
+
+    def __init__(self, sldMasterIdLst: CT_SlideMasterIdList, parent: Presentation):
+        super(SlideMasters, self).__init__(sldMasterIdLst, parent)
+        self._sldMasterIdLst = sldMasterIdLst
+
+    def __getitem__(self, idx: int) -> SlideMaster:
+        """Provides indexed access, e.g. `slide_masters[2]`."""
+        try:
+            sldMasterId = self._sldMasterIdLst.sldMasterId_lst[idx]
+        except IndexError:
+            raise IndexError("slide master index out of range")
+        return self.part.related_slide_master(sldMasterId.rId)
+
+    def __iter__(self):
+        """Generate each |SlideMaster| instance in the collection, in sequence."""
+        for smi in self._sldMasterIdLst.sldMasterId_lst:
+            yield self.part.related_slide_master(smi.rId)
+
+    def __len__(self):
+        """Support len() built-in function, e.g. `len(slide_masters) == 4`."""
+        return len(self._sldMasterIdLst)
+
+
+class _Background(ElementProxy):
+    """Provides access to slide background properties.
+
+    Note that the presence of this object does not by itself imply an
+    explicitly-defined background; a slide with an inherited background still
+    has a |_Background| object.
+    """
+
+    def __init__(self, cSld: CT_CommonSlideData):
+        super(_Background, self).__init__(cSld)
+        self._cSld = cSld
+
+    @lazyproperty
+    def fill(self):
+        """|FillFormat| instance for this background.
+
+        This |FillFormat| object is used to interrogate or specify the fill
+        of the slide background.
+
+        Note that accessing this property is potentially destructive. A slide
+        background can also be specified by a background style reference and
+        accessing this property will remove that reference, if present, and
+        replace it with NoFill. This is frequently the case for a slide
+        master background.
+
+        This is also the case when there is no explicitly defined background
+        (background is inherited); merely accessing this property will cause
+        the background to be set to NoFill and the inheritance link will be
+        interrupted. This is frequently the case for a slide background.
+
+        Of course, if you are accessing this property in order to set the
+        fill, then these changes are of no consequence, but the existing
+        background cannot be reliably interrogated using this property unless
+        you have already established it is an explicit fill.
+
+        If the background is already a fill, then accessing this property
+        makes no changes to the current background.
+        """
+        bgPr = self._cSld.get_or_add_bgPr()
+        return FillFormat.from_fill_parent(bgPr)
diff --git a/.venv/lib/python3.12/site-packages/pptx/spec.py b/.venv/lib/python3.12/site-packages/pptx/spec.py
new file mode 100644
index 00000000..e9d3b7d5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/spec.py
@@ -0,0 +1,632 @@
+"""Mappings from the ISO/IEC 29500 spec.
+
+Some of these are inferred from PowerPoint application behavior
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, TypedDict
+
+from pptx.enum.shapes import MSO_SHAPE
+
+GRAPHIC_DATA_URI_CHART = "http://schemas.openxmlformats.org/drawingml/2006/chart"
+GRAPHIC_DATA_URI_OLEOBJ = "http://schemas.openxmlformats.org/presentationml/2006/ole"
+GRAPHIC_DATA_URI_TABLE = "http://schemas.openxmlformats.org/drawingml/2006/table"
+
+if TYPE_CHECKING:
+    from typing_extensions import TypeAlias
+
+AdjustmentValue: TypeAlias = "tuple[str, int]"
+
+
+class ShapeSpec(TypedDict):
+    basename: str
+    avLst: tuple[AdjustmentValue, ...]
+
+
+# ============================================================================
+# AutoShape type specs
+# ============================================================================
+
+autoshape_types: dict[MSO_SHAPE, ShapeSpec] = {
+    MSO_SHAPE.ACTION_BUTTON_BACK_OR_PREVIOUS: {
+        "basename": "Action Button: Back or Previous",
+        "avLst": (),
+    },
+    MSO_SHAPE.ACTION_BUTTON_BEGINNING: {
+        "basename": "Action Button: Beginning",
+        "avLst": (),
+    },
+    MSO_SHAPE.ACTION_BUTTON_CUSTOM: {"basename": "Action Button: Custom", "avLst": ()},
+    MSO_SHAPE.ACTION_BUTTON_DOCUMENT: {
+        "basename": "Action Button: Document",
+        "avLst": (),
+    },
+    MSO_SHAPE.ACTION_BUTTON_END: {"basename": "Action Button: End", "avLst": ()},
+    MSO_SHAPE.ACTION_BUTTON_FORWARD_OR_NEXT: {
+        "basename": "Action Button: Forward or Next",
+        "avLst": (),
+    },
+    MSO_SHAPE.ACTION_BUTTON_HELP: {"basename": "Action Button: Help", "avLst": ()},
+    MSO_SHAPE.ACTION_BUTTON_HOME: {"basename": "Action Button: Home", "avLst": ()},
+    MSO_SHAPE.ACTION_BUTTON_INFORMATION: {
+        "basename": "Action Button: Information",
+        "avLst": (),
+    },
+    MSO_SHAPE.ACTION_BUTTON_MOVIE: {"basename": "Action Button: Movie", "avLst": ()},
+    MSO_SHAPE.ACTION_BUTTON_RETURN: {"basename": "Action Button: Return", "avLst": ()},
+    MSO_SHAPE.ACTION_BUTTON_SOUND: {"basename": "Action Button: Sound", "avLst": ()},
+    MSO_SHAPE.ARC: {"basename": "Arc", "avLst": (("adj1", 16200000), ("adj2", 0))},
+    MSO_SHAPE.BALLOON: {
+        "basename": "Rounded Rectangular Callout",
+        "avLst": (("adj1", -20833), ("adj2", 62500), ("adj3", 16667)),
+    },
+    MSO_SHAPE.BENT_ARROW: {
+        "basename": "Bent Arrow",
+        "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000), ("adj4", 43750)),
+    },
+    MSO_SHAPE.BENT_UP_ARROW: {
+        "basename": "Bent-Up Arrow",
+        "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000)),
+    },
+    MSO_SHAPE.BEVEL: {"basename": "Bevel", "avLst": (("adj", 12500),)},
+    MSO_SHAPE.BLOCK_ARC: {
+        "basename": "Block Arc",
+        "avLst": (("adj1", 10800000), ("adj2", 0), ("adj3", 25000)),
+    },
+    MSO_SHAPE.CAN: {"basename": "Can", "avLst": (("adj", 25000),)},
+    MSO_SHAPE.CHART_PLUS: {"basename": "Chart Plus", "avLst": ()},
+    MSO_SHAPE.CHART_STAR: {"basename": "Chart Star", "avLst": ()},
+    MSO_SHAPE.CHART_X: {"basename": "Chart X", "avLst": ()},
+    MSO_SHAPE.CHEVRON: {"basename": "Chevron", "avLst": (("adj", 50000),)},
+    MSO_SHAPE.CHORD: {
+        "basename": "Chord",
+        "avLst": (("adj1", 2700000), ("adj2", 16200000)),
+    },
+    MSO_SHAPE.CIRCULAR_ARROW: {
+        "basename": "Circular Arrow",
+        "avLst": (
+            ("adj1", 12500),
+            ("adj2", 1142319),
+            ("adj3", 20457681),
+            ("adj4", 10800000),
+            ("adj5", 12500),
+        ),
+    },
+    MSO_SHAPE.CLOUD: {"basename": "Cloud", "avLst": ()},
+    MSO_SHAPE.CLOUD_CALLOUT: {
+        "basename": "Cloud Callout",
+        "avLst": (("adj1", -20833), ("adj2", 62500)),
+    },
+    MSO_SHAPE.CORNER: {
+        "basename": "Corner",
+        "avLst": (("adj1", 50000), ("adj2", 50000)),
+    },
+    MSO_SHAPE.CORNER_TABS: {"basename": "Corner Tabs", "avLst": ()},
+    MSO_SHAPE.CROSS: {"basename": "Cross", "avLst": (("adj", 25000),)},
+    MSO_SHAPE.CUBE: {"basename": "Cube", "avLst": (("adj", 25000),)},
+    MSO_SHAPE.CURVED_DOWN_ARROW: {
+        "basename": "Curved Down Arrow",
+        "avLst": (("adj1", 25000), ("adj2", 50000), ("adj3", 25000)),
+    },
+    MSO_SHAPE.CURVED_DOWN_RIBBON: {
+        "basename": "Curved Down Ribbon",
+        "avLst": (("adj1", 25000), ("adj2", 50000), ("adj3", 12500)),
+    },
+    MSO_SHAPE.CURVED_LEFT_ARROW: {
+        "basename": "Curved Left Arrow",
+        "avLst": (("adj1", 25000), ("adj2", 50000), ("adj3", 25000)),
+    },
+    MSO_SHAPE.CURVED_RIGHT_ARROW: {
+        "basename": "Curved Right Arrow",
+        "avLst": (("adj1", 25000), ("adj2", 50000), ("adj3", 25000)),
+    },
+    MSO_SHAPE.CURVED_UP_ARROW: {
+        "basename": "Curved Up Arrow",
+        "avLst": (("adj1", 25000), ("adj2", 50000), ("adj3", 25000)),
+    },
+    MSO_SHAPE.CURVED_UP_RIBBON: {
+        "basename": "Curved Up Ribbon",
+        "avLst": (("adj1", 25000), ("adj2", 50000), ("adj3", 12500)),
+    },
+    MSO_SHAPE.DECAGON: {"basename": "Decagon", "avLst": (("vf", 105146),)},
+    MSO_SHAPE.DIAGONAL_STRIPE: {
+        "basename": "Diagonal Stripe",
+        "avLst": (("adj", 50000),),
+    },
+    MSO_SHAPE.DIAMOND: {"basename": "Diamond", "avLst": ()},
+    MSO_SHAPE.DODECAGON: {"basename": "Dodecagon", "avLst": ()},
+    MSO_SHAPE.DONUT: {"basename": "Donut", "avLst": (("adj", 25000),)},
+    MSO_SHAPE.DOUBLE_BRACE: {"basename": "Double Brace", "avLst": (("adj", 8333),)},
+    MSO_SHAPE.DOUBLE_BRACKET: {
+        "basename": "Double Bracket",
+        "avLst": (("adj", 16667),),
+    },
+    MSO_SHAPE.DOUBLE_WAVE: {
+        "basename": "Double Wave",
+        "avLst": (("adj1", 6250), ("adj2", 0)),
+    },
+    MSO_SHAPE.DOWN_ARROW: {
+        "basename": "Down Arrow",
+        "avLst": (("adj1", 50000), ("adj2", 50000)),
+    },
+    MSO_SHAPE.DOWN_ARROW_CALLOUT: {
+        "basename": "Down Arrow Callout",
+        "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000), ("adj4", 64977)),
+    },
+    MSO_SHAPE.DOWN_RIBBON: {
+        "basename": "Down Ribbon",
+        "avLst": (("adj1", 16667), ("adj2", 50000)),
+    },
+    MSO_SHAPE.EXPLOSION1: {"basename": "Explosion", "avLst": ()},
+    MSO_SHAPE.EXPLOSION2: {"basename": "Explosion", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_ALTERNATE_PROCESS: {
+        "basename": "Alternate process",
+        "avLst": (),
+    },
+    MSO_SHAPE.FLOWCHART_CARD: {"basename": "Card", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_COLLATE: {"basename": "Collate", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_CONNECTOR: {"basename": "Connector", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_DATA: {"basename": "Data", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_DECISION: {"basename": "Decision", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_DELAY: {"basename": "Delay", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_DIRECT_ACCESS_STORAGE: {
+        "basename": "Direct Access Storage",
+        "avLst": (),
+    },
+    MSO_SHAPE.FLOWCHART_DISPLAY: {"basename": "Display", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_DOCUMENT: {"basename": "Document", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_EXTRACT: {"basename": "Extract", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_INTERNAL_STORAGE: {"basename": "Internal Storage", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_MAGNETIC_DISK: {"basename": "Magnetic Disk", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_MANUAL_INPUT: {"basename": "Manual Input", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_MANUAL_OPERATION: {"basename": "Manual Operation", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_MERGE: {"basename": "Merge", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_MULTIDOCUMENT: {"basename": "Multidocument", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_OFFLINE_STORAGE: {"basename": "Offline Storage", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_OFFPAGE_CONNECTOR: {
+        "basename": "Off-page Connector",
+        "avLst": (),
+    },
+    MSO_SHAPE.FLOWCHART_OR: {"basename": "Or", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_PREDEFINED_PROCESS: {
+        "basename": "Predefined Process",
+        "avLst": (),
+    },
+    MSO_SHAPE.FLOWCHART_PREPARATION: {"basename": "Preparation", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_PROCESS: {"basename": "Process", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_PUNCHED_TAPE: {"basename": "Punched Tape", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_SEQUENTIAL_ACCESS_STORAGE: {
+        "basename": "Sequential Access Storage",
+        "avLst": (),
+    },
+    MSO_SHAPE.FLOWCHART_SORT: {"basename": "Sort", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_STORED_DATA: {"basename": "Stored Data", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_SUMMING_JUNCTION: {"basename": "Summing Junction", "avLst": ()},
+    MSO_SHAPE.FLOWCHART_TERMINATOR: {"basename": "Terminator", "avLst": ()},
+    MSO_SHAPE.FOLDED_CORNER: {"basename": "Folded Corner", "avLst": ()},
+    MSO_SHAPE.FRAME: {"basename": "Frame", "avLst": (("adj1", 12500),)},
+    MSO_SHAPE.FUNNEL: {"basename": "Funnel", "avLst": ()},
+    MSO_SHAPE.GEAR_6: {
+        "basename": "Gear 6",
+        "avLst": (("adj1", 15000), ("adj2", 3526)),
+    },
+    MSO_SHAPE.GEAR_9: {
+        "basename": "Gear 9",
+        "avLst": (("adj1", 10000), ("adj2", 1763)),
+    },
+    MSO_SHAPE.HALF_FRAME: {
+        "basename": "Half Frame",
+        "avLst": (("adj1", 33333), ("adj2", 33333)),
+    },
+    MSO_SHAPE.HEART: {"basename": "Heart", "avLst": ()},
+    MSO_SHAPE.HEPTAGON: {
+        "basename": "Heptagon",
+        "avLst": (("hf", 102572), ("vf", 105210)),
+    },
+    MSO_SHAPE.HEXAGON: {
+        "basename": "Hexagon",
+        "avLst": (("adj", 25000), ("vf", 115470)),
+    },
+    MSO_SHAPE.HORIZONTAL_SCROLL: {
+        "basename": "Horizontal Scroll",
+        "avLst": (("adj", 12500),),
+    },
+    MSO_SHAPE.ISOSCELES_TRIANGLE: {
+        "basename": "Isosceles Triangle",
+        "avLst": (("adj", 50000),),
+    },
+    MSO_SHAPE.LEFT_ARROW: {
+        "basename": "Left Arrow",
+        "avLst": (("adj1", 50000), ("adj2", 50000)),
+    },
+    MSO_SHAPE.LEFT_ARROW_CALLOUT: {
+        "basename": "Left Arrow Callout",
+        "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000), ("adj4", 64977)),
+    },
+    MSO_SHAPE.LEFT_BRACE: {
+        "basename": "Left Brace",
+        "avLst": (("adj1", 8333), ("adj2", 50000)),
+    },
+    MSO_SHAPE.LEFT_BRACKET: {"basename": "Left Bracket", "avLst": (("adj", 8333),)},
+    MSO_SHAPE.LEFT_CIRCULAR_ARROW: {
+        "basename": "Left Circular Arrow",
+        "avLst": (
+            ("adj1", 12500),
+            ("adj2", -1142319),
+            ("adj3", 1142319),
+            ("adj4", 10800000),
+            ("adj5", 12500),
+        ),
+    },
+    MSO_SHAPE.LEFT_RIGHT_ARROW: {
+        "basename": "Left-Right Arrow",
+        "avLst": (("adj1", 50000), ("adj2", 50000)),
+    },
+    MSO_SHAPE.LEFT_RIGHT_ARROW_CALLOUT: {
+        "basename": "Left-Right Arrow Callout",
+        "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000), ("adj4", 48123)),
+    },
+    MSO_SHAPE.LEFT_RIGHT_CIRCULAR_ARROW: {
+        "basename": "Left Right Circular Arrow",
+        "avLst": (
+            ("adj1", 12500),
+            ("adj2", 1142319),
+            ("adj3", 20457681),
+            ("adj4", 11942319),
+            ("adj5", 12500),
+        ),
+    },
+    MSO_SHAPE.LEFT_RIGHT_RIBBON: {
+        "basename": "Left Right Ribbon",
+        "avLst": (("adj1", 50000), ("adj2", 50000), ("adj3", 16667)),
+    },
+    MSO_SHAPE.LEFT_RIGHT_UP_ARROW: {
+        "basename": "Left-Right-Up Arrow",
+        "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000)),
+    },
+    MSO_SHAPE.LEFT_UP_ARROW: {
+        "basename": "Left-Up Arrow",
+        "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000)),
+    },
+    MSO_SHAPE.LIGHTNING_BOLT: {"basename": "Lightning Bolt", "avLst": ()},
+    MSO_SHAPE.LINE_CALLOUT_1: {
+        "basename": "Line Callout 1",
+        "avLst": (("adj1", 18750), ("adj2", -8333), ("adj3", 112500), ("adj4", -38333)),
+    },
+    MSO_SHAPE.LINE_CALLOUT_1_ACCENT_BAR: {
+        "basename": "Line Callout 1 (Accent Bar)",
+        "avLst": (("adj1", 18750), ("adj2", -8333), ("adj3", 112500), ("adj4", -38333)),
+    },
+    MSO_SHAPE.LINE_CALLOUT_1_BORDER_AND_ACCENT_BAR: {
+        "basename": "Line Callout 1 (Border and Accent Bar)",
+        "avLst": (("adj1", 18750), ("adj2", -8333), ("adj3", 112500), ("adj4", -38333)),
+    },
+    MSO_SHAPE.LINE_CALLOUT_1_NO_BORDER: {
+        "basename": "Line Callout 1 (No Border)",
+        "avLst": (("adj1", 18750), ("adj2", -8333), ("adj3", 112500), ("adj4", -38333)),
+    },
+    MSO_SHAPE.LINE_CALLOUT_2: {
+        "basename": "Line Callout 2",
+        "avLst": (
+            ("adj1", 18750),
+            ("adj2", -8333),
+            ("adj3", 18750),
+            ("adj4", -16667),
+            ("adj5", 112500),
+            ("adj6", -46667),
+        ),
+    },
+    MSO_SHAPE.LINE_CALLOUT_2_ACCENT_BAR: {
+        "basename": "Line Callout 2 (Accent Bar)",
+        "avLst": (
+            ("adj1", 18750),
+            ("adj2", -8333),
+            ("adj3", 18750),
+            ("adj4", -16667),
+            ("adj5", 112500),
+            ("adj6", -46667),
+        ),
+    },
+    MSO_SHAPE.LINE_CALLOUT_2_BORDER_AND_ACCENT_BAR: {
+        "basename": "Line Callout 2 (Border and Accent Bar)",
+        "avLst": (
+            ("adj1", 18750),
+            ("adj2", -8333),
+            ("adj3", 18750),
+            ("adj4", -16667),
+            ("adj5", 112500),
+            ("adj6", -46667),
+        ),
+    },
+    MSO_SHAPE.LINE_CALLOUT_2_NO_BORDER: {
+        "basename": "Line Callout 2 (No Border)",
+        "avLst": (
+            ("adj1", 18750),
+            ("adj2", -8333),
+            ("adj3", 18750),
+            ("adj4", -16667),
+            ("adj5", 112500),
+            ("adj6", -46667),
+        ),
+    },
+    MSO_SHAPE.LINE_CALLOUT_3: {
+        "basename": "Line Callout 3",
+        "avLst": (
+            ("adj1", 18750),
+            ("adj2", -8333),
+            ("adj3", 18750),
+            ("adj4", -16667),
+            ("adj5", 100000),
+            ("adj6", -16667),
+            ("adj7", 112963),
+            ("adj8", -8333),
+        ),
+    },
+    MSO_SHAPE.LINE_CALLOUT_3_ACCENT_BAR: {
+        "basename": "Line Callout 3 (Accent Bar)",
+        "avLst": (
+            ("adj1", 18750),
+            ("adj2", -8333),
+            ("adj3", 18750),
+            ("adj4", -16667),
+            ("adj5", 100000),
+            ("adj6", -16667),
+            ("adj7", 112963),
+            ("adj8", -8333),
+        ),
+    },
+    MSO_SHAPE.LINE_CALLOUT_3_BORDER_AND_ACCENT_BAR: {
+        "basename": "Line Callout 3 (Border and Accent Bar)",
+        "avLst": (
+            ("adj1", 18750),
+            ("adj2", -8333),
+            ("adj3", 18750),
+            ("adj4", -16667),
+            ("adj5", 100000),
+            ("adj6", -16667),
+            ("adj7", 112963),
+            ("adj8", -8333),
+        ),
+    },
+    MSO_SHAPE.LINE_CALLOUT_3_NO_BORDER: {
+        "basename": "Line Callout 3 (No Border)",
+        "avLst": (
+            ("adj1", 18750),
+            ("adj2", -8333),
+            ("adj3", 18750),
+            ("adj4", -16667),
+            ("adj5", 100000),
+            ("adj6", -16667),
+            ("adj7", 112963),
+            ("adj8", -8333),
+        ),
+    },
+    MSO_SHAPE.LINE_CALLOUT_4: {
+        "basename": "Line Callout 3",
+        "avLst": (
+            ("adj1", 18750),
+            ("adj2", -8333),
+            ("adj3", 18750),
+            ("adj4", -16667),
+            ("adj5", 100000),
+            ("adj6", -16667),
+            ("adj7", 112963),
+            ("adj8", -8333),
+        ),
+    },
+    MSO_SHAPE.LINE_CALLOUT_4_ACCENT_BAR: {
+        "basename": "Line Callout 3 (Accent Bar)",
+        "avLst": (
+            ("adj1", 18750),
+            ("adj2", -8333),
+            ("adj3", 18750),
+            ("adj4", -16667),
+            ("adj5", 100000),
+            ("adj6", -16667),
+            ("adj7", 112963),
+            ("adj8", -8333),
+        ),
+    },
+    MSO_SHAPE.LINE_CALLOUT_4_BORDER_AND_ACCENT_BAR: {
+        "basename": "Line Callout 3 (Border and Accent Bar)",
+        "avLst": (
+            ("adj1", 18750),
+            ("adj2", -8333),
+            ("adj3", 18750),
+            ("adj4", -16667),
+            ("adj5", 100000),
+            ("adj6", -16667),
+            ("adj7", 112963),
+            ("adj8", -8333),
+        ),
+    },
+    MSO_SHAPE.LINE_CALLOUT_4_NO_BORDER: {
+        "basename": "Line Callout 3 (No Border)",
+        "avLst": (
+            ("adj1", 18750),
+            ("adj2", -8333),
+            ("adj3", 18750),
+            ("adj4", -16667),
+            ("adj5", 100000),
+            ("adj6", -16667),
+            ("adj7", 112963),
+            ("adj8", -8333),
+        ),
+    },
+    MSO_SHAPE.LINE_INVERSE: {"basename": "Straight Connector", "avLst": ()},
+    MSO_SHAPE.MATH_DIVIDE: {
+        "basename": "Division",
+        "avLst": (("adj1", 23520), ("adj2", 5880), ("adj3", 11760)),
+    },
+    MSO_SHAPE.MATH_EQUAL: {
+        "basename": "Equal",
+        "avLst": (("adj1", 23520), ("adj2", 11760)),
+    },
+    MSO_SHAPE.MATH_MINUS: {"basename": "Minus", "avLst": (("adj1", 23520),)},
+    MSO_SHAPE.MATH_MULTIPLY: {"basename": "Multiply", "avLst": (("adj1", 23520),)},
+    MSO_SHAPE.MATH_NOT_EQUAL: {
+        "basename": "Not Equal",
+        "avLst": (("adj1", 23520), ("adj2", 6600000), ("adj3", 11760)),
+    },
+    MSO_SHAPE.MATH_PLUS: {"basename": "Plus", "avLst": (("adj1", 23520),)},
+    MSO_SHAPE.MOON: {"basename": "Moon", "avLst": (("adj", 50000),)},
+    MSO_SHAPE.NON_ISOSCELES_TRAPEZOID: {
+        "basename": "Non-isosceles Trapezoid",
+        "avLst": (("adj1", 25000), ("adj2", 25000)),
+    },
+    MSO_SHAPE.NOTCHED_RIGHT_ARROW: {
+        "basename": "Notched Right Arrow",
+        "avLst": (("adj1", 50000), ("adj2", 50000)),
+    },
+    MSO_SHAPE.NO_SYMBOL: {"basename": '"No" Symbol', "avLst": (("adj", 18750),)},
+    MSO_SHAPE.OCTAGON: {"basename": "Octagon", "avLst": (("adj", 29289),)},
+    MSO_SHAPE.OVAL: {"basename": "Oval", "avLst": ()},
+    MSO_SHAPE.OVAL_CALLOUT: {
+        "basename": "Oval Callout",
+        "avLst": (("adj1", -20833), ("adj2", 62500)),
+    },
+    MSO_SHAPE.PARALLELOGRAM: {"basename": "Parallelogram", "avLst": (("adj", 25000),)},
+    MSO_SHAPE.PENTAGON: {"basename": "Pentagon", "avLst": (("adj", 50000),)},
+    MSO_SHAPE.PIE: {"basename": "Pie", "avLst": (("adj1", 0), ("adj2", 16200000))},
+    MSO_SHAPE.PIE_WEDGE: {"basename": "Pie", "avLst": ()},
+    MSO_SHAPE.PLAQUE: {"basename": "Plaque", "avLst": (("adj", 16667),)},
+    MSO_SHAPE.PLAQUE_TABS: {"basename": "Plaque Tabs", "avLst": ()},
+    MSO_SHAPE.QUAD_ARROW: {
+        "basename": "Quad Arrow",
+        "avLst": (("adj1", 22500), ("adj2", 22500), ("adj3", 22500)),
+    },
+    MSO_SHAPE.QUAD_ARROW_CALLOUT: {
+        "basename": "Quad Arrow Callout",
+        "avLst": (("adj1", 18515), ("adj2", 18515), ("adj3", 18515), ("adj4", 48123)),
+    },
+    MSO_SHAPE.RECTANGLE: {"basename": "Rectangle", "avLst": ()},
+    MSO_SHAPE.RECTANGULAR_CALLOUT: {
+        "basename": "Rectangular Callout",
+        "avLst": (("adj1", -20833), ("adj2", 62500)),
+    },
+    MSO_SHAPE.REGULAR_PENTAGON: {
+        "basename": "Regular Pentagon",
+        "avLst": (("hf", 105146), ("vf", 110557)),
+    },
+    MSO_SHAPE.RIGHT_ARROW: {
+        "basename": "Right Arrow",
+        "avLst": (("adj1", 50000), ("adj2", 50000)),
+    },
+    MSO_SHAPE.RIGHT_ARROW_CALLOUT: {
+        "basename": "Right Arrow Callout",
+        "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000), ("adj4", 64977)),
+    },
+    MSO_SHAPE.RIGHT_BRACE: {
+        "basename": "Right Brace",
+        "avLst": (("adj1", 8333), ("adj2", 50000)),
+    },
+    MSO_SHAPE.RIGHT_BRACKET: {"basename": "Right Bracket", "avLst": (("adj", 8333),)},
+    MSO_SHAPE.RIGHT_TRIANGLE: {"basename": "Right Triangle", "avLst": ()},
+    MSO_SHAPE.ROUNDED_RECTANGLE: {
+        "basename": "Rounded Rectangle",
+        "avLst": (("adj", 16667),),
+    },
+    MSO_SHAPE.ROUNDED_RECTANGULAR_CALLOUT: {
+        "basename": "Rounded Rectangular Callout",
+        "avLst": (("adj1", -20833), ("adj2", 62500), ("adj3", 16667)),
+    },
+    MSO_SHAPE.ROUND_1_RECTANGLE: {
+        "basename": "Round Single Corner Rectangle",
+        "avLst": (("adj", 16667),),
+    },
+    MSO_SHAPE.ROUND_2_DIAG_RECTANGLE: {
+        "basename": "Round Diagonal Corner Rectangle",
+        "avLst": (("adj1", 16667), ("adj2", 0)),
+    },
+    MSO_SHAPE.ROUND_2_SAME_RECTANGLE: {
+        "basename": "Round Same Side Corner Rectangle",
+        "avLst": (("adj1", 16667), ("adj2", 0)),
+    },
+    MSO_SHAPE.SMILEY_FACE: {"basename": "Smiley Face", "avLst": (("adj", 4653),)},
+    MSO_SHAPE.SNIP_1_RECTANGLE: {
+        "basename": "Snip Single Corner Rectangle",
+        "avLst": (("adj", 16667),),
+    },
+    MSO_SHAPE.SNIP_2_DIAG_RECTANGLE: {
+        "basename": "Snip Diagonal Corner Rectangle",
+        "avLst": (("adj1", 0), ("adj2", 16667)),
+    },
+    MSO_SHAPE.SNIP_2_SAME_RECTANGLE: {
+        "basename": "Snip Same Side Corner Rectangle",
+        "avLst": (("adj1", 16667), ("adj2", 0)),
+    },
+    MSO_SHAPE.SNIP_ROUND_RECTANGLE: {
+        "basename": "Snip and Round Single Corner Rectangle",
+        "avLst": (("adj1", 16667), ("adj2", 16667)),
+    },
+    MSO_SHAPE.SQUARE_TABS: {"basename": "Square Tabs", "avLst": ()},
+    MSO_SHAPE.STAR_10_POINT: {
+        "basename": "10-Point Star",
+        "avLst": (("adj", 42533), ("hf", 105146)),
+    },
+    MSO_SHAPE.STAR_12_POINT: {"basename": "12-Point Star", "avLst": (("adj", 37500),)},
+    MSO_SHAPE.STAR_16_POINT: {"basename": "16-Point Star", "avLst": (("adj", 37500),)},
+    MSO_SHAPE.STAR_24_POINT: {"basename": "24-Point Star", "avLst": (("adj", 37500),)},
+    MSO_SHAPE.STAR_32_POINT: {"basename": "32-Point Star", "avLst": (("adj", 37500),)},
+    MSO_SHAPE.STAR_4_POINT: {"basename": "4-Point Star", "avLst": (("adj", 12500),)},
+    MSO_SHAPE.STAR_5_POINT: {
+        "basename": "5-Point Star",
+        "avLst": (("adj", 19098), ("hf", 105146), ("vf", 110557)),
+    },
+    MSO_SHAPE.STAR_6_POINT: {
+        "basename": "6-Point Star",
+        "avLst": (("adj", 28868), ("hf", 115470)),
+    },
+    MSO_SHAPE.STAR_7_POINT: {
+        "basename": "7-Point Star",
+        "avLst": (("adj", 34601), ("hf", 102572), ("vf", 105210)),
+    },
+    MSO_SHAPE.STAR_8_POINT: {"basename": "8-Point Star", "avLst": (("adj", 37500),)},
+    MSO_SHAPE.STRIPED_RIGHT_ARROW: {
+        "basename": "Striped Right Arrow",
+        "avLst": (("adj1", 50000), ("adj2", 50000)),
+    },
+    MSO_SHAPE.SUN: {"basename": "Sun", "avLst": (("adj", 25000),)},
+    MSO_SHAPE.SWOOSH_ARROW: {
+        "basename": "Swoosh Arrow",
+        "avLst": (("adj1", 25000), ("adj2", 16667)),
+    },
+    MSO_SHAPE.TEAR: {"basename": "Teardrop", "avLst": (("adj", 100000),)},
+    MSO_SHAPE.TRAPEZOID: {"basename": "Trapezoid", "avLst": (("adj", 25000),)},
+    MSO_SHAPE.UP_ARROW: {
+        "basename": "Up Arrow",
+        "avLst": (("adj1", 50000), ("adj2", 50000)),
+    },
+    MSO_SHAPE.UP_ARROW_CALLOUT: {
+        "basename": "Up Arrow Callout",
+        "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000), ("adj4", 64977)),
+    },
+    MSO_SHAPE.UP_DOWN_ARROW: {
+        "basename": "Up-Down Arrow",
+        "avLst": (("adj1", 50000), ("adj1", 50000), ("adj2", 50000), ("adj2", 50000)),
+    },
+    MSO_SHAPE.UP_DOWN_ARROW_CALLOUT: {
+        "basename": "Up-Down Arrow Callout",
+        "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000), ("adj4", 48123)),
+    },
+    MSO_SHAPE.UP_RIBBON: {
+        "basename": "Up Ribbon",
+        "avLst": (("adj1", 16667), ("adj2", 50000)),
+    },
+    MSO_SHAPE.U_TURN_ARROW: {
+        "basename": "U-Turn Arrow",
+        "avLst": (
+            ("adj1", 25000),
+            ("adj2", 25000),
+            ("adj3", 25000),
+            ("adj4", 43750),
+            ("adj5", 75000),
+        ),
+    },
+    MSO_SHAPE.VERTICAL_SCROLL: {
+        "basename": "Vertical Scroll",
+        "avLst": (("adj", 12500),),
+    },
+    MSO_SHAPE.WAVE: {"basename": "Wave", "avLst": (("adj1", 12500), ("adj2", 0))},
+}
diff --git a/.venv/lib/python3.12/site-packages/pptx/table.py b/.venv/lib/python3.12/site-packages/pptx/table.py
new file mode 100644
index 00000000..3bdf54ba
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/table.py
@@ -0,0 +1,496 @@
+"""Table-related objects such as Table and Cell."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Iterator
+
+from pptx.dml.fill import FillFormat
+from pptx.oxml.table import TcRange
+from pptx.shapes import Subshape
+from pptx.text.text import TextFrame
+from pptx.util import Emu, lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.enum.text import MSO_VERTICAL_ANCHOR
+    from pptx.oxml.table import CT_Table, CT_TableCell, CT_TableCol, CT_TableRow
+    from pptx.parts.slide import BaseSlidePart
+    from pptx.shapes.graphfrm import GraphicFrame
+    from pptx.types import ProvidesPart
+    from pptx.util import Length
+
+
+class Table(object):
+    """A DrawingML table object.
+
+    Not intended to be constructed directly, use
+    :meth:`.Slide.shapes.add_table` to add a table to a slide.
+    """
+
+    def __init__(self, tbl: CT_Table, graphic_frame: GraphicFrame):
+        super(Table, self).__init__()
+        self._tbl = tbl
+        self._graphic_frame = graphic_frame
+
+    def cell(self, row_idx: int, col_idx: int) -> _Cell:
+        """Return cell at `row_idx`, `col_idx`.
+
+        Return value is an instance of |_Cell|. `row_idx` and `col_idx` are zero-based, e.g.
+        cell(0, 0) is the top, left cell in the table.
+        """
+        return _Cell(self._tbl.tc(row_idx, col_idx), self)
+
+    @lazyproperty
+    def columns(self) -> _ColumnCollection:
+        """|_ColumnCollection| instance for this table.
+
+        Provides access to |_Column| objects representing the table's columns. |_Column| objects
+        are accessed using list notation, e.g. `col = tbl.columns[0]`.
+        """
+        return _ColumnCollection(self._tbl, self)
+
+    @property
+    def first_col(self) -> bool:
+        """When `True`, indicates first column should have distinct formatting.
+
+        Read/write. Distinct formatting is used, for example, when the first column contains row
+        headings (is a side-heading column).
+        """
+        return self._tbl.firstCol
+
+    @first_col.setter
+    def first_col(self, value: bool):
+        self._tbl.firstCol = value
+
+    @property
+    def first_row(self) -> bool:
+        """When `True`, indicates first row should have distinct formatting.
+
+        Read/write. Distinct formatting is used, for example, when the first row contains column
+        headings.
+        """
+        return self._tbl.firstRow
+
+    @first_row.setter
+    def first_row(self, value: bool):
+        self._tbl.firstRow = value
+
+    @property
+    def horz_banding(self) -> bool:
+        """When `True`, indicates rows should have alternating shading.
+
+        Read/write. Used to allow rows to be traversed more easily without losing track of which
+        row is being read.
+        """
+        return self._tbl.bandRow
+
+    @horz_banding.setter
+    def horz_banding(self, value: bool):
+        self._tbl.bandRow = value
+
+    def iter_cells(self) -> Iterator[_Cell]:
+        """Generate _Cell object for each cell in this table.
+
+        Each grid cell is generated in left-to-right, top-to-bottom order.
+        """
+        return (_Cell(tc, self) for tc in self._tbl.iter_tcs())
+
+    @property
+    def last_col(self) -> bool:
+        """When `True`, indicates the rightmost column should have distinct formatting.
+
+        Read/write. Used, for example, when a row totals column appears at the far right of the
+        table.
+        """
+        return self._tbl.lastCol
+
+    @last_col.setter
+    def last_col(self, value: bool):
+        self._tbl.lastCol = value
+
+    @property
+    def last_row(self) -> bool:
+        """When `True`, indicates the bottom row should have distinct formatting.
+
+        Read/write. Used, for example, when a totals row appears as the bottom row.
+        """
+        return self._tbl.lastRow
+
+    @last_row.setter
+    def last_row(self, value: bool):
+        self._tbl.lastRow = value
+
+    def notify_height_changed(self) -> None:
+        """Called by a row when its height changes.
+
+        Triggers the graphic frame to recalculate its total height (as the sum of the row
+        heights).
+        """
+        new_table_height = Emu(sum([row.height for row in self.rows]))
+        self._graphic_frame.height = new_table_height
+
+    def notify_width_changed(self) -> None:
+        """Called by a column when its width changes.
+
+        Triggers the graphic frame to recalculate its total width (as the sum of the column
+        widths).
+        """
+        new_table_width = Emu(sum([col.width for col in self.columns]))
+        self._graphic_frame.width = new_table_width
+
+    @property
+    def part(self) -> BaseSlidePart:
+        """The package part containing this table."""
+        return self._graphic_frame.part
+
+    @lazyproperty
+    def rows(self):
+        """|_RowCollection| instance for this table.
+
+        Provides access to |_Row| objects representing the table's rows. |_Row| objects are
+        accessed using list notation, e.g. `col = tbl.rows[0]`.
+        """
+        return _RowCollection(self._tbl, self)
+
+    @property
+    def vert_banding(self) -> bool:
+        """When `True`, indicates columns should have alternating shading.
+
+        Read/write. Used to allow columns to be traversed more easily without losing track of
+        which column is being read.
+        """
+        return self._tbl.bandCol
+
+    @vert_banding.setter
+    def vert_banding(self, value: bool):
+        self._tbl.bandCol = value
+
+
+class _Cell(Subshape):
+    """Table cell"""
+
+    def __init__(self, tc: CT_TableCell, parent: ProvidesPart):
+        super(_Cell, self).__init__(parent)
+        self._tc = tc
+
+    def __eq__(self, other: object) -> bool:
+        """|True| if this object proxies the same element as `other`.
+
+        Equality for proxy objects is defined as referring to the same XML element, whether or not
+        they are the same proxy object instance.
+        """
+        if not isinstance(other, type(self)):
+            return False
+        return self._tc is other._tc
+
+    def __ne__(self, other: object) -> bool:
+        if not isinstance(other, type(self)):
+            return True
+        return self._tc is not other._tc
+
+    @lazyproperty
+    def fill(self) -> FillFormat:
+        """|FillFormat| instance for this cell.
+
+        Provides access to fill properties such as foreground color.
+        """
+        tcPr = self._tc.get_or_add_tcPr()
+        return FillFormat.from_fill_parent(tcPr)
+
+    @property
+    def is_merge_origin(self) -> bool:
+        """True if this cell is the top-left grid cell in a merged cell."""
+        return self._tc.is_merge_origin
+
+    @property
+    def is_spanned(self) -> bool:
+        """True if this cell is spanned by a merge-origin cell.
+
+        A merge-origin cell "spans" the other grid cells in its merge range, consuming their area
+        and "shadowing" the spanned grid cells.
+
+        Note this value is |False| for a merge-origin cell. A merge-origin cell spans other grid
+        cells, but is not itself a spanned cell.
+        """
+        return self._tc.is_spanned
+
+    @property
+    def margin_left(self) -> Length:
+        """Left margin of cells.
+
+        Read/write. If assigned |None|, the default value is used, 0.1 inches for left and right
+        margins and 0.05 inches for top and bottom.
+        """
+        return self._tc.marL
+
+    @margin_left.setter
+    def margin_left(self, margin_left: Length | None):
+        self._validate_margin_value(margin_left)
+        self._tc.marL = margin_left
+
+    @property
+    def margin_right(self) -> Length:
+        """Right margin of cell."""
+        return self._tc.marR
+
+    @margin_right.setter
+    def margin_right(self, margin_right: Length | None):
+        self._validate_margin_value(margin_right)
+        self._tc.marR = margin_right
+
+    @property
+    def margin_top(self) -> Length:
+        """Top margin of cell."""
+        return self._tc.marT
+
+    @margin_top.setter
+    def margin_top(self, margin_top: Length | None):
+        self._validate_margin_value(margin_top)
+        self._tc.marT = margin_top
+
+    @property
+    def margin_bottom(self) -> Length:
+        """Bottom margin of cell."""
+        return self._tc.marB
+
+    @margin_bottom.setter
+    def margin_bottom(self, margin_bottom: Length | None):
+        self._validate_margin_value(margin_bottom)
+        self._tc.marB = margin_bottom
+
+    def merge(self, other_cell: _Cell) -> None:
+        """Create merged cell from this cell to `other_cell`.
+
+        This cell and `other_cell` specify opposite corners of the merged cell range. Either
+        diagonal of the cell region may be specified in either order, e.g. self=bottom-right,
+        other_cell=top-left, etc.
+
+        Raises |ValueError| if the specified range already contains merged cells anywhere within
+        its extents or if `other_cell` is not in the same table as `self`.
+        """
+        tc_range = TcRange(self._tc, other_cell._tc)
+
+        if not tc_range.in_same_table:
+            raise ValueError("other_cell from different table")
+        if tc_range.contains_merged_cell:
+            raise ValueError("range contains one or more merged cells")
+
+        tc_range.move_content_to_origin()
+
+        row_count, col_count = tc_range.dimensions
+
+        for tc in tc_range.iter_top_row_tcs():
+            tc.rowSpan = row_count
+        for tc in tc_range.iter_left_col_tcs():
+            tc.gridSpan = col_count
+        for tc in tc_range.iter_except_left_col_tcs():
+            tc.hMerge = True
+        for tc in tc_range.iter_except_top_row_tcs():
+            tc.vMerge = True
+
+    @property
+    def span_height(self) -> int:
+        """int count of rows spanned by this cell.
+
+        The value of this property may be misleading (often 1) on cells where `.is_merge_origin`
+        is not |True|, since only a merge-origin cell contains complete span information. This
+        property is only intended for use on cells known to be a merge origin by testing
+        `.is_merge_origin`.
+        """
+        return self._tc.rowSpan
+
+    @property
+    def span_width(self) -> int:
+        """int count of columns spanned by this cell.
+
+        The value of this property may be misleading (often 1) on cells where `.is_merge_origin`
+        is not |True|, since only a merge-origin cell contains complete span information. This
+        property is only intended for use on cells known to be a merge origin by testing
+        `.is_merge_origin`.
+        """
+        return self._tc.gridSpan
+
+    def split(self) -> None:
+        """Remove merge from this (merge-origin) cell.
+
+        The merged cell represented by this object will be "unmerged", yielding a separate
+        unmerged cell for each grid cell previously spanned by this merge.
+
+        Raises |ValueError| when this cell is not a merge-origin cell. Test with
+        `.is_merge_origin` before calling.
+        """
+        if not self.is_merge_origin:
+            raise ValueError("not a merge-origin cell; only a merge-origin cell can be sp" "lit")
+
+        tc_range = TcRange.from_merge_origin(self._tc)
+
+        for tc in tc_range.iter_tcs():
+            tc.rowSpan = tc.gridSpan = 1
+            tc.hMerge = tc.vMerge = False
+
+    @property
+    def text(self) -> str:
+        """Textual content of cell as a single string.
+
+        The returned string will contain a newline character (`"\\n"`) separating each paragraph
+        and a vertical-tab (`"\\v"`) character for each line break (soft carriage return) in the
+        cell's text.
+
+        Assignment to `text` replaces all text currently contained in the cell. A newline
+        character (`"\\n"`) in the assigned text causes a new paragraph to be started. A
+        vertical-tab (`"\\v"`) character in the assigned text causes a line-break (soft
+        carriage-return) to be inserted. (The vertical-tab character appears in clipboard text
+        copied from PowerPoint as its encoding of line-breaks.)
+        """
+        return self.text_frame.text
+
+    @text.setter
+    def text(self, text: str):
+        self.text_frame.text = text
+
+    @property
+    def text_frame(self) -> TextFrame:
+        """|TextFrame| containing the text that appears in the cell."""
+        txBody = self._tc.get_or_add_txBody()
+        return TextFrame(txBody, self)
+
+    @property
+    def vertical_anchor(self) -> MSO_VERTICAL_ANCHOR | None:
+        """Vertical alignment of this cell.
+
+        This value is a member of the :ref:`MsoVerticalAnchor` enumeration or |None|. A value of
+        |None| indicates the cell has no explicitly applied vertical anchor setting and its
+        effective value is inherited from its style-hierarchy ancestors.
+
+        Assigning |None| to this property causes any explicitly applied vertical anchor setting to
+        be cleared and inheritance of its effective value to be restored.
+        """
+        return self._tc.anchor
+
+    @vertical_anchor.setter
+    def vertical_anchor(self, mso_anchor_idx: MSO_VERTICAL_ANCHOR | None):
+        self._tc.anchor = mso_anchor_idx
+
+    @staticmethod
+    def _validate_margin_value(margin_value: Length | None) -> None:
+        """Raise ValueError if `margin_value` is not a positive integer value or |None|."""
+        if not isinstance(margin_value, int) and margin_value is not None:
+            tmpl = "margin value must be integer or None, got '%s'"
+            raise TypeError(tmpl % margin_value)
+
+
+class _Column(Subshape):
+    """Table column"""
+
+    def __init__(self, gridCol: CT_TableCol, parent: _ColumnCollection):
+        super(_Column, self).__init__(parent)
+        self._parent = parent
+        self._gridCol = gridCol
+
+    @property
+    def width(self) -> Length:
+        """Width of column in EMU."""
+        return self._gridCol.w
+
+    @width.setter
+    def width(self, width: Length):
+        self._gridCol.w = width
+        self._parent.notify_width_changed()
+
+
+class _Row(Subshape):
+    """Table row"""
+
+    def __init__(self, tr: CT_TableRow, parent: _RowCollection):
+        super(_Row, self).__init__(parent)
+        self._parent = parent
+        self._tr = tr
+
+    @property
+    def cells(self):
+        """Read-only reference to collection of cells in row.
+
+        An individual cell is referenced using list notation, e.g. `cell = row.cells[0]`.
+        """
+        return _CellCollection(self._tr, self)
+
+    @property
+    def height(self) -> Length:
+        """Height of row in EMU."""
+        return self._tr.h
+
+    @height.setter
+    def height(self, height: Length):
+        self._tr.h = height
+        self._parent.notify_height_changed()
+
+
+class _CellCollection(Subshape):
+    """Horizontal sequence of row cells"""
+
+    def __init__(self, tr: CT_TableRow, parent: _Row):
+        super(_CellCollection, self).__init__(parent)
+        self._parent = parent
+        self._tr = tr
+
+    def __getitem__(self, idx: int) -> _Cell:
+        """Provides indexed access, (e.g. 'cells[0]')."""
+        if idx < 0 or idx >= len(self._tr.tc_lst):
+            msg = "cell index [%d] out of range" % idx
+            raise IndexError(msg)
+        return _Cell(self._tr.tc_lst[idx], self)
+
+    def __iter__(self) -> Iterator[_Cell]:
+        """Provides iterability."""
+        return (_Cell(tc, self) for tc in self._tr.tc_lst)
+
+    def __len__(self) -> int:
+        """Supports len() function (e.g. 'len(cells) == 1')."""
+        return len(self._tr.tc_lst)
+
+
+class _ColumnCollection(Subshape):
+    """Sequence of table columns."""
+
+    def __init__(self, tbl: CT_Table, parent: Table):
+        super(_ColumnCollection, self).__init__(parent)
+        self._parent = parent
+        self._tbl = tbl
+
+    def __getitem__(self, idx: int):
+        """Provides indexed access, (e.g. 'columns[0]')."""
+        if idx < 0 or idx >= len(self._tbl.tblGrid.gridCol_lst):
+            msg = "column index [%d] out of range" % idx
+            raise IndexError(msg)
+        return _Column(self._tbl.tblGrid.gridCol_lst[idx], self)
+
+    def __len__(self):
+        """Supports len() function (e.g. 'len(columns) == 1')."""
+        return len(self._tbl.tblGrid.gridCol_lst)
+
+    def notify_width_changed(self):
+        """Called by a column when its width changes. Pass along to parent."""
+        self._parent.notify_width_changed()
+
+
+class _RowCollection(Subshape):
+    """Sequence of table rows"""
+
+    def __init__(self, tbl: CT_Table, parent: Table):
+        super(_RowCollection, self).__init__(parent)
+        self._parent = parent
+        self._tbl = tbl
+
+    def __getitem__(self, idx: int) -> _Row:
+        """Provides indexed access, (e.g. 'rows[0]')."""
+        if idx < 0 or idx >= len(self):
+            msg = "row index [%d] out of range" % idx
+            raise IndexError(msg)
+        return _Row(self._tbl.tr_lst[idx], self)
+
+    def __len__(self):
+        """Supports len() function (e.g. 'len(rows) == 1')."""
+        return len(self._tbl.tr_lst)
+
+    def notify_height_changed(self):
+        """Called by a row when its height changes. Pass along to parent."""
+        self._parent.notify_height_changed()
diff --git a/.venv/lib/python3.12/site-packages/pptx/templates/default.pptx b/.venv/lib/python3.12/site-packages/pptx/templates/default.pptx
new file mode 100644
index 00000000..e7fd6565
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/default.pptx
Binary files differdiff --git a/.venv/lib/python3.12/site-packages/pptx/templates/docx-icon.emf b/.venv/lib/python3.12/site-packages/pptx/templates/docx-icon.emf
new file mode 100644
index 00000000..b8660118
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/docx-icon.emf
Binary files differdiff --git a/.venv/lib/python3.12/site-packages/pptx/templates/generic-icon.emf b/.venv/lib/python3.12/site-packages/pptx/templates/generic-icon.emf
new file mode 100644
index 00000000..d0914e00
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/generic-icon.emf
Binary files differdiff --git a/.venv/lib/python3.12/site-packages/pptx/templates/notes.xml b/.venv/lib/python3.12/site-packages/pptx/templates/notes.xml
new file mode 100644
index 00000000..654effbb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/notes.xml
@@ -0,0 +1,23 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
+<p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
+  <p:cSld>
+    <p:spTree>
+      <p:nvGrpSpPr>
+        <p:cNvPr id="1" name=""/>
+        <p:cNvGrpSpPr/>
+        <p:nvPr/>
+      </p:nvGrpSpPr>
+      <p:grpSpPr>
+        <a:xfrm>
+          <a:off x="0" y="0"/>
+          <a:ext cx="0" cy="0"/>
+          <a:chOff x="0" y="0"/>
+          <a:chExt cx="0" cy="0"/>
+        </a:xfrm>
+      </p:grpSpPr>
+    </p:spTree>
+  </p:cSld>
+  <p:clrMapOvr>
+    <a:masterClrMapping/>
+  </p:clrMapOvr>
+</p:notes>
diff --git a/.venv/lib/python3.12/site-packages/pptx/templates/notesMaster.xml b/.venv/lib/python3.12/site-packages/pptx/templates/notesMaster.xml
new file mode 100644
index 00000000..80008e07
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/notesMaster.xml
@@ -0,0 +1,352 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
+<p:notesMaster
+    xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
+    xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
+    xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
+    >
+  <p:cSld>
+    <p:bg>
+      <p:bgRef idx="1001">
+        <a:schemeClr val="bg1"/>
+      </p:bgRef>
+    </p:bg>
+    <p:spTree>
+      <p:nvGrpSpPr>
+        <p:cNvPr id="1" name=""/>
+        <p:cNvGrpSpPr/>
+        <p:nvPr/>
+      </p:nvGrpSpPr>
+      <p:grpSpPr>
+        <a:xfrm>
+          <a:off x="0" y="0"/>
+          <a:ext cx="0" cy="0"/>
+          <a:chOff x="0" y="0"/>
+          <a:chExt cx="0" cy="0"/>
+        </a:xfrm>
+      </p:grpSpPr>
+      <p:sp>
+        <p:nvSpPr>
+          <p:cNvPr id="2" name="Header Placeholder 1"/>
+          <p:cNvSpPr>
+            <a:spLocks noGrp="1"/>
+          </p:cNvSpPr>
+          <p:nvPr>
+            <p:ph type="hdr" sz="quarter"/>
+          </p:nvPr>
+        </p:nvSpPr>
+        <p:spPr>
+          <a:xfrm>
+            <a:off x="0" y="0"/>
+            <a:ext cx="2971800" cy="457200"/>
+          </a:xfrm>
+          <a:prstGeom prst="rect">
+            <a:avLst/>
+          </a:prstGeom>
+        </p:spPr>
+        <p:txBody>
+          <a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0"/>
+          <a:lstStyle>
+            <a:lvl1pPr algn="l">
+              <a:defRPr sz="1200"/>
+            </a:lvl1pPr>
+          </a:lstStyle>
+          <a:p>
+            <a:endParaRPr lang="en-US"/>
+          </a:p>
+        </p:txBody>
+      </p:sp>
+      <p:sp>
+        <p:nvSpPr>
+          <p:cNvPr id="3" name="Date Placeholder 2"/>
+          <p:cNvSpPr>
+            <a:spLocks noGrp="1"/>
+          </p:cNvSpPr>
+          <p:nvPr>
+            <p:ph type="dt" idx="1"/>
+          </p:nvPr>
+        </p:nvSpPr>
+        <p:spPr>
+          <a:xfrm>
+            <a:off x="3884613" y="0"/>
+            <a:ext cx="2971800" cy="457200"/>
+          </a:xfrm>
+          <a:prstGeom prst="rect">
+            <a:avLst/>
+          </a:prstGeom>
+        </p:spPr>
+        <p:txBody>
+          <a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0"/>
+          <a:lstStyle>
+            <a:lvl1pPr algn="r">
+              <a:defRPr sz="1200"/>
+            </a:lvl1pPr>
+          </a:lstStyle>
+          <a:p>
+            <a:fld id="{0F89C1C7-3DCD-1040-A9CF-14679D8B5DDD}" type="datetimeFigureOut">
+              <a:rPr lang="en-US" smtClean="0"/>
+              <a:t>10/17/16</a:t>
+            </a:fld>
+            <a:endParaRPr lang="en-US"/>
+          </a:p>
+        </p:txBody>
+      </p:sp>
+      <p:sp>
+        <p:nvSpPr>
+          <p:cNvPr id="4" name="Slide Image Placeholder 3"/>
+          <p:cNvSpPr>
+            <a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/>
+          </p:cNvSpPr>
+          <p:nvPr>
+            <p:ph type="sldImg" idx="2"/>
+          </p:nvPr>
+        </p:nvSpPr>
+        <p:spPr>
+          <a:xfrm>
+            <a:off x="1143000" y="685800"/>
+            <a:ext cx="4572000" cy="3429000"/>
+          </a:xfrm>
+          <a:prstGeom prst="rect">
+            <a:avLst/>
+          </a:prstGeom>
+          <a:noFill/>
+          <a:ln w="12700">
+            <a:solidFill>
+              <a:prstClr val="black"/>
+            </a:solidFill>
+          </a:ln>
+        </p:spPr>
+        <p:txBody>
+          <a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0" anchor="ctr"/>
+          <a:lstStyle/>
+          <a:p>
+            <a:endParaRPr lang="en-US"/>
+          </a:p>
+        </p:txBody>
+      </p:sp>
+      <p:sp>
+        <p:nvSpPr>
+          <p:cNvPr id="5" name="Notes Placeholder 4"/>
+          <p:cNvSpPr>
+            <a:spLocks noGrp="1"/>
+          </p:cNvSpPr>
+          <p:nvPr>
+            <p:ph type="body" sz="quarter" idx="3"/>
+          </p:nvPr>
+        </p:nvSpPr>
+        <p:spPr>
+          <a:xfrm>
+            <a:off x="685800" y="4343400"/>
+            <a:ext cx="5486400" cy="4114800"/>
+          </a:xfrm>
+          <a:prstGeom prst="rect">
+            <a:avLst/>
+          </a:prstGeom>
+        </p:spPr>
+        <p:txBody>
+          <a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0"/>
+          <a:lstStyle/>
+          <a:p>
+            <a:pPr lvl="0"/>
+            <a:r>
+              <a:rPr lang="en-US" smtClean="0"/>
+              <a:t>Click to edit Master text styles</a:t>
+            </a:r>
+          </a:p>
+          <a:p>
+            <a:pPr lvl="1"/>
+            <a:r>
+              <a:rPr lang="en-US" smtClean="0"/>
+              <a:t>Second level</a:t>
+            </a:r>
+          </a:p>
+          <a:p>
+            <a:pPr lvl="2"/>
+            <a:r>
+              <a:rPr lang="en-US" smtClean="0"/>
+              <a:t>Third level</a:t>
+            </a:r>
+          </a:p>
+          <a:p>
+            <a:pPr lvl="3"/>
+            <a:r>
+              <a:rPr lang="en-US" smtClean="0"/>
+              <a:t>Fourth level</a:t>
+            </a:r>
+          </a:p>
+          <a:p>
+            <a:pPr lvl="4"/>
+            <a:r>
+              <a:rPr lang="en-US" smtClean="0"/>
+              <a:t>Fifth level</a:t>
+            </a:r>
+            <a:endParaRPr lang="en-US"/>
+          </a:p>
+        </p:txBody>
+      </p:sp>
+      <p:sp>
+        <p:nvSpPr>
+          <p:cNvPr id="6" name="Footer Placeholder 5"/>
+          <p:cNvSpPr>
+            <a:spLocks noGrp="1"/>
+          </p:cNvSpPr>
+          <p:nvPr>
+            <p:ph type="ftr" sz="quarter" idx="4"/>
+          </p:nvPr>
+        </p:nvSpPr>
+        <p:spPr>
+          <a:xfrm>
+            <a:off x="0" y="8685213"/>
+            <a:ext cx="2971800" cy="457200"/>
+          </a:xfrm>
+          <a:prstGeom prst="rect">
+            <a:avLst/>
+          </a:prstGeom>
+        </p:spPr>
+        <p:txBody>
+          <a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0" anchor="b"/>
+          <a:lstStyle>
+            <a:lvl1pPr algn="l">
+              <a:defRPr sz="1200"/>
+            </a:lvl1pPr>
+          </a:lstStyle>
+          <a:p>
+            <a:endParaRPr lang="en-US"/>
+          </a:p>
+        </p:txBody>
+      </p:sp>
+      <p:sp>
+        <p:nvSpPr>
+          <p:cNvPr id="7" name="Slide Number Placeholder 6"/>
+          <p:cNvSpPr>
+            <a:spLocks noGrp="1"/>
+          </p:cNvSpPr>
+          <p:nvPr>
+            <p:ph type="sldNum" sz="quarter" idx="5"/>
+          </p:nvPr>
+        </p:nvSpPr>
+        <p:spPr>
+          <a:xfrm>
+            <a:off x="3884613" y="8685213"/>
+            <a:ext cx="2971800" cy="457200"/>
+          </a:xfrm>
+          <a:prstGeom prst="rect">
+            <a:avLst/>
+          </a:prstGeom>
+        </p:spPr>
+        <p:txBody>
+          <a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0" anchor="b"/>
+          <a:lstStyle>
+            <a:lvl1pPr algn="r">
+              <a:defRPr sz="1200"/>
+            </a:lvl1pPr>
+          </a:lstStyle>
+          <a:p>
+            <a:fld id="{BB5E49A5-4136-284D-997B-48E1D791AD67}" type="slidenum">
+              <a:rPr lang="en-US" smtClean="0"/>
+              <a:t>‹#›</a:t>
+            </a:fld>
+            <a:endParaRPr lang="en-US"/>
+          </a:p>
+        </p:txBody>
+      </p:sp>
+    </p:spTree>
+    <p:extLst>
+      <p:ext uri="{BB962C8B-B14F-4D97-AF65-F5344CB8AC3E}">
+        <p14:creationId xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" val="2623252185"/>
+      </p:ext>
+    </p:extLst>
+  </p:cSld>
+  <p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>
+  <p:notesStyle>
+    <a:lvl1pPr marL="0" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+      <a:defRPr sz="1200" kern="1200">
+        <a:solidFill>
+          <a:schemeClr val="tx1"/>
+        </a:solidFill>
+        <a:latin typeface="+mn-lt"/>
+        <a:ea typeface="+mn-ea"/>
+        <a:cs typeface="+mn-cs"/>
+      </a:defRPr>
+    </a:lvl1pPr>
+    <a:lvl2pPr marL="457200" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+      <a:defRPr sz="1200" kern="1200">
+        <a:solidFill>
+          <a:schemeClr val="tx1"/>
+        </a:solidFill>
+        <a:latin typeface="+mn-lt"/>
+        <a:ea typeface="+mn-ea"/>
+        <a:cs typeface="+mn-cs"/>
+      </a:defRPr>
+    </a:lvl2pPr>
+    <a:lvl3pPr marL="914400" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+      <a:defRPr sz="1200" kern="1200">
+        <a:solidFill>
+          <a:schemeClr val="tx1"/>
+        </a:solidFill>
+        <a:latin typeface="+mn-lt"/>
+        <a:ea typeface="+mn-ea"/>
+        <a:cs typeface="+mn-cs"/>
+      </a:defRPr>
+    </a:lvl3pPr>
+    <a:lvl4pPr marL="1371600" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+      <a:defRPr sz="1200" kern="1200">
+        <a:solidFill>
+          <a:schemeClr val="tx1"/>
+        </a:solidFill>
+        <a:latin typeface="+mn-lt"/>
+        <a:ea typeface="+mn-ea"/>
+        <a:cs typeface="+mn-cs"/>
+      </a:defRPr>
+    </a:lvl4pPr>
+    <a:lvl5pPr marL="1828800" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+      <a:defRPr sz="1200" kern="1200">
+        <a:solidFill>
+          <a:schemeClr val="tx1"/>
+        </a:solidFill>
+        <a:latin typeface="+mn-lt"/>
+        <a:ea typeface="+mn-ea"/>
+        <a:cs typeface="+mn-cs"/>
+      </a:defRPr>
+    </a:lvl5pPr>
+    <a:lvl6pPr marL="2286000" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+      <a:defRPr sz="1200" kern="1200">
+        <a:solidFill>
+          <a:schemeClr val="tx1"/>
+        </a:solidFill>
+        <a:latin typeface="+mn-lt"/>
+        <a:ea typeface="+mn-ea"/>
+        <a:cs typeface="+mn-cs"/>
+      </a:defRPr>
+    </a:lvl6pPr>
+    <a:lvl7pPr marL="2743200" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+      <a:defRPr sz="1200" kern="1200">
+        <a:solidFill>
+          <a:schemeClr val="tx1"/>
+        </a:solidFill>
+        <a:latin typeface="+mn-lt"/>
+        <a:ea typeface="+mn-ea"/>
+        <a:cs typeface="+mn-cs"/>
+      </a:defRPr>
+    </a:lvl7pPr>
+    <a:lvl8pPr marL="3200400" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+      <a:defRPr sz="1200" kern="1200">
+        <a:solidFill>
+          <a:schemeClr val="tx1"/>
+        </a:solidFill>
+        <a:latin typeface="+mn-lt"/>
+        <a:ea typeface="+mn-ea"/>
+        <a:cs typeface="+mn-cs"/>
+      </a:defRPr>
+    </a:lvl8pPr>
+    <a:lvl9pPr marL="3657600" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+      <a:defRPr sz="1200" kern="1200">
+        <a:solidFill>
+          <a:schemeClr val="tx1"/>
+        </a:solidFill>
+        <a:latin typeface="+mn-lt"/>
+        <a:ea typeface="+mn-ea"/>
+        <a:cs typeface="+mn-cs"/>
+      </a:defRPr>
+    </a:lvl9pPr>
+  </p:notesStyle>
+</p:notesMaster>
diff --git a/.venv/lib/python3.12/site-packages/pptx/templates/pptx-icon.emf b/.venv/lib/python3.12/site-packages/pptx/templates/pptx-icon.emf
new file mode 100644
index 00000000..e9b1ce88
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/pptx-icon.emf
Binary files differdiff --git a/.venv/lib/python3.12/site-packages/pptx/templates/theme.xml b/.venv/lib/python3.12/site-packages/pptx/templates/theme.xml
new file mode 100644
index 00000000..bf57418d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/theme.xml
@@ -0,0 +1,321 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
+<a:theme
+    xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
+    name="Office Theme"
+    >
+  <a:themeElements>
+    <a:clrScheme name="Office">
+      <a:dk1>
+        <a:sysClr val="windowText" lastClr="000000"/>
+      </a:dk1>
+      <a:lt1>
+        <a:sysClr val="window" lastClr="FFFFFF"/>
+      </a:lt1>
+      <a:dk2>
+        <a:srgbClr val="1F497D"/>
+      </a:dk2>
+      <a:lt2>
+        <a:srgbClr val="EEECE1"/>
+      </a:lt2>
+      <a:accent1>
+        <a:srgbClr val="4F81BD"/>
+      </a:accent1>
+      <a:accent2>
+        <a:srgbClr val="C0504D"/>
+      </a:accent2>
+      <a:accent3>
+        <a:srgbClr val="9BBB59"/>
+      </a:accent3>
+      <a:accent4>
+        <a:srgbClr val="8064A2"/>
+      </a:accent4>
+      <a:accent5>
+        <a:srgbClr val="4BACC6"/>
+      </a:accent5>
+      <a:accent6>
+        <a:srgbClr val="F79646"/>
+      </a:accent6>
+      <a:hlink>
+        <a:srgbClr val="0000FF"/>
+      </a:hlink>
+      <a:folHlink>
+        <a:srgbClr val="800080"/>
+      </a:folHlink>
+    </a:clrScheme>
+    <a:fontScheme name="Office">
+      <a:majorFont>
+        <a:latin typeface="Calibri"/>
+        <a:ea typeface=""/>
+        <a:cs typeface=""/>
+        <a:font script="Jpan" typeface="MS Pゴシック"/>
+        <a:font script="Hang" typeface="맑은 고딕"/>
+        <a:font script="Hans" typeface="宋体"/>
+        <a:font script="Hant" typeface="新細明體"/>
+        <a:font script="Arab" typeface="Times New Roman"/>
+        <a:font script="Hebr" typeface="Times New Roman"/>
+        <a:font script="Thai" typeface="Angsana New"/>
+        <a:font script="Ethi" typeface="Nyala"/>
+        <a:font script="Beng" typeface="Vrinda"/>
+        <a:font script="Gujr" typeface="Shruti"/>
+        <a:font script="Khmr" typeface="MoolBoran"/>
+        <a:font script="Knda" typeface="Tunga"/>
+        <a:font script="Guru" typeface="Raavi"/>
+        <a:font script="Cans" typeface="Euphemia"/>
+        <a:font script="Cher" typeface="Plantagenet Cherokee"/>
+        <a:font script="Yiii" typeface="Microsoft Yi Baiti"/>
+        <a:font script="Tibt" typeface="Microsoft Himalaya"/>
+        <a:font script="Thaa" typeface="MV Boli"/>
+        <a:font script="Deva" typeface="Mangal"/>
+        <a:font script="Telu" typeface="Gautami"/>
+        <a:font script="Taml" typeface="Latha"/>
+        <a:font script="Syrc" typeface="Estrangelo Edessa"/>
+        <a:font script="Orya" typeface="Kalinga"/>
+        <a:font script="Mlym" typeface="Kartika"/>
+        <a:font script="Laoo" typeface="DokChampa"/>
+        <a:font script="Sinh" typeface="Iskoola Pota"/>
+        <a:font script="Mong" typeface="Mongolian Baiti"/>
+        <a:font script="Viet" typeface="Times New Roman"/>
+        <a:font script="Uigh" typeface="Microsoft Uighur"/>
+        <a:font script="Geor" typeface="Sylfaen"/>
+      </a:majorFont>
+      <a:minorFont>
+        <a:latin typeface="Calibri"/>
+        <a:ea typeface=""/>
+        <a:cs typeface=""/>
+        <a:font script="Jpan" typeface="MS Pゴシック"/>
+        <a:font script="Hang" typeface="맑은 고딕"/>
+        <a:font script="Hans" typeface="宋体"/>
+        <a:font script="Hant" typeface="新細明體"/>
+        <a:font script="Arab" typeface="Arial"/>
+        <a:font script="Hebr" typeface="Arial"/>
+        <a:font script="Thai" typeface="Cordia New"/>
+        <a:font script="Ethi" typeface="Nyala"/>
+        <a:font script="Beng" typeface="Vrinda"/>
+        <a:font script="Gujr" typeface="Shruti"/>
+        <a:font script="Khmr" typeface="DaunPenh"/>
+        <a:font script="Knda" typeface="Tunga"/>
+        <a:font script="Guru" typeface="Raavi"/>
+        <a:font script="Cans" typeface="Euphemia"/>
+        <a:font script="Cher" typeface="Plantagenet Cherokee"/>
+        <a:font script="Yiii" typeface="Microsoft Yi Baiti"/>
+        <a:font script="Tibt" typeface="Microsoft Himalaya"/>
+        <a:font script="Thaa" typeface="MV Boli"/>
+        <a:font script="Deva" typeface="Mangal"/>
+        <a:font script="Telu" typeface="Gautami"/>
+        <a:font script="Taml" typeface="Latha"/>
+        <a:font script="Syrc" typeface="Estrangelo Edessa"/>
+        <a:font script="Orya" typeface="Kalinga"/>
+        <a:font script="Mlym" typeface="Kartika"/>
+        <a:font script="Laoo" typeface="DokChampa"/>
+        <a:font script="Sinh" typeface="Iskoola Pota"/>
+        <a:font script="Mong" typeface="Mongolian Baiti"/>
+        <a:font script="Viet" typeface="Arial"/>
+        <a:font script="Uigh" typeface="Microsoft Uighur"/>
+        <a:font script="Geor" typeface="Sylfaen"/>
+      </a:minorFont>
+    </a:fontScheme>
+    <a:fmtScheme name="Office">
+      <a:fillStyleLst>
+        <a:solidFill>
+          <a:schemeClr val="phClr"/>
+        </a:solidFill>
+        <a:gradFill rotWithShape="1">
+          <a:gsLst>
+            <a:gs pos="0">
+              <a:schemeClr val="phClr">
+                <a:tint val="50000"/>
+                <a:satMod val="300000"/>
+              </a:schemeClr>
+            </a:gs>
+            <a:gs pos="35000">
+              <a:schemeClr val="phClr">
+                <a:tint val="37000"/>
+                <a:satMod val="300000"/>
+              </a:schemeClr>
+            </a:gs>
+            <a:gs pos="100000">
+              <a:schemeClr val="phClr">
+                <a:tint val="15000"/>
+                <a:satMod val="350000"/>
+              </a:schemeClr>
+            </a:gs>
+          </a:gsLst>
+          <a:lin ang="16200000" scaled="1"/>
+        </a:gradFill>
+        <a:gradFill rotWithShape="1">
+          <a:gsLst>
+            <a:gs pos="0">
+              <a:schemeClr val="phClr">
+                <a:tint val="100000"/>
+                <a:shade val="100000"/>
+                <a:satMod val="130000"/>
+              </a:schemeClr>
+            </a:gs>
+            <a:gs pos="100000">
+              <a:schemeClr val="phClr">
+                <a:tint val="50000"/>
+                <a:shade val="100000"/>
+                <a:satMod val="350000"/>
+              </a:schemeClr>
+            </a:gs>
+          </a:gsLst>
+          <a:lin ang="16200000" scaled="0"/>
+        </a:gradFill>
+      </a:fillStyleLst>
+      <a:lnStyleLst>
+        <a:ln w="9525" cap="flat" cmpd="sng" algn="ctr">
+          <a:solidFill>
+            <a:schemeClr val="phClr">
+              <a:shade val="95000"/>
+              <a:satMod val="105000"/>
+            </a:schemeClr>
+          </a:solidFill>
+          <a:prstDash val="solid"/>
+        </a:ln>
+        <a:ln w="25400" cap="flat" cmpd="sng" algn="ctr">
+          <a:solidFill>
+            <a:schemeClr val="phClr"/>
+          </a:solidFill>
+          <a:prstDash val="solid"/>
+        </a:ln>
+        <a:ln w="38100" cap="flat" cmpd="sng" algn="ctr">
+          <a:solidFill>
+            <a:schemeClr val="phClr"/>
+          </a:solidFill>
+          <a:prstDash val="solid"/>
+        </a:ln>
+      </a:lnStyleLst>
+      <a:effectStyleLst>
+        <a:effectStyle>
+          <a:effectLst>
+            <a:outerShdw blurRad="40000" dist="20000" dir="5400000" rotWithShape="0">
+              <a:srgbClr val="000000">
+                <a:alpha val="38000"/>
+              </a:srgbClr>
+            </a:outerShdw>
+          </a:effectLst>
+        </a:effectStyle>
+        <a:effectStyle>
+          <a:effectLst>
+            <a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0">
+              <a:srgbClr val="000000">
+                <a:alpha val="35000"/>
+              </a:srgbClr>
+            </a:outerShdw>
+          </a:effectLst>
+        </a:effectStyle>
+        <a:effectStyle>
+          <a:effectLst>
+            <a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0">
+              <a:srgbClr val="000000">
+                <a:alpha val="35000"/>
+              </a:srgbClr>
+            </a:outerShdw>
+          </a:effectLst>
+          <a:scene3d>
+            <a:camera prst="orthographicFront">
+              <a:rot lat="0" lon="0" rev="0"/>
+            </a:camera>
+            <a:lightRig rig="threePt" dir="t">
+              <a:rot lat="0" lon="0" rev="1200000"/>
+            </a:lightRig>
+          </a:scene3d>
+          <a:sp3d>
+            <a:bevelT w="63500" h="25400"/>
+          </a:sp3d>
+        </a:effectStyle>
+      </a:effectStyleLst>
+      <a:bgFillStyleLst>
+        <a:solidFill>
+          <a:schemeClr val="phClr"/>
+        </a:solidFill>
+        <a:gradFill rotWithShape="1">
+          <a:gsLst>
+            <a:gs pos="0">
+              <a:schemeClr val="phClr">
+                <a:tint val="40000"/>
+                <a:satMod val="350000"/>
+              </a:schemeClr>
+            </a:gs>
+            <a:gs pos="40000">
+              <a:schemeClr val="phClr">
+                <a:tint val="45000"/>
+                <a:shade val="99000"/>
+                <a:satMod val="350000"/>
+              </a:schemeClr>
+            </a:gs>
+            <a:gs pos="100000">
+              <a:schemeClr val="phClr">
+                <a:shade val="20000"/>
+                <a:satMod val="255000"/>
+              </a:schemeClr>
+            </a:gs>
+          </a:gsLst>
+          <a:path path="circle">
+            <a:fillToRect l="50000" t="-80000" r="50000" b="180000"/>
+          </a:path>
+        </a:gradFill>
+        <a:gradFill rotWithShape="1">
+          <a:gsLst>
+            <a:gs pos="0">
+              <a:schemeClr val="phClr">
+                <a:tint val="80000"/>
+                <a:satMod val="300000"/>
+              </a:schemeClr>
+            </a:gs>
+            <a:gs pos="100000">
+              <a:schemeClr val="phClr">
+                <a:shade val="30000"/>
+                <a:satMod val="200000"/>
+              </a:schemeClr>
+            </a:gs>
+          </a:gsLst>
+          <a:path path="circle">
+            <a:fillToRect l="50000" t="50000" r="50000" b="50000"/>
+          </a:path>
+        </a:gradFill>
+      </a:bgFillStyleLst>
+    </a:fmtScheme>
+  </a:themeElements>
+  <a:objectDefaults>
+    <a:spDef>
+      <a:spPr/>
+      <a:bodyPr/>
+      <a:lstStyle/>
+      <a:style>
+        <a:lnRef idx="1">
+          <a:schemeClr val="accent1"/>
+        </a:lnRef>
+        <a:fillRef idx="3">
+          <a:schemeClr val="accent1"/>
+        </a:fillRef>
+        <a:effectRef idx="2">
+          <a:schemeClr val="accent1"/>
+        </a:effectRef>
+        <a:fontRef idx="minor">
+          <a:schemeClr val="lt1"/>
+        </a:fontRef>
+      </a:style>
+    </a:spDef>
+    <a:lnDef>
+      <a:spPr/>
+      <a:bodyPr/>
+      <a:lstStyle/>
+      <a:style>
+        <a:lnRef idx="2">
+          <a:schemeClr val="accent1"/>
+        </a:lnRef>
+        <a:fillRef idx="0">
+          <a:schemeClr val="accent1"/>
+        </a:fillRef>
+        <a:effectRef idx="1">
+          <a:schemeClr val="accent1"/>
+        </a:effectRef>
+        <a:fontRef idx="minor">
+          <a:schemeClr val="tx1"/>
+        </a:fontRef>
+      </a:style>
+    </a:lnDef>
+  </a:objectDefaults>
+  <a:extraClrSchemeLst/>
+</a:theme>
diff --git a/.venv/lib/python3.12/site-packages/pptx/templates/xlsx-icon.emf b/.venv/lib/python3.12/site-packages/pptx/templates/xlsx-icon.emf
new file mode 100644
index 00000000..658eac20
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/xlsx-icon.emf
Binary files differdiff --git a/.venv/lib/python3.12/site-packages/pptx/text/__init__.py b/.venv/lib/python3.12/site-packages/pptx/text/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/text/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/text/fonts.py b/.venv/lib/python3.12/site-packages/pptx/text/fonts.py
new file mode 100644
index 00000000..5ae054a8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/text/fonts.py
@@ -0,0 +1,399 @@
+"""Objects related to system font file lookup."""
+
+from __future__ import annotations
+
+import os
+import sys
+from struct import calcsize, unpack_from
+
+from pptx.util import lazyproperty
+
+
+class FontFiles(object):
+    """A class-based singleton serving as a lazy cache for system font details."""
+
+    _font_files = None
+
+    @classmethod
+    def find(cls, family_name: str, is_bold: bool, is_italic: bool) -> str:
+        """Return the absolute path to an installed OpenType font.
+
+        File is matched by `family_name` and the styles `is_bold` and `is_italic`.
+        """
+        if cls._font_files is None:
+            cls._font_files = cls._installed_fonts()
+        return cls._font_files[(family_name, is_bold, is_italic)]
+
+    @classmethod
+    def _installed_fonts(cls):
+        """
+        Return a dict mapping a font descriptor to its font file path,
+        containing all the font files resident on the current machine. The
+        font descriptor is a (family_name, is_bold, is_italic) 3-tuple.
+        """
+        fonts = {}
+        for d in cls._font_directories():
+            for key, path in cls._iter_font_files_in(d):
+                fonts[key] = path
+        return fonts
+
+    @classmethod
+    def _font_directories(cls):
+        """
+        Return a sequence of directory paths likely to contain fonts on the
+        current platform.
+        """
+        if sys.platform.startswith("darwin"):
+            return cls._os_x_font_directories()
+        if sys.platform.startswith("win32"):
+            return cls._windows_font_directories()
+        raise OSError("unsupported operating system")
+
+    @classmethod
+    def _iter_font_files_in(cls, directory):
+        """
+        Generate the OpenType font files found in and under *directory*. Each
+        item is a key/value pair. The key is a (family_name, is_bold,
+        is_italic) 3-tuple, like ('Arial', True, False), and the value is the
+        absolute path to the font file.
+        """
+        for root, dirs, files in os.walk(directory):
+            for filename in files:
+                file_ext = os.path.splitext(filename)[1]
+                if file_ext.lower() not in (".otf", ".ttf"):
+                    continue
+                path = os.path.abspath(os.path.join(root, filename))
+                with _Font.open(path) as f:
+                    yield ((f.family_name, f.is_bold, f.is_italic), path)
+
+    @classmethod
+    def _os_x_font_directories(cls):
+        """
+        Return a sequence of directory paths on a Mac in which fonts are
+        likely to be located.
+        """
+        os_x_font_dirs = [
+            "/Library/Fonts",
+            "/Network/Library/Fonts",
+            "/System/Library/Fonts",
+        ]
+        home = os.environ.get("HOME")
+        if home is not None:
+            os_x_font_dirs.extend(
+                [os.path.join(home, "Library", "Fonts"), os.path.join(home, ".fonts")]
+            )
+        return os_x_font_dirs
+
+    @classmethod
+    def _windows_font_directories(cls):
+        """
+        Return a sequence of directory paths on Windows in which fonts are
+        likely to be located.
+        """
+        return [r"C:\Windows\Fonts"]
+
+
+class _Font(object):
+    """
+    A wrapper around an OTF/TTF font file stream that knows how to parse it
+    for its name and style characteristics, e.g. bold and italic.
+    """
+
+    def __init__(self, stream):
+        self._stream = stream
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exception_type, exception_value, exception_tb):
+        self._stream.close()
+
+    @property
+    def is_bold(self):
+        """
+        |True| if this font is marked as a bold style of its font family.
+        """
+        try:
+            return self._tables["head"].is_bold
+        except KeyError:
+            # some files don't have a head table
+            return False
+
+    @property
+    def is_italic(self):
+        """
+        |True| if this font is marked as an italic style of its font family.
+        """
+        try:
+            return self._tables["head"].is_italic
+        except KeyError:
+            # some files don't have a head table
+            return False
+
+    @classmethod
+    def open(cls, font_file_path):
+        """
+        Return a |_Font| instance loaded from *font_file_path*.
+        """
+        return cls(_Stream.open(font_file_path))
+
+    @property
+    def family_name(self):
+        """
+        The name of the typeface family for this font, e.g. 'Arial'. The full
+        typeface name includes optional style names, such as 'Regular' or
+        'Bold Italic'. This attribute is only the common base name shared by
+        all fonts in the family.
+        """
+        return self._tables["name"].family_name
+
+    @lazyproperty
+    def _fields(self):
+        """5-tuple containing the fields read from the font file header.
+
+        Also known as the offset table.
+        """
+        # sfnt_version, tbl_count, search_range, entry_selector, range_shift
+        return self._stream.read_fields(">4sHHHH", 0)
+
+    def _iter_table_records(self):
+        """
+        Generate a (tag, offset, length) 3-tuple for each of the tables in
+        this font file.
+        """
+        count = self._table_count
+        bufr = self._stream.read(offset=12, length=count * 16)
+        tmpl = ">4sLLL"
+        for i in range(count):
+            offset = i * 16
+            tag, checksum, off, len_ = unpack_from(tmpl, bufr, offset)
+            yield tag.decode("utf-8"), off, len_
+
+    @lazyproperty
+    def _tables(self):
+        """
+        A mapping of OpenType table tag, e.g. 'name', to a table object
+        providing access to the contents of that table.
+        """
+        return dict(
+            (tag, _TableFactory(tag, self._stream, off, len_))
+            for tag, off, len_ in self._iter_table_records()
+        )
+
+    @property
+    def _table_count(self):
+        """
+        The number of tables in this OpenType font file.
+        """
+        return self._fields[1]
+
+
+class _Stream(object):
+    """A thin wrapper around a binary file that facilitates reading C-struct values."""
+
+    def __init__(self, file):
+        self._file = file
+
+    @classmethod
+    def open(cls, path):
+        """Return |_Stream| providing binary access to contents of file at `path`."""
+        return cls(open(path, "rb"))
+
+    def close(self):
+        """
+        Close the wrapped file. Using the stream after closing raises an
+        exception.
+        """
+        self._file.close()
+
+    def read(self, offset, length):
+        """
+        Return *length* bytes from this stream starting at *offset*.
+        """
+        self._file.seek(offset)
+        return self._file.read(length)
+
+    def read_fields(self, template, offset=0):
+        """
+        Return a tuple containing the C-struct fields in this stream
+        specified by *template* and starting at *offset*.
+        """
+        self._file.seek(offset)
+        bufr = self._file.read(calcsize(template))
+        return unpack_from(template, bufr)
+
+
+class _BaseTable(object):
+    """
+    Base class for OpenType font file table objects.
+    """
+
+    def __init__(self, tag, stream, offset, length):
+        self._tag = tag
+        self._stream = stream
+        self._offset = offset
+        self._length = length
+
+
+class _HeadTable(_BaseTable):
+    """
+    OpenType font table having the tag 'head' and containing certain header
+    information for the font, including its bold and/or italic style.
+    """
+
+    def __init__(self, tag, stream, offset, length):
+        super(_HeadTable, self).__init__(tag, stream, offset, length)
+
+    @property
+    def is_bold(self):
+        """
+        |True| if this font is marked as having emboldened characters.
+        """
+        return bool(self._macStyle & 1)
+
+    @property
+    def is_italic(self):
+        """
+        |True| if this font is marked as having italicized characters.
+        """
+        return bool(self._macStyle & 2)
+
+    @lazyproperty
+    def _fields(self):
+        """
+        A 17-tuple containing the fields in this table.
+        """
+        return self._stream.read_fields(">4s4sLLHHqqhhhhHHHHH", self._offset)
+
+    @property
+    def _macStyle(self):
+        """
+        The unsigned short value of the 'macStyle' field in this head table.
+        """
+        return self._fields[12]
+
+
+class _NameTable(_BaseTable):
+    """
+    An OpenType font table having the tag 'name' and containing the
+    name-related strings for the font.
+    """
+
+    def __init__(self, tag, stream, offset, length):
+        super(_NameTable, self).__init__(tag, stream, offset, length)
+
+    @property
+    def family_name(self):
+        """
+        The name of the typeface family for this font, e.g. 'Arial'.
+        """
+
+        def find_first(dict_, keys, default=None):
+            for key in keys:
+                value = dict_.get(key)
+                if value is not None:
+                    return value
+            return default
+
+        # keys for Unicode, Mac, and Windows family name, respectively
+        return find_first(self._names, ((0, 1), (1, 1), (3, 1)))
+
+    @staticmethod
+    def _decode_name(raw_name, platform_id, encoding_id):
+        """
+        Return the unicode name decoded from *raw_name* using the encoding
+        implied by the combination of *platform_id* and *encoding_id*.
+        """
+        if platform_id == 1:
+            # reject non-Roman Mac font names
+            if encoding_id != 0:
+                return None
+            return raw_name.decode("mac-roman")
+        elif platform_id in (0, 3):
+            return raw_name.decode("utf-16-be")
+        else:
+            return None
+
+    def _iter_names(self):
+        """Generate a key/value pair for each name in this table.
+
+        The key is a (platform_id, name_id) 2-tuple and the value is the unicode text
+        corresponding to that key.
+        """
+        table_format, count, strings_offset = self._table_header
+        table_bytes = self._table_bytes
+
+        for idx in range(count):
+            platform_id, name_id, name = self._read_name(table_bytes, idx, strings_offset)
+            if name is None:
+                continue
+            yield ((platform_id, name_id), name)
+
+    @staticmethod
+    def _name_header(bufr, idx):
+        """
+        The (platform_id, encoding_id, language_id, name_id, length,
+        name_str_offset) 6-tuple encoded in each name record C-struct.
+        """
+        name_hdr_offset = 6 + idx * 12
+        return unpack_from(">HHHHHH", bufr, name_hdr_offset)
+
+    @staticmethod
+    def _raw_name_string(bufr, strings_offset, str_offset, length):
+        """
+        Return the *length* bytes comprising the encoded string in *bufr* at
+        *str_offset* in the strings area beginning at *strings_offset*.
+        """
+        offset = strings_offset + str_offset
+        tmpl = "%ds" % length
+        return unpack_from(tmpl, bufr, offset)[0]
+
+    def _read_name(self, bufr, idx, strings_offset):
+        """Return a (platform_id, name_id, name) 3-tuple for name at `idx` in `bufr`.
+
+        The triple looks like (0, 1, 'Arial'). `strings_offset` is the for the name at
+        `idx` position in `bufr`. `strings_offset` is the index into `bufr` where actual
+        name strings begin. The returned name is a unicode string.
+        """
+        platform_id, enc_id, lang_id, name_id, length, str_offset = self._name_header(bufr, idx)
+        name = self._read_name_text(bufr, platform_id, enc_id, strings_offset, str_offset, length)
+        return platform_id, name_id, name
+
+    def _read_name_text(
+        self, bufr, platform_id, encoding_id, strings_offset, name_str_offset, length
+    ):
+        """
+        Return the unicode name string at *name_str_offset* or |None| if
+        decoding its format is not supported.
+        """
+        raw_name = self._raw_name_string(bufr, strings_offset, name_str_offset, length)
+        return self._decode_name(raw_name, platform_id, encoding_id)
+
+    @lazyproperty
+    def _table_bytes(self):
+        """
+        The binary contents of this name table.
+        """
+        return self._stream.read(self._offset, self._length)
+
+    @property
+    def _table_header(self):
+        """
+        The (table_format, name_count, strings_offset) 3-tuple contained
+        in the header of this table.
+        """
+        return unpack_from(">HHH", self._table_bytes)
+
+    @lazyproperty
+    def _names(self):
+        """A mapping of (platform_id, name_id) keys to string names for this font."""
+        return dict(self._iter_names())
+
+
+def _TableFactory(tag, stream, offset, length):
+    """
+    Return an instance of |Table| appropriate to *tag*, loaded from
+    *font_file* with content of *length* starting at *offset*.
+    """
+    TableClass = {"head": _HeadTable, "name": _NameTable}.get(tag, _BaseTable)
+    return TableClass(tag, stream, offset, length)
diff --git a/.venv/lib/python3.12/site-packages/pptx/text/layout.py b/.venv/lib/python3.12/site-packages/pptx/text/layout.py
new file mode 100644
index 00000000..d2b43993
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/text/layout.py
@@ -0,0 +1,325 @@
+"""Objects related to layout of rendered text, such as TextFitter."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from PIL import ImageFont
+
+if TYPE_CHECKING:
+    from pptx.util import Length
+
+
+class TextFitter(tuple):
+    """Value object that knows how to fit text into given rectangular extents."""
+
+    def __new__(cls, line_source, extents, font_file):
+        width, height = extents
+        return tuple.__new__(cls, (line_source, width, height, font_file))
+
+    @classmethod
+    def best_fit_font_size(
+        cls, text: str, extents: tuple[Length, Length], max_size: int, font_file: str
+    ) -> int:
+        """Return whole-number best fit point size less than or equal to `max_size`.
+
+        The return value is the largest whole-number point size less than or equal to
+        `max_size` that allows `text` to fit completely within `extents` when rendered
+        using font defined in `font_file`.
+        """
+        line_source = _LineSource(text)
+        text_fitter = cls(line_source, extents, font_file)
+        return text_fitter._best_fit_font_size(max_size)
+
+    def _best_fit_font_size(self, max_size):
+        """
+        Return the largest whole-number point size less than or equal to
+        *max_size* that this fitter can fit.
+        """
+        predicate = self._fits_inside_predicate
+        sizes = _BinarySearchTree.from_ordered_sequence(range(1, int(max_size) + 1))
+        return sizes.find_max(predicate)
+
+    def _break_line(self, line_source, point_size):
+        """
+        Return a (line, remainder) pair where *line* is the longest line in
+        *line_source* that will fit in this fitter's width and *remainder* is
+        a |_LineSource| object containing the text following the break point.
+        """
+        lines = _BinarySearchTree.from_ordered_sequence(line_source)
+        predicate = self._fits_in_width_predicate(point_size)
+        return lines.find_max(predicate)
+
+    def _fits_in_width_predicate(self, point_size):
+        """
+        Return a function taking a text string value and returns |True| if
+        that text fits in this fitter when rendered at *point_size*. Used as
+        predicate for _break_line()
+        """
+
+        def predicate(line):
+            """
+            Return |True| if *line* fits in this fitter when rendered at
+            *point_size*.
+            """
+            cx = _rendered_size(line.text, point_size, self._font_file)[0]
+            return cx <= self._width
+
+        return predicate
+
+    @property
+    def _fits_inside_predicate(self):
+        """Return  function taking an integer point size argument.
+
+        The function returns |True| if the text in this fitter can be wrapped to fit
+        entirely within its extents when rendered at that point size.
+        """
+
+        def predicate(point_size):
+            """Return |True| when text in `line_source` can be wrapped to fit.
+
+            Fit means text can be broken into lines that fit entirely within `extents`
+            when rendered at `point_size` using the font defined in `font_file`.
+            """
+            text_lines = self._wrap_lines(self._line_source, point_size)
+            cy = _rendered_size("Ty", point_size, self._font_file)[1]
+            return (cy * len(text_lines)) <= self._height
+
+        return predicate
+
+    @property
+    def _font_file(self):
+        return self[3]
+
+    @property
+    def _height(self):
+        return self[2]
+
+    @property
+    def _line_source(self):
+        return self[0]
+
+    @property
+    def _width(self):
+        return self[1]
+
+    def _wrap_lines(self, line_source, point_size):
+        """
+        Return a sequence of str values representing the text in
+        *line_source* wrapped within this fitter when rendered at
+        *point_size*.
+        """
+        text, remainder = self._break_line(line_source, point_size)
+        lines = [text]
+        if remainder:
+            lines.extend(self._wrap_lines(remainder, point_size))
+        return lines
+
+
+class _BinarySearchTree(object):
+    """
+    A node in a binary search tree. Uniform for root, subtree root, and leaf
+    nodes.
+    """
+
+    def __init__(self, value):
+        self._value = value
+        self._lesser = None
+        self._greater = None
+
+    def find_max(self, predicate, max_=None):
+        """
+        Return the largest item in or under this node that satisfies
+        *predicate*.
+        """
+        if predicate(self.value):
+            max_ = self.value
+            next_node = self._greater
+        else:
+            next_node = self._lesser
+        if next_node is None:
+            return max_
+        return next_node.find_max(predicate, max_)
+
+    @classmethod
+    def from_ordered_sequence(cls, iseq):
+        """
+        Return the root of a balanced binary search tree populated with the
+        values in iterable *iseq*.
+        """
+        seq = list(iseq)
+        # optimize for usually all fits by making longest first
+        bst = cls(seq.pop())
+        bst._insert_from_ordered_sequence(seq)
+        return bst
+
+    def insert(self, value):
+        """
+        Insert a new node containing *value* into this tree such that its
+        structure as a binary search tree is preserved.
+        """
+        side = "_lesser" if value < self.value else "_greater"
+        child = getattr(self, side)
+        if child is None:
+            setattr(self, side, _BinarySearchTree(value))
+        else:
+            child.insert(value)
+
+    def tree(self, level=0, prefix=""):
+        """
+        A string representation of the tree rooted in this node, useful for
+        debugging purposes.
+        """
+        text = "%s%s\n" % (prefix, self.value.text)
+        prefix = "%s└── " % ("    " * level)
+        if self._lesser:
+            text += self._lesser.tree(level + 1, prefix)
+        if self._greater:
+            text += self._greater.tree(level + 1, prefix)
+        return text
+
+    @property
+    def value(self):
+        """
+        The value object contained in this node.
+        """
+        return self._value
+
+    @staticmethod
+    def _bisect(seq):
+        """
+        Return a (medial_value, greater_values, lesser_values) 3-tuple
+        obtained by bisecting sequence *seq*.
+        """
+        if len(seq) == 0:
+            return [], None, []
+        mid_idx = int(len(seq) / 2)
+        mid = seq[mid_idx]
+        greater = seq[mid_idx + 1 :]
+        lesser = seq[:mid_idx]
+        return mid, greater, lesser
+
+    def _insert_from_ordered_sequence(self, seq):
+        """
+        Insert the new values contained in *seq* into this tree such that
+        a balanced tree is produced.
+        """
+        if len(seq) == 0:
+            return
+        mid, greater, lesser = self._bisect(seq)
+        self.insert(mid)
+        self._insert_from_ordered_sequence(greater)
+        self._insert_from_ordered_sequence(lesser)
+
+
+class _LineSource(object):
+    """
+    Generates all the possible even-word line breaks in a string of text,
+    each in the form of a (line, remainder) 2-tuple where *line* contains the
+    text before the break and *remainder* the text after as a |_LineSource|
+    object. Its boolean value is |True| when it contains text, |False| when
+    its text is the empty string or whitespace only.
+    """
+
+    def __init__(self, text):
+        self._text = text
+
+    def __bool__(self):
+        """
+        Gives this object boolean behaviors (in Python 3). bool(line_source)
+        is False if it contains the empty string or whitespace only.
+        """
+        return self._text.strip() != ""
+
+    def __eq__(self, other):
+        return self._text == other._text
+
+    def __iter__(self):
+        """
+        Generate a (text, remainder) pair for each possible even-word line
+        break in this line source, where *text* is a str value and remainder
+        is a |_LineSource| value.
+        """
+        words = self._text.split()
+        for idx in range(1, len(words) + 1):
+            line_text = " ".join(words[:idx])
+            remainder_text = " ".join(words[idx:])
+            remainder = _LineSource(remainder_text)
+            yield _Line(line_text, remainder)
+
+    def __nonzero__(self):
+        """
+        Gives this object boolean behaviors (in Python 2). bool(line_source)
+        is False if it contains the empty string or whitespace only.
+        """
+        return self._text.strip() != ""
+
+    def __repr__(self):
+        return "<_LineSource('%s')>" % self._text
+
+
+class _Line(tuple):
+    """
+    A candidate line broken at an even word boundary from a string of text,
+    and a |_LineSource| value containing the text that remains after the line
+    is broken at this spot.
+    """
+
+    def __new__(cls, text, remainder):
+        return tuple.__new__(cls, (text, remainder))
+
+    def __gt__(self, other):
+        return len(self.text) > len(other.text)
+
+    def __lt__(self, other):
+        return not self.__gt__(other)
+
+    def __len__(self):
+        return len(self.text)
+
+    def __repr__(self):
+        return "'%s' => '%s'" % (self.text, self.remainder)
+
+    @property
+    def remainder(self):
+        return self[1]
+
+    @property
+    def text(self):
+        return self[0]
+
+
+class _Fonts(object):
+    """
+    A memoizing cache for ImageFont objects.
+    """
+
+    fonts = {}
+
+    @classmethod
+    def font(cls, font_path, point_size):
+        if (font_path, point_size) not in cls.fonts:
+            cls.fonts[(font_path, point_size)] = ImageFont.truetype(font_path, point_size)
+        return cls.fonts[(font_path, point_size)]
+
+
+def _rendered_size(text, point_size, font_file):
+    """
+    Return a (width, height) pair representing the size of *text* in English
+    Metric Units (EMU) when rendered at *point_size* in the font defined in
+    *font_file*.
+    """
+    emu_per_inch = 914400
+    px_per_inch = 72.0
+
+    font = _Fonts.font(font_file, point_size)
+    try:
+        px_width, px_height = font.getsize(text)
+    except AttributeError:
+        left, top, right, bottom = font.getbbox(text)
+        px_width, px_height = right - left, bottom - top
+
+    emu_width = int(px_width / px_per_inch * emu_per_inch)
+    emu_height = int(px_height / px_per_inch * emu_per_inch)
+
+    return emu_width, emu_height
diff --git a/.venv/lib/python3.12/site-packages/pptx/text/text.py b/.venv/lib/python3.12/site-packages/pptx/text/text.py
new file mode 100644
index 00000000..e139410c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/text/text.py
@@ -0,0 +1,681 @@
+"""Text-related objects such as TextFrame and Paragraph."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Iterator, cast
+
+from pptx.dml.fill import FillFormat
+from pptx.enum.dml import MSO_FILL
+from pptx.enum.lang import MSO_LANGUAGE_ID
+from pptx.enum.text import MSO_AUTO_SIZE, MSO_UNDERLINE, MSO_VERTICAL_ANCHOR
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.oxml.simpletypes import ST_TextWrappingType
+from pptx.shapes import Subshape
+from pptx.text.fonts import FontFiles
+from pptx.text.layout import TextFitter
+from pptx.util import Centipoints, Emu, Length, Pt, lazyproperty
+
+if TYPE_CHECKING:
+    from pptx.dml.color import ColorFormat
+    from pptx.enum.text import (
+        MSO_TEXT_UNDERLINE_TYPE,
+        MSO_VERTICAL_ANCHOR,
+        PP_PARAGRAPH_ALIGNMENT,
+    )
+    from pptx.oxml.action import CT_Hyperlink
+    from pptx.oxml.text import (
+        CT_RegularTextRun,
+        CT_TextBody,
+        CT_TextCharacterProperties,
+        CT_TextParagraph,
+        CT_TextParagraphProperties,
+    )
+    from pptx.types import ProvidesExtents, ProvidesPart
+
+
+class TextFrame(Subshape):
+    """The part of a shape that contains its text.
+
+    Not all shapes have a text frame. Corresponds to the `p:txBody` element that can
+    appear as a child element of `p:sp`. Not intended to be constructed directly.
+    """
+
+    def __init__(self, txBody: CT_TextBody, parent: ProvidesPart):
+        super(TextFrame, self).__init__(parent)
+        self._element = self._txBody = txBody
+        self._parent = parent
+
+    def add_paragraph(self):
+        """
+        Return new |_Paragraph| instance appended to the sequence of
+        paragraphs contained in this text frame.
+        """
+        p = self._txBody.add_p()
+        return _Paragraph(p, self)
+
+    @property
+    def auto_size(self) -> MSO_AUTO_SIZE | None:
+        """Resizing strategy used to fit text within this shape.
+
+        Determins the type of automatic resizing used to fit the text of this shape within its
+        bounding box when the text would otherwise extend beyond the shape boundaries. May be
+        |None|, `MSO_AUTO_SIZE.NONE`, `MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT`, or
+        `MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE`.
+        """
+        return self._bodyPr.autofit
+
+    @auto_size.setter
+    def auto_size(self, value: MSO_AUTO_SIZE | None):
+        self._bodyPr.autofit = value
+
+    def clear(self):
+        """Remove all paragraphs except one empty one."""
+        for p in self._txBody.p_lst[1:]:
+            self._txBody.remove(p)
+        p = self.paragraphs[0]
+        p.clear()
+
+    def fit_text(
+        self,
+        font_family: str = "Calibri",
+        max_size: int = 18,
+        bold: bool = False,
+        italic: bool = False,
+        font_file: str | None = None,
+    ):
+        """Fit text-frame text entirely within bounds of its shape.
+
+        Make the text in this text frame fit entirely within the bounds of its shape by setting
+        word wrap on and applying the "best-fit" font size to all the text it contains.
+
+        :attr:`TextFrame.auto_size` is set to :attr:`MSO_AUTO_SIZE.NONE`. The font size will not
+        be set larger than `max_size` points. If the path to a matching TrueType font is provided
+        as `font_file`, that font file will be used for the font metrics. If `font_file` is |None|,
+        best efforts are made to locate a font file with matchhing `font_family`, `bold`, and
+        `italic` installed on the current system (usually succeeds if the font is installed).
+        """
+        # ---no-op when empty as fit behavior not defined for that case---
+        if self.text == "":
+            return  # pragma: no cover
+
+        font_size = self._best_fit_font_size(font_family, max_size, bold, italic, font_file)
+        self._apply_fit(font_family, font_size, bold, italic)
+
+    @property
+    def margin_bottom(self) -> Length:
+        """|Length| value representing the inset of text from the bottom text frame border.
+
+        :meth:`pptx.util.Inches` provides a convenient way of setting the value, e.g.
+        `text_frame.margin_bottom = Inches(0.05)`.
+        """
+        return self._bodyPr.bIns
+
+    @margin_bottom.setter
+    def margin_bottom(self, emu: Length):
+        self._bodyPr.bIns = emu
+
+    @property
+    def margin_left(self) -> Length:
+        """Inset of text from left text frame border as |Length| value."""
+        return self._bodyPr.lIns
+
+    @margin_left.setter
+    def margin_left(self, emu: Length):
+        self._bodyPr.lIns = emu
+
+    @property
+    def margin_right(self) -> Length:
+        """Inset of text from right text frame border as |Length| value."""
+        return self._bodyPr.rIns
+
+    @margin_right.setter
+    def margin_right(self, emu: Length):
+        self._bodyPr.rIns = emu
+
+    @property
+    def margin_top(self) -> Length:
+        """Inset of text from top text frame border as |Length| value."""
+        return self._bodyPr.tIns
+
+    @margin_top.setter
+    def margin_top(self, emu: Length):
+        self._bodyPr.tIns = emu
+
+    @property
+    def paragraphs(self) -> tuple[_Paragraph, ...]:
+        """Sequence of paragraphs in this text frame.
+
+        A text frame always contains at least one paragraph.
+        """
+        return tuple([_Paragraph(p, self) for p in self._txBody.p_lst])
+
+    @property
+    def text(self) -> str:
+        """All text in this text-frame as a single string.
+
+        Read/write. The return value contains all text in this text-frame. A line-feed character
+        (`"\\n"`) separates the text for each paragraph. A vertical-tab character (`"\\v"`) appears
+        for each line break (aka. soft carriage-return) encountered.
+
+        The vertical-tab character is how PowerPoint represents a soft carriage return in clipboard
+        text, which is why that encoding was chosen.
+
+        Assignment replaces all text in the text frame. A new paragraph is added for each line-feed
+        character (`"\\n"`) encountered. A line-break (soft carriage-return) is inserted for each
+        vertical-tab character (`"\\v"`) encountered.
+
+        Any control character other than newline, tab, or vertical-tab are escaped as plain-text
+        like "_x001B_" (for ESC (ASCII 32) in this example).
+        """
+        return "\n".join(paragraph.text for paragraph in self.paragraphs)
+
+    @text.setter
+    def text(self, text: str):
+        txBody = self._txBody
+        txBody.clear_content()
+        for p_text in text.split("\n"):
+            p = txBody.add_p()
+            p.append_text(p_text)
+
+    @property
+    def vertical_anchor(self) -> MSO_VERTICAL_ANCHOR | None:
+        """Represents the vertical alignment of text in this text frame.
+
+        |None| indicates the effective value should be inherited from this object's style hierarchy.
+        """
+        return self._txBody.bodyPr.anchor
+
+    @vertical_anchor.setter
+    def vertical_anchor(self, value: MSO_VERTICAL_ANCHOR | None):
+        bodyPr = self._txBody.bodyPr
+        bodyPr.anchor = value
+
+    @property
+    def word_wrap(self) -> bool | None:
+        """`True` when lines of text in this shape are wrapped to fit within the shape's width.
+
+        Read-write. Valid values are True, False, or None. True and False turn word wrap on and
+        off, respectively. Assigning None to word wrap causes any word wrap setting to be removed
+        from the text frame, causing it to inherit this setting from its style hierarchy.
+        """
+        return {
+            ST_TextWrappingType.SQUARE: True,
+            ST_TextWrappingType.NONE: False,
+            None: None,
+        }[self._txBody.bodyPr.wrap]
+
+    @word_wrap.setter
+    def word_wrap(self, value: bool | None):
+        if value not in (True, False, None):
+            raise ValueError(  # pragma: no cover
+                "assigned value must be True, False, or None, got %s" % value
+            )
+        self._txBody.bodyPr.wrap = {
+            True: ST_TextWrappingType.SQUARE,
+            False: ST_TextWrappingType.NONE,
+            None: None,
+        }[value]
+
+    def _apply_fit(self, font_family: str, font_size: int, is_bold: bool, is_italic: bool):
+        """Arrange text in this text frame to fit inside its extents.
+
+        This is accomplished by setting auto size off, wrap on, and setting the font of
+        all its text to `font_family`, `font_size`, `is_bold`, and `is_italic`.
+        """
+        self.auto_size = MSO_AUTO_SIZE.NONE
+        self.word_wrap = True
+        self._set_font(font_family, font_size, is_bold, is_italic)
+
+    def _best_fit_font_size(
+        self, family: str, max_size: int, bold: bool, italic: bool, font_file: str | None
+    ) -> int:
+        """Return font-size in points that best fits text in this text-frame.
+
+        The best-fit font size is the largest integer point size not greater than `max_size` that
+        allows all the text in this text frame to fit inside its extents when rendered using the
+        font described by `family`, `bold`, and `italic`. If `font_file` is specified, it is used
+        to calculate the fit, whether or not it matches `family`, `bold`, and `italic`.
+        """
+        if font_file is None:
+            font_file = FontFiles.find(family, bold, italic)
+        return TextFitter.best_fit_font_size(self.text, self._extents, max_size, font_file)
+
+    @property
+    def _bodyPr(self):
+        return self._txBody.bodyPr
+
+    @property
+    def _extents(self) -> tuple[Length, Length]:
+        """(cx, cy) 2-tuple representing the effective rendering area of this text-frame.
+
+        Margins are taken into account.
+        """
+        parent = cast("ProvidesExtents", self._parent)
+        return (
+            Length(parent.width - self.margin_left - self.margin_right),
+            Length(parent.height - self.margin_top - self.margin_bottom),
+        )
+
+    def _set_font(self, family: str, size: int, bold: bool, italic: bool):
+        """Set the font properties of all the text in this text frame."""
+
+        def iter_rPrs(txBody: CT_TextBody) -> Iterator[CT_TextCharacterProperties]:
+            for p in txBody.p_lst:
+                for elm in p.content_children:
+                    yield elm.get_or_add_rPr()
+                # generate a:endParaRPr for each <a:p> element
+                yield p.get_or_add_endParaRPr()
+
+        def set_rPr_font(
+            rPr: CT_TextCharacterProperties, name: str, size: int, bold: bool, italic: bool
+        ):
+            f = Font(rPr)
+            f.name, f.size, f.bold, f.italic = family, Pt(size), bold, italic
+
+        txBody = self._element
+        for rPr in iter_rPrs(txBody):
+            set_rPr_font(rPr, family, size, bold, italic)
+
+
+class Font(object):
+    """Character properties object, providing font size, font name, bold, italic, etc.
+
+    Corresponds to `a:rPr` child element of a run. Also appears as `a:defRPr` and
+    `a:endParaRPr` in paragraph and `a:defRPr` in list style elements.
+    """
+
+    def __init__(self, rPr: CT_TextCharacterProperties):
+        super(Font, self).__init__()
+        self._element = self._rPr = rPr
+
+    @property
+    def bold(self) -> bool | None:
+        """Get or set boolean bold value of |Font|, e.g. `paragraph.font.bold = True`.
+
+        If set to |None|, the bold setting is cleared and is inherited from an enclosing shape's
+        setting, or a setting in a style or master. Returns None if no bold attribute is present,
+        meaning the effective bold value is inherited from a master or the theme.
+        """
+        return self._rPr.b
+
+    @bold.setter
+    def bold(self, value: bool | None):
+        self._rPr.b = value
+
+    @lazyproperty
+    def color(self) -> ColorFormat:
+        """The |ColorFormat| instance that provides access to the color settings for this font."""
+        if self.fill.type != MSO_FILL.SOLID:
+            self.fill.solid()
+        return self.fill.fore_color
+
+    @lazyproperty
+    def fill(self) -> FillFormat:
+        """|FillFormat| instance for this font.
+
+        Provides access to fill properties such as fill color.
+        """
+        return FillFormat.from_fill_parent(self._rPr)
+
+    @property
+    def italic(self) -> bool | None:
+        """Get or set boolean italic value of |Font| instance.
+
+        Has the same behaviors as bold with respect to None values.
+        """
+        return self._rPr.i
+
+    @italic.setter
+    def italic(self, value: bool | None):
+        self._rPr.i = value
+
+    @property
+    def language_id(self) -> MSO_LANGUAGE_ID | None:
+        """Get or set the language id of this |Font| instance.
+
+        The language id is a member of the :ref:`MsoLanguageId` enumeration. Assigning |None|
+        removes any language setting, the same behavior as assigning `MSO_LANGUAGE_ID.NONE`.
+        """
+        lang = self._rPr.lang
+        if lang is None:
+            return MSO_LANGUAGE_ID.NONE
+        return self._rPr.lang
+
+    @language_id.setter
+    def language_id(self, value: MSO_LANGUAGE_ID | None):
+        if value == MSO_LANGUAGE_ID.NONE:
+            value = None
+        self._rPr.lang = value
+
+    @property
+    def name(self) -> str | None:
+        """Get or set the typeface name for this |Font| instance.
+
+        Causes the text it controls to appear in the named font, if a matching font is found.
+        Returns |None| if the typeface is currently inherited from the theme. Setting it to |None|
+        removes any override of the theme typeface.
+        """
+        latin = self._rPr.latin
+        if latin is None:
+            return None
+        return latin.typeface
+
+    @name.setter
+    def name(self, value: str | None):
+        if value is None:
+            self._rPr._remove_latin()  # pyright: ignore[reportPrivateUsage]
+        else:
+            latin = self._rPr.get_or_add_latin()
+            latin.typeface = value
+
+    @property
+    def size(self) -> Length | None:
+        """Indicates the font height in English Metric Units (EMU).
+
+        Read/write. |None| indicates the font size should be inherited from its style hierarchy,
+        such as a placeholder or document defaults (usually 18pt). |Length| is a subclass of |int|
+        having properties for convenient conversion into points or other length units. Likewise,
+        the :class:`pptx.util.Pt` class allows convenient specification of point values::
+
+            >>> font.size = Pt(24)
+            >>> font.size
+            304800
+            >>> font.size.pt
+            24.0
+        """
+        sz = self._rPr.sz
+        if sz is None:
+            return None
+        return Centipoints(sz)
+
+    @size.setter
+    def size(self, emu: Length | None):
+        if emu is None:
+            self._rPr.sz = None
+        else:
+            sz = Emu(emu).centipoints
+            self._rPr.sz = sz
+
+    @property
+    def underline(self) -> bool | MSO_TEXT_UNDERLINE_TYPE | None:
+        """Indicaties the underline setting for this font.
+
+        Value is |True|, |False|, |None|, or a member of the :ref:`MsoTextUnderlineType`
+        enumeration. |None| is the default and indicates the underline setting should be inherited
+        from the style hierarchy, such as from a placeholder. |True| indicates single underline.
+        |False| indicates no underline. Other settings such as double and wavy underlining are
+        indicated with members of the :ref:`MsoTextUnderlineType` enumeration.
+        """
+        u = self._rPr.u
+        if u is MSO_UNDERLINE.NONE:
+            return False
+        if u is MSO_UNDERLINE.SINGLE_LINE:
+            return True
+        return u
+
+    @underline.setter
+    def underline(self, value: bool | MSO_TEXT_UNDERLINE_TYPE | None):
+        if value is True:
+            value = MSO_UNDERLINE.SINGLE_LINE
+        elif value is False:
+            value = MSO_UNDERLINE.NONE
+        self._element.u = value
+
+
+class _Hyperlink(Subshape):
+    """Text run hyperlink object.
+
+    Corresponds to `a:hlinkClick` child element of the run's properties element (`a:rPr`).
+    """
+
+    def __init__(self, rPr: CT_TextCharacterProperties, parent: ProvidesPart):
+        super(_Hyperlink, self).__init__(parent)
+        self._rPr = rPr
+
+    @property
+    def address(self) -> str | None:
+        """The URL of the hyperlink.
+
+        Read/write. URL can be on http, https, mailto, or file scheme; others may work.
+        """
+        if self._hlinkClick is None:
+            return None
+        return self.part.target_ref(self._hlinkClick.rId)
+
+    @address.setter
+    def address(self, url: str | None):
+        # implements all three of add, change, and remove hyperlink
+        if self._hlinkClick is not None:
+            self._remove_hlinkClick()
+        if url:
+            self._add_hlinkClick(url)
+
+    def _add_hlinkClick(self, url: str):
+        rId = self.part.relate_to(url, RT.HYPERLINK, is_external=True)
+        self._rPr.add_hlinkClick(rId)
+
+    @property
+    def _hlinkClick(self) -> CT_Hyperlink | None:
+        return self._rPr.hlinkClick
+
+    def _remove_hlinkClick(self):
+        assert self._hlinkClick is not None
+        self.part.drop_rel(self._hlinkClick.rId)
+        self._rPr._remove_hlinkClick()  # pyright: ignore[reportPrivateUsage]
+
+
+class _Paragraph(Subshape):
+    """Paragraph object. Not intended to be constructed directly."""
+
+    def __init__(self, p: CT_TextParagraph, parent: ProvidesPart):
+        super(_Paragraph, self).__init__(parent)
+        self._element = self._p = p
+
+    def add_line_break(self):
+        """Add line break at end of this paragraph."""
+        self._p.add_br()
+
+    def add_run(self) -> _Run:
+        """Return a new run appended to the runs in this paragraph."""
+        r = self._p.add_r()
+        return _Run(r, self)
+
+    @property
+    def alignment(self) -> PP_PARAGRAPH_ALIGNMENT | None:
+        """Horizontal alignment of this paragraph.
+
+        The value |None| indicates the paragraph should 'inherit' its effective value from its
+        style hierarchy. Assigning |None| removes any explicit setting, causing its inherited
+        value to be used.
+        """
+        return self._pPr.algn
+
+    @alignment.setter
+    def alignment(self, value: PP_PARAGRAPH_ALIGNMENT | None):
+        self._pPr.algn = value
+
+    def clear(self):
+        """Remove all content from this paragraph.
+
+        Paragraph properties are preserved. Content includes runs, line breaks, and fields.
+        """
+        for elm in self._element.content_children:
+            self._element.remove(elm)
+        return self
+
+    @property
+    def font(self) -> Font:
+        """|Font| object containing default character properties for the runs in this paragraph.
+
+        These character properties override default properties inherited from parent objects such
+        as the text frame the paragraph is contained in and they may be overridden by character
+        properties set at the run level.
+        """
+        return Font(self._defRPr)
+
+    @property
+    def level(self) -> int:
+        """Indentation level of this paragraph.
+
+        Read-write. Integer in range 0..8 inclusive. 0 represents a top-level paragraph and is the
+        default value. Indentation level is most commonly encountered in a bulleted list, as is
+        found on a word bullet slide.
+        """
+        return self._pPr.lvl
+
+    @level.setter
+    def level(self, level: int):
+        self._pPr.lvl = level
+
+    @property
+    def line_spacing(self) -> int | float | Length | None:
+        """The space between baselines in successive lines of this paragraph.
+
+        A value of |None| indicates no explicit value is assigned and its effective value is
+        inherited from the paragraph's style hierarchy. A numeric value, e.g. `2` or `1.5`,
+        indicates spacing is applied in multiples of line heights. A |Length| value such as
+        `Pt(12)` indicates spacing is a fixed height. The |Pt| value class is a convenient way to
+        apply line spacing in units of points.
+        """
+        pPr = self._p.pPr
+        if pPr is None:
+            return None
+        return pPr.line_spacing
+
+    @line_spacing.setter
+    def line_spacing(self, value: int | float | Length | None):
+        pPr = self._p.get_or_add_pPr()
+        pPr.line_spacing = value
+
+    @property
+    def runs(self) -> tuple[_Run, ...]:
+        """Sequence of runs in this paragraph."""
+        return tuple(_Run(r, self) for r in self._element.r_lst)
+
+    @property
+    def space_after(self) -> Length | None:
+        """The spacing to appear between this paragraph and the subsequent paragraph.
+
+        A value of |None| indicates no explicit value is assigned and its effective value is
+        inherited from the paragraph's style hierarchy. |Length| objects provide convenience
+        properties, such as `.pt` and `.inches`, that allow easy conversion to various length
+        units.
+        """
+        pPr = self._p.pPr
+        if pPr is None:
+            return None
+        return pPr.space_after
+
+    @space_after.setter
+    def space_after(self, value: Length | None):
+        pPr = self._p.get_or_add_pPr()
+        pPr.space_after = value
+
+    @property
+    def space_before(self) -> Length | None:
+        """The spacing to appear between this paragraph and the prior paragraph.
+
+        A value of |None| indicates no explicit value is assigned and its effective value is
+        inherited from the paragraph's style hierarchy. |Length| objects provide convenience
+        properties, such as `.pt` and `.cm`, that allow easy conversion to various length units.
+        """
+        pPr = self._p.pPr
+        if pPr is None:
+            return None
+        return pPr.space_before
+
+    @space_before.setter
+    def space_before(self, value: Length | None):
+        pPr = self._p.get_or_add_pPr()
+        pPr.space_before = value
+
+    @property
+    def text(self) -> str:
+        """Text of paragraph as a single string.
+
+        Read/write. This value is formed by concatenating the text in each run and field making up
+        the paragraph, adding a vertical-tab character (`"\\v"`) for each line-break element
+        (`<a:br>`, soft carriage-return) encountered.
+
+        While the encoding of line-breaks as a vertical tab might be surprising at first, doing so
+        is consistent with PowerPoint's clipboard copy behavior and allows a line-break to be
+        distinguished from a paragraph boundary within the str return value.
+
+        Assignment causes all content in the paragraph to be replaced. Each vertical-tab character
+        (`"\\v"`) in the assigned str is translated to a line-break, as is each line-feed
+        character (`"\\n"`). Contrast behavior of line-feed character in `TextFrame.text` setter.
+        If line-feed characters are intended to produce new paragraphs, use `TextFrame.text`
+        instead. Any other control characters in the assigned string are escaped as a hex
+        representation like "_x001B_" (for ESC (ASCII 27) in this example).
+        """
+        return "".join(elm.text for elm in self._element.content_children)
+
+    @text.setter
+    def text(self, text: str):
+        self.clear()
+        self._element.append_text(text)
+
+    @property
+    def _defRPr(self) -> CT_TextCharacterProperties:
+        """The element that defines the default run properties for runs in this paragraph.
+
+        Causes the element to be added if not present.
+        """
+        return self._pPr.get_or_add_defRPr()
+
+    @property
+    def _pPr(self) -> CT_TextParagraphProperties:
+        """Contains the properties for this paragraph.
+
+        Causes the element to be added if not present.
+        """
+        return self._p.get_or_add_pPr()
+
+
+class _Run(Subshape):
+    """Text run object. Corresponds to `a:r` child element in a paragraph."""
+
+    def __init__(self, r: CT_RegularTextRun, parent: ProvidesPart):
+        super(_Run, self).__init__(parent)
+        self._r = r
+
+    @property
+    def font(self):
+        """|Font| instance containing run-level character properties for the text in this run.
+
+        Character properties can be and perhaps most often are inherited from parent objects such
+        as the paragraph and slide layout the run is contained in. Only those specifically
+        overridden at the run level are contained in the font object.
+        """
+        rPr = self._r.get_or_add_rPr()
+        return Font(rPr)
+
+    @lazyproperty
+    def hyperlink(self) -> _Hyperlink:
+        """Proxy for any `a:hlinkClick` element under the run properties element.
+
+        Created on demand, the hyperlink object is available whether an `a:hlinkClick` element is
+        present or not, and creates or deletes that element as appropriate in response to actions
+        on its methods and attributes.
+        """
+        rPr = self._r.get_or_add_rPr()
+        return _Hyperlink(rPr, self)
+
+    @property
+    def text(self):
+        """Read/write. A unicode string containing the text in this run.
+
+        Assignment replaces all text in the run. The assigned value can be a 7-bit ASCII
+        string, a UTF-8 encoded 8-bit string, or unicode. String values are converted to
+        unicode assuming UTF-8 encoding.
+
+        Any other control characters in the assigned string other than tab or newline
+        are escaped as a hex representation. For example, ESC (ASCII 27) is escaped as
+        "_x001B_". Contrast the behavior of `TextFrame.text` and `_Paragraph.text` with
+        respect to line-feed and vertical-tab characters.
+        """
+        return self._r.text
+
+    @text.setter
+    def text(self, text: str):
+        self._r.text = text
diff --git a/.venv/lib/python3.12/site-packages/pptx/types.py b/.venv/lib/python3.12/site-packages/pptx/types.py
new file mode 100644
index 00000000..46d86661
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/types.py
@@ -0,0 +1,36 @@
+"""Abstract types used by `python-pptx`."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from typing_extensions import Protocol
+
+if TYPE_CHECKING:
+    from pptx.opc.package import XmlPart
+    from pptx.util import Length
+
+
+class ProvidesExtents(Protocol):
+    """An object that has width and height."""
+
+    @property
+    def height(self) -> Length:
+        """Distance between top and bottom extents of shape in EMUs."""
+        ...
+
+    @property
+    def width(self) -> Length:
+        """Distance between left and right extents of shape in EMUs."""
+        ...
+
+
+class ProvidesPart(Protocol):
+    """An object that provides access to its XmlPart.
+
+    This type is for objects that need access to their part, possibly because they need access to
+    the package or related parts.
+    """
+
+    @property
+    def part(self) -> XmlPart: ...
diff --git a/.venv/lib/python3.12/site-packages/pptx/util.py b/.venv/lib/python3.12/site-packages/pptx/util.py
new file mode 100644
index 00000000..fdec7929
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/util.py
@@ -0,0 +1,214 @@
+"""Utility functions and classes."""
+
+from __future__ import annotations
+
+import functools
+from typing import Any, Callable, Generic, TypeVar, cast
+
+
+class Length(int):
+    """Base class for length classes Inches, Emu, Cm, Mm, and Pt.
+
+    Provides properties for converting length values to convenient units.
+    """
+
+    _EMUS_PER_INCH = 914400
+    _EMUS_PER_CENTIPOINT = 127
+    _EMUS_PER_CM = 360000
+    _EMUS_PER_MM = 36000
+    _EMUS_PER_PT = 12700
+
+    def __new__(cls, emu: int):
+        return int.__new__(cls, emu)
+
+    @property
+    def inches(self) -> float:
+        """Floating point length in inches."""
+        return self / float(self._EMUS_PER_INCH)
+
+    @property
+    def centipoints(self) -> int:
+        """Integer length in hundredths of a point (1/7200 inch).
+
+        Used internally because PowerPoint stores font size in centipoints.
+        """
+        return self // self._EMUS_PER_CENTIPOINT
+
+    @property
+    def cm(self) -> float:
+        """Floating point length in centimeters."""
+        return self / float(self._EMUS_PER_CM)
+
+    @property
+    def emu(self) -> int:
+        """Integer length in English Metric Units."""
+        return self
+
+    @property
+    def mm(self) -> float:
+        """Floating point length in millimeters."""
+        return self / float(self._EMUS_PER_MM)
+
+    @property
+    def pt(self) -> float:
+        """Floating point length in points."""
+        return self / float(self._EMUS_PER_PT)
+
+
+class Inches(Length):
+    """Convenience constructor for length in inches."""
+
+    def __new__(cls, inches: float):
+        emu = int(inches * Length._EMUS_PER_INCH)
+        return Length.__new__(cls, emu)
+
+
+class Centipoints(Length):
+    """Convenience constructor for length in hundredths of a point."""
+
+    def __new__(cls, centipoints: int):
+        emu = int(centipoints * Length._EMUS_PER_CENTIPOINT)
+        return Length.__new__(cls, emu)
+
+
+class Cm(Length):
+    """Convenience constructor for length in centimeters."""
+
+    def __new__(cls, cm: float):
+        emu = int(cm * Length._EMUS_PER_CM)
+        return Length.__new__(cls, emu)
+
+
+class Emu(Length):
+    """Convenience constructor for length in english metric units."""
+
+    def __new__(cls, emu: int):
+        return Length.__new__(cls, int(emu))
+
+
+class Mm(Length):
+    """Convenience constructor for length in millimeters."""
+
+    def __new__(cls, mm: float):
+        emu = int(mm * Length._EMUS_PER_MM)
+        return Length.__new__(cls, emu)
+
+
+class Pt(Length):
+    """Convenience value class for specifying a length in points."""
+
+    def __new__(cls, points: float):
+        emu = int(points * Length._EMUS_PER_PT)
+        return Length.__new__(cls, emu)
+
+
+_T = TypeVar("_T")
+
+
+class lazyproperty(Generic[_T]):
+    """Decorator like @property, but evaluated only on first access.
+
+    Like @property, this can only be used to decorate methods having only a `self` parameter, and
+    is accessed like an attribute on an instance, i.e. trailing parentheses are not used. Unlike
+    @property, the decorated method is only evaluated on first access; the resulting value is
+    cached and that same value returned on second and later access without re-evaluation of the
+    method.
+
+    Like @property, this class produces a *data descriptor* object, which is stored in the __dict__
+    of the *class* under the name of the decorated method ('fget' nominally). The cached value is
+    stored in the __dict__ of the *instance* under that same name.
+
+    Because it is a data descriptor (as opposed to a *non-data descriptor*), its `__get__()` method
+    is executed on each access of the decorated attribute; the __dict__ item of the same name is
+    "shadowed" by the descriptor.
+
+    While this may represent a performance improvement over a property, its greater benefit may be
+    its other characteristics. One common use is to construct collaborator objects, removing that
+    "real work" from the constructor, while still only executing once. It also de-couples client
+    code from any sequencing considerations; if it's accessed from more than one location, it's
+    assured it will be ready whenever needed.
+
+    Loosely based on: https://stackoverflow.com/a/6849299/1902513.
+
+    A lazyproperty is read-only. There is no counterpart to the optional "setter" (or deleter)
+    behavior of an @property. This is critically important to maintaining its immutability and
+    idempotence guarantees. Attempting to assign to a lazyproperty raises AttributeError
+    unconditionally.
+
+    The parameter names in the methods below correspond to this usage example::
+
+        class Obj(object)
+
+            @lazyproperty
+            def fget(self):
+                return 'some result'
+
+        obj = Obj()
+
+    Not suitable for wrapping a function (as opposed to a method) because it is not callable.
+    """
+
+    def __init__(self, fget: Callable[..., _T]) -> None:
+        """*fget* is the decorated method (a "getter" function).
+
+        A lazyproperty is read-only, so there is only an *fget* function (a regular
+        @property can also have an fset and fdel function). This name was chosen for
+        consistency with Python's `property` class which uses this name for the
+        corresponding parameter.
+        """
+        # --- maintain a reference to the wrapped getter method
+        self._fget = fget
+        # --- and store the name of that decorated method
+        self._name = fget.__name__
+        # --- adopt fget's __name__, __doc__, and other attributes
+        functools.update_wrapper(self, fget)  # pyright: ignore
+
+    def __get__(self, obj: Any, type: Any = None) -> _T:
+        """Called on each access of 'fget' attribute on class or instance.
+
+        *self* is this instance of a lazyproperty descriptor "wrapping" the property
+        method it decorates (`fget`, nominally).
+
+        *obj* is the "host" object instance when the attribute is accessed from an
+        object instance, e.g. `obj = Obj(); obj.fget`. *obj* is None when accessed on
+        the class, e.g. `Obj.fget`.
+
+        *type* is the class hosting the decorated getter method (`fget`) on both class
+        and instance attribute access.
+        """
+        # --- when accessed on class, e.g. Obj.fget, just return this descriptor
+        # --- instance (patched above to look like fget).
+        if obj is None:
+            return self  # type: ignore
+
+        # --- when accessed on instance, start by checking instance __dict__ for
+        # --- item with key matching the wrapped function's name
+        value = obj.__dict__.get(self._name)
+        if value is None:
+            # --- on first access, the __dict__ item will be absent. Evaluate fget()
+            # --- and store that value in the (otherwise unused) host-object
+            # --- __dict__ value of same name ('fget' nominally)
+            value = self._fget(obj)
+            obj.__dict__[self._name] = value
+        return cast(_T, value)
+
+    def __set__(self, obj: Any, value: Any) -> None:
+        """Raises unconditionally, to preserve read-only behavior.
+
+        This decorator is intended to implement immutable (and idempotent) object
+        attributes. For that reason, assignment to this property must be explicitly
+        prevented.
+
+        If this __set__ method was not present, this descriptor would become a
+        *non-data descriptor*. That would be nice because the cached value would be
+        accessed directly once set (__dict__ attrs have precedence over non-data
+        descriptors on instance attribute lookup). The problem is, there would be
+        nothing to stop assignment to the cached value, which would overwrite the result
+        of `fget()` and break both the immutability and idempotence guarantees of this
+        decorator.
+
+        The performance with this __set__() method in place was roughly 0.4 usec per
+        access when measured on a 2.8GHz development machine; so quite snappy and
+        probably not a rich target for optimization efforts.
+        """
+        raise AttributeError("can't set attribute")