about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/pptx/oxml
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/pptx/oxml
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx/oxml')
-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
31 files changed, 8223 insertions, 0 deletions
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)