OverviewFilesCommitsIssuesDocsReleasesPeers

electrical-review / artifacts / racking / exploded / exploded_opt3_final.py

1000 lines · python ref:
#!/usr/bin/env python3
"""
FIG. R8 — EXPLODED ASSEMBLY · OPTION 3 — SINGLE-PIVOT TILTING TABLE (owner-favored)
PRSM Phase-1 ground-mount · INHERITED racking adaptation
(SOLAR_RACKING_HANDOFF §5; Vandewetering/Hayibo/Pearce, Designs 2022 6(3):41;
 tilting-pivot-mount.md + tilting-bom-reconciliation.md consensus BOM)

VISUALIZATION ONLY of an inherited engineering ESTIMATE — not a stamped calc.
ENERGY figures are script-computed (15_tilt_sweep.py) and used verbatim.
The only electrical items are the cross-pivot BOND jumper (>=10 AWG, cited) and
the PV SERVICE LOOPS — no new electrical numbers invented.

Adapted from the Fixed-55 "tiered-cards" exploded template (exploded_final.py).
Same iso() projection, box/cyl helpers, shading, palette, layout — only the
CONTENT changes for the SINGLE-PIVOT TILT option:

STAGE PLAN
  1 FOOTINGS    — ~6-8 holes (~HALF the base) on dedicated post-pairs
  2 PIVOT POSTS — ~6-8 dedicated >=6x6 pairs (NO 4x4 fronts)
  3 PIVOT MECH  — through-bolt + steel bushings + positive wind-lock + bond
                  braid + PV service loop  (the HEADLINE stage, exploded)
  4 RAILS/JOISTS— 2x10 rotating frame (largely no continuous 2x8 beam line)
  5 BLOCKS+PANELS — attach blocks + 14x Waaree on the rotating plane, tilt arc,
                    "~390 ft2 PLANE = A SAIL · POSITIVE LOCK MANDATORY" note
"""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon, FancyBboxPatch, Circle, Ellipse, FancyArrow, Arc
from matplotlib.lines import Line2D
from matplotlib import font_manager
import numpy as np

# ----------------------------------------------------------------------------
# FONTS — mono field-guide feel.
# ----------------------------------------------------------------------------
MONO = "DejaVu Sans Mono"
for cand in ("IBM Plex Mono",):
    try:
        font_manager.findfont(cand, fallback_to_default=False)
        MONO = cand
        break
    except Exception:
        pass
plt.rcParams["font.family"] = "monospace"
plt.rcParams["font.monospace"] = [MONO, "DejaVu Sans Mono"]
plt.rcParams["svg.fonttype"] = "none"

# ----------------------------------------------------------------------------
# PALETTE (field-guide; same as template)
# ----------------------------------------------------------------------------
PAPER     = "#F4F0E7"
SURFACE   = "#FCFAF4"
SURFACE2  = "#F9F5EE"
INK       = "#271F18"
INK2      = "#50473F"
INK3      = "#797269"
RULE      = "#D3CFC6"
RULE2     = "#B6B1A9"
AMBER     = "#AE692D"
AMBER_SOFT= "#EBDECF"
PINE      = "#316843"
PINE_SOFT = "#DDE0D3"
PINE_STK  = "#091E10"
PINE_LITE = "#7FA587"
SLATE     = "#456783"
SLATE_SOFT= "#D7E0E6"
CLAY      = "#B0512F"   # safety / positive-lock accent (per racking.css --clay role)

def hx(c):
    c = c.lstrip("#")
    return tuple(int(c[i:i+2], 16) / 255 for i in (0, 2, 4))

def darken(c, k):
    r, g, b = hx(c)
    return (r * k, g * k, b * k)

# ----------------------------------------------------------------------------
# ISOMETRIC PROJECTION  (true 2:1 iso)
# ----------------------------------------------------------------------------
COS30 = 0.86602540378
SIN30 = 0.5

def iso(X, Y, Z, S=1.0, ox=0.0, oy=0.0):
    return (ox + S * (X - Y) * COS30,
            oy + S * (-(X + Y) * SIN30 + Z))

K_TOP, K_LEFT, K_RIGHT = 1.00, 0.82, 0.66

def box(ax, X, Y, Z, dx, dy, dz, fill, stroke, lw=1.5, S=1.0, ox=0.0, oy=0.0,
        z=2, alpha=1.0, top_k=None, left_k=None, right_k=None):
    kt = K_TOP if top_k is None else top_k
    kl = K_LEFT if left_k is None else left_k
    kr = K_RIGHT if right_k is None else right_k
    def P(x, y, zz):
        return iso(x, y, zz, S, ox, oy)
    c = [P(X, Y, Z), P(X+dx, Y, Z), P(X+dx, Y+dy, Z), P(X, Y+dy, Z),
         P(X, Y, Z+dz), P(X+dx, Y, Z+dz), P(X+dx, Y+dy, Z+dz), P(X, Y+dy, Z+dz)]
    base = hx(fill)
    right = [c[1], c[2], c[6], c[5]]
    ax.add_patch(Polygon(right, closed=True, facecolor=tuple(v*kr for v in base),
                         edgecolor=stroke, linewidth=lw, zorder=z, joinstyle="miter",
                         alpha=alpha))
    left = [c[0], c[1], c[5], c[4]]
    ax.add_patch(Polygon(left, closed=True, facecolor=tuple(v*kl for v in base),
                         edgecolor=stroke, linewidth=lw, zorder=z+0.1, joinstyle="miter",
                         alpha=alpha))
    top = [c[4], c[5], c[6], c[7]]
    ax.add_patch(Polygon(top, closed=True, facecolor=tuple(v*kt for v in base),
                         edgecolor=stroke, linewidth=lw, zorder=z+0.2, joinstyle="miter",
                         alpha=alpha))
    return c

def cyl(ax, X, Y, Z, dia, depth, fill, stroke, lw=1.4, S=1.0, ox=0.0, oy=0.0, z=2,
        hatch=None, side_k=K_LEFT):
    r = dia / 2.0
    cxt, cyt = iso(X, Y, Z, S, ox, oy)
    cxb, cyb = iso(X, Y, Z - depth, S, ox, oy)
    ew = dia * S * 1.0
    eh = dia * S * SIN30
    base = hx(fill)
    side = [(cxt - ew/2, cyt), (cxt + ew/2, cyt),
            (cxb + ew/2, cyb), (cxb - ew/2, cyb)]
    ax.add_patch(Polygon(side, closed=True, facecolor=tuple(v*side_k for v in base),
                         edgecolor=stroke, linewidth=lw, zorder=z, hatch=hatch))
    ax.add_patch(Ellipse((cxb, cyb), ew, eh, facecolor=tuple(v*K_RIGHT for v in base),
                         edgecolor=stroke, linewidth=lw*0.8, zorder=z+0.05))
    ax.add_patch(Ellipse((cxt, cyt), ew, eh, facecolor=tuple(v*K_TOP for v in base),
                         edgecolor=stroke, linewidth=lw, zorder=z+0.3))
    rng = np.random.default_rng(int(abs(X*97 + Y*131)))
    for _ in range(7):
        fx = cxt + (rng.random()-0.5)*ew*0.7
        fy = cyt + (rng.random()-0.5)*eh*0.7
        ax.add_patch(Circle((fx, fy), ew*0.018, facecolor=INK3, alpha=0.4,
                            edgecolor="none", zorder=z+0.4))
    return (cxt, cyt)

# ----------------------------------------------------------------------------
# DRESSED-LUMBER CROSS SECTIONS (meters)
# ----------------------------------------------------------------------------
IN = 0.0254
S66 = 5.5 * IN        # 6x6
S44 = 3.5 * IN        # 4x4 (NOT used as a structural front here — reference only)
T2  = 1.5 * IN        # nominal 2x thickness
D10 = 9.25 * IN       # 2x10
D8  = 7.25 * IN       # 2x8
D6  = 5.5 * IN        # 2x6
D4  = 3.5 * IN        # 2x4

BAY   = 16.1 / 7.0    # 2.30 m
DEPTH = 1.30          # front->back run of the rotating frame footprint
BACK_TOP  = 2.23
FRONT_TOP = 0.50
# Single-pivot option: pivot-post PAIRS, one pair per sub-section.
# Representative 2-of-7-bay section -> show 2 post-pairs framing one tilt bay
# plus the start of the next -> 3 post rows (X positions) along width.
N_PAIRS = 3
SPAN_X  = 2 * BAY     # 4.60 m
# Pivot axis height (the plane bears on the axle, mid-height of the >=6x6 posts)
PIVOT_Z = 1.35

# ----------------------------------------------------------------------------
# CALL-OUT BADGE + LEADER helper
# ----------------------------------------------------------------------------
def callout(ax, anchor_xy, badge_xy, num, z=30, color=INK, ring=SURFACE):
    ax_pt, ay = anchor_xy
    bx, by = badge_xy
    ax.add_line(Line2D([ax_pt, bx], [ay, by], color=INK3, lw=1.0,
                       linestyle=(0, (3, 3)), zorder=z))
    ax.add_patch(Circle((ax_pt, ay), 2.6, facecolor=INK, edgecolor="none", zorder=z+0.5))
    ax.add_patch(Circle((bx, by), 10.5, facecolor=ring, edgecolor=color,
                        linewidth=1.5, zorder=z+1))
    ax.text(bx, by, num, family="monospace",
            fontsize=8.5 if len(num) == 1 else 7.2,
            color=color, ha="center", va="center", zorder=z+2, weight="bold")

# ============================================================================
# FIGURE
# ============================================================================
fig = plt.figure(figsize=(13, 16.4), dpi=200)
fig.patch.set_facecolor(PAPER)
ax = fig.add_axes([0, 0, 1, 1])
ax.set_facecolor(PAPER)
ax.set_xlim(0, 1300)
ax.set_ylim(0, 1640)
ax.set_aspect("equal")
ax.axis("off")

YSHIFT = 40
LEFT_W = 1010
RIGHT_X = 1028

# ---- TOP STRIP: fig label + title --------------------------------------------
ax.text(34, 1560 + YSHIFT, "FIG. R8 · OPTION 3 — SINGLE-PIVOT TILT · EXPLODED ASSEMBLY",
        family="monospace", fontsize=11.5, color=INK3, weight="bold")
ax.add_line(Line2D([34, 990], [1548 + YSHIFT, 1548 + YSHIFT], color=RULE2, lw=1.0))
ax.text(34, 1524 + YSHIFT, "OPTION 3 — SINGLE-PIVOT TILTING TABLE  (OWNER-FAVORED)",
        family="monospace", fontsize=15.5, color=INK, weight="bold")
ax.text(34, 1500 + YSHIFT,
        "REPRESENTATIVE 2 OF 7 BAYS · 14 × WAAREE 575 W · ROTATING PLANE · DEDICATED PIVOT-POST PAIRS",
        family="monospace", fontsize=9.8, color=INK2)

# energy strip (script-computed, verbatim from 15_tilt_sweep.py)
ax.text(34, 1481 + YSHIFT,
        "ENERGY (15_tilt_sweep.py): 11,023 kWh/yr · +577 (+5.5%) gross vs fixed 55° · "
        "monthly ≈ 4-position (4→monthly adds ≈ 0)",
        family="monospace", fontsize=8.6, color=PINE, weight="bold")

# ---- STAMP (top-right, estimate/amber style) ---------------------------------
stamp_w, stamp_h = 268, 56
sx0, sy0 = LEFT_W - stamp_w, 1500 + YSHIFT
stamp = FancyBboxPatch((sx0, sy0), stamp_w, stamp_h,
                       boxstyle="round,pad=2,rounding_size=6",
                       facecolor=AMBER_SOFT, edgecolor=AMBER, linewidth=1.4,
                       zorder=20)
ax.add_patch(stamp)
ax.add_patch(Circle((sx0 + 16, sy0 + stamp_h/2), 4.5, facecolor=AMBER,
                    edgecolor="none", zorder=21))
ax.text(sx0 + 30, sy0 + stamp_h/2 + 14, "INHERITED · NOT STAMPED",
        family="monospace", fontsize=8.8, color=AMBER, weight="bold",
        va="center", zorder=21)
ax.text(sx0 + 30, sy0 + stamp_h/2, "ENGINEER REVIEW BEFORE POUR",
        family="monospace", fontsize=8.8, color=AMBER, weight="bold",
        va="center", zorder=21)
ax.text(sx0 + 30, sy0 + stamp_h/2 - 14, "PIVOT/LOCK/BUSHING = ENGINEER-SET (A7)",
        family="monospace", fontsize=7.6, color=AMBER, weight="bold",
        va="center", zorder=21)

# ============================================================================
# STAGE VIGNETTES  (instruction-sheet tiers)
# ============================================================================
S = 48.0

TIER_X0 = 34
TIER_X1 = LEFT_W - 6
TIER_TOP = 1456 + YSHIFT
TIER_BOT = 70 + YSHIFT
N_TIERS = 5
GAP = 18
tier_h = (TIER_TOP - TIER_BOT - GAP * (N_TIERS - 1)) / N_TIERS

def tier_rect(i):
    y1 = TIER_TOP - i * (tier_h + GAP)
    y0 = y1 - tier_h
    return (TIER_X0, y0, TIER_X1 - TIER_X0, tier_h)

def draw_card(i, num, name, sub):
    x0, y0, w, h = tier_rect(i)
    card = FancyBboxPatch((x0, y0), w, h,
                          boxstyle="round,pad=0,rounding_size=8",
                          facecolor=SURFACE, edgecolor=RULE2, linewidth=1.2,
                          zorder=1)
    ax.add_patch(card)
    bx, by = x0 + 34, y0 + h - 32
    ax.add_patch(Circle((bx, by), 19, facecolor=SURFACE2, edgecolor=INK2,
                        linewidth=1.6, zorder=5))
    ax.text(bx, by, str(num), family="monospace", fontsize=17, color=INK,
            weight="bold", ha="center", va="center", zorder=6)
    ax.text(x0 + 64, y0 + h - 22, name, family="monospace", fontsize=12.5,
            color=INK, weight="bold", va="center")
    ax.text(x0 + 64, y0 + h - 40, sub, family="monospace", fontsize=9.0,
            color=INK2, va="center")
    return (x0, y0, w, h)

def stage_label_chip(x0, y0, h, callouts):
    cx = x0 + 64
    cyy = y0 + h - 66
    for c in callouts:
        ax.add_patch(Circle((cx, cyy), 10.5, facecolor=SURFACE, edgecolor=INK2,
                            linewidth=1.3, zorder=6))
        ax.text(cx, cyy, c, family="monospace", fontsize=8.5, color=INK,
                ha="center", va="center", zorder=7)
        cx += 28

def down_arrow(i):
    x0, y0, w, h = tier_rect(i)
    ax_pt = x0 + 150
    ya = y0 - 3
    yb = y0 - GAP + 3
    ax.add_patch(FancyArrow(ax_pt, ya, 0, (yb - ya), width=2.4,
                            head_width=12, head_length=8, length_includes_head=True,
                            facecolor=INK3, edgecolor="none", zorder=8, alpha=0.9))

# ----------------------------------------------------------------------------
# VIGNETTE BUILDERS
# ----------------------------------------------------------------------------
def ground_plane(ox, oy, x_lo=-0.55, x_hi=None, alpha=0.5):
    if x_hi is None:
        x_hi = SPAN_X + 0.55
    pts = [iso(x_lo, 0.0, 0.0, S, ox, oy),
           iso(x_hi, 0.0, 0.0, S, ox, oy),
           iso(x_hi, DEPTH, 0.0, S, ox, oy),
           iso(x_lo, DEPTH, 0.0, S, ox, oy)]
    ax.add_patch(Polygon(pts, closed=True, facecolor=SURFACE2, edgecolor=RULE2,
                         linewidth=1.0, zorder=1.5, alpha=alpha))
    ax.add_line(Line2D([pts[0][0], pts[1][0]], [pts[0][1], pts[1][1]],
                       color=INK, lw=1.6, zorder=1.7))
    return pts

def vignette_origin(x0, y0, w, h):
    # taller pivot geometry -> shift left + sit lower so the structure rises
    # into the card and the wide 2-bay array stays inside the right edge
    ox = x0 + w * 0.42
    oy = y0 + h * 0.30
    return ox, oy

def centered_origin(rect, zlo, zhi, x_lo=-0.55, x_hi=None,
                    header=46, margin=10, fx=0.50, scale=None):
    """Center a stage's *projected* iso bounding box inside the card's drawing
    area (below the header). iso() is linear → screen bbox = min/max over the
    world-AABB corners; center THAT so each stage sits centered regardless of how
    its geometry extends. `scale` lets a stage with a local scale (footings SF)
    center correctly."""
    sc = S if scale is None else scale
    x0, y0, w, h = rect
    if x_hi is None:
        x_hi = SPAN_X + 0.55
    corners = [(X, Y, Z) for X in (x_lo, x_hi) for Y in (0.0, DEPTH)
               for Z in (zlo, zhi)]
    sxs = [sc * (X - Y) * COS30 for (X, Y, Z) in corners]
    sys = [sc * (-(X + Y) * SIN30 + Z) for (X, Y, Z) in corners]
    cx0 = (min(sxs) + max(sxs)) / 2.0
    cy0 = (min(sys) + max(sys)) / 2.0
    tcx = x0 + w * fx
    tcy = y0 + margin + (h - header - margin) / 2.0
    return tcx - cx0, tcy - cy0

# per-stage vertical world extent (Z) for the iso stages that use the loop origin
STAGE_Z3 = {
    "draw_pivot_posts": (0.0, PIVOT_Z + 0.30),
    "draw_rails":       (0.0, PIVOT_Z + 0.70),
    "draw_panels":      (0.0, PIVOT_Z + 0.85),
}

# Pivot-post pair Y positions (front and back of each pair, straddling the axis)
PAIR_DY = 0.34          # spacing front<->back within a post pair
PY_FRONT = DEPTH/2 - PAIR_DY/2
PY_BACK  = DEPTH/2 + PAIR_DY/2

def draw_footings(ox, oy):
    """STAGE 1 — ~6-8 holes, ~HALF the base. Post-PAIR footings (two close
    holes per pivot-post pair) instead of separate back+front rows."""
    # Footings extend ~1.4 m BELOW grade. The 2-bay-wide array + the downward
    # piers are TALLER than the card at the full scale, so this stage uses a
    # local reduced scale + raised origin to keep all buried geometry + the
    # BURIED depth dimension INSIDE the card (no spill into stage 2).
    x0c, y0c, wc, hc = tier_rect(0)
    SF = 44.0
    ox, oy = centered_origin((x0c, y0c, wc, hc), -1.10, 0.30, scale=SF, fx=0.50)
    DEP = 1.10                      # drawn pier depth (BURIED dim says 4–5 ft)
    for p in range(N_PAIRS):
        X = p * BAY
        for Y in (PY_BACK, PY_FRONT):
            cyl(ax, X, Y, 0.0, 0.32, DEP, "#9C8A6E", INK2, lw=1.3, S=SF,
                ox=ox, oy=oy, z=3, hatch="////", side_k=0.62)
    # local-scale grade plane (ground_plane() uses the global S, so build here)
    pts = [iso(-0.55, 0.0, 0.0, SF, ox, oy), iso(SPAN_X+0.55, 0.0, 0.0, SF, ox, oy),
           iso(SPAN_X+0.55, DEPTH, 0.0, SF, ox, oy), iso(-0.55, DEPTH, 0.0, SF, ox, oy)]
    ax.add_patch(Polygon(pts, closed=True, facecolor=SURFACE2, edgecolor=RULE2,
                         linewidth=1.0, zorder=4.5, alpha=0.5))
    ax.add_line(Line2D([pts[0][0], pts[1][0]], [pts[0][1], pts[1][1]],
                       color=INK, lw=1.6, zorder=4.7))
    gx0, gy0 = pts[0]; gx1, gy1 = pts[1]
    band_h = 14
    soil = [(gx0, gy0), (gx1, gy1), (gx1, gy1 - band_h), (gx0, gy0 - band_h)]
    ax.add_patch(Polygon(soil, closed=True, facecolor="#E4D8C2", edgecolor=INK2,
                         linewidth=0.8, hatch="....", zorder=4.6, alpha=0.9))
    for p in range(N_PAIRS):
        X = p * BAY
        for Y in (PY_BACK, PY_FRONT):
            cyl(ax, X, Y, 0.10, 0.32, 0.10, SURFACE, INK2, lw=1.3, S=SF,
                ox=ox, oy=oy, z=5)
    # frost line
    fx0, fy0 = iso(-0.55, 0.0, -0.95, SF, ox, oy)
    fx1, fy1 = iso(SPAN_X + 0.55, 0.0, -0.95, SF, ox, oy)
    ax.add_line(Line2D([fx0, fx1], [fy0, fy1], color=SLATE, lw=1.4,
                       linestyle=(0, (6, 4)), zorder=4))
    ax.text(fx1 + 6, fy1, "frost ~1.2 m", family="monospace", fontsize=8,
            color=SLATE, va="center", zorder=6)
    # depth dimension on near-right pier
    Xd = (N_PAIRS - 1) * BAY
    dim_x = iso(Xd, PY_FRONT, 0.0, SF, ox, oy)[0] + 0.36 * SF * COS30 + 14
    yg = iso(Xd, PY_FRONT, 0.0, SF, ox, oy)[1]
    yb = iso(Xd, PY_FRONT, -DEP, SF, ox, oy)[1]
    ax.add_line(Line2D([dim_x, dim_x], [yg, yb], color=INK2, lw=1.2, zorder=6))
    for yy, dy in ((yg, -7), (yb, 7)):
        ax.add_patch(Polygon([(dim_x, yy), (dim_x - 3.2, yy + dy),
                              (dim_x + 3.2, yy + dy)], closed=True,
                             facecolor=INK2, edgecolor="none", zorder=6))
    ax.add_line(Line2D([iso(Xd,PY_FRONT,0,SF,ox,oy)[0], dim_x], [yg, yg],
                       color=INK2, lw=0.7, linestyle=(0,(2,2)), zorder=6))
    ax.add_line(Line2D([iso(Xd,PY_FRONT,-DEP,SF,ox,oy)[0], dim_x], [yb, yb],
                       color=INK2, lw=0.7, linestyle=(0,(2,2)), zorder=6))
    ax.text(dim_x + 7, (yg + yb)/2, "BURIED\n4–5 ft", family="monospace",
            fontsize=8, color=INK2, va="center", zorder=6, weight="bold")
    # spec + KEY NOTE: stacked in the OPEN left column, clear of the sub-title
    # band above and the drawing to the right (fixes the label/title collision).
    x0c, y0c, wc, hc = tier_rect(0)
    ax.text(x0c + 60, y0c + hc*0.64, "30 MPa pier · Ø320 mm",
            family="monospace", fontsize=8, color=INK2, ha="left",
            va="center", zorder=8)
    ax.text(x0c + 60, y0c + hc*0.46,
            "≈ HALF THE BASE — POST PAIRS\n~6–8 holes (vs base 16)",
            family="monospace", fontsize=8.4, color=AMBER, weight="bold",
            ha="left", va="center", zorder=8)
    ah = iso((N_PAIRS-1)*BAY, PY_BACK, 0.18, SF, ox, oy)
    callout(ax, ah, (ah[0] - 70, ah[1] + 34), "F")

def draw_pivot_posts(ox, oy):
    """STAGE 2 — dedicated >=6x6 pivot-post PAIRS. NO 4x4 fronts.
    Both members of each pair are 6x6, taller (to PIVOT_Z + headroom) so the
    plane can rotate on the axle."""
    ground_plane(ox, oy, alpha=0.5)
    for p in range(N_PAIRS):
        X = p * BAY
        for Y in (PY_FRONT, PY_BACK):
            cx, cy = iso(X, Y, 0.0, S, ox, oy)
            ax.add_patch(Ellipse((cx, cy), 0.32*S, 0.32*S*SIN30,
                                 facecolor=SURFACE2, edgecolor=RULE2,
                                 linewidth=1.0, zorder=2))
    post_top = PIVOT_Z + 0.20
    for p in range(N_PAIRS):
        X = p * BAY
        for Y in (PY_BACK, PY_FRONT):
            zb = 4 + (0 if Y == PY_BACK else 4)   # near pair member draws over far
            box(ax, X - S66/2, Y - S66/2, 0.0, S66, S66, post_top,
                AMBER_SOFT, AMBER, lw=2.0, S=S, ox=ox, oy=oy, z=zb)
            # pivot-hole mark near the top of each post (where the axle passes)
            hx_, hy_ = iso(X, Y, PIVOT_Z, S, ox, oy)
            ax.add_patch(Circle((hx_, hy_), 3.0, facecolor=SURFACE,
                                edgecolor=INK, linewidth=1.3, zorder=zb+1))
    # note: ALL >=6x6, no 4x4 — placed in the OPEN upper-left of the card.
    x0c, y0c, wc, hc = tier_rect(1)
    ax.text(x0c + 60, y0c + hc*0.62,
            "ALL ≥6×6 PIVOT-POST PAIRS\nNO 4×4 FRONTS",
            family="monospace", fontsize=8.4, color=AMBER, weight="bold",
            ha="left", va="center", zorder=10)
    # pivot-axis reference line through the post-top holes
    a0 = iso(-0.4, DEPTH/2, PIVOT_Z, S, ox, oy)
    a1 = iso(SPAN_X + 0.4, DEPTH/2, PIVOT_Z, S, ox, oy)
    ax.add_line(Line2D([a0[0], a1[0]], [a0[1], a1[1]], color=SLATE, lw=1.2,
                       linestyle=(0,(5,3)), zorder=9))
    ax.text(a1[0] + 6, a1[1], "pivot axis", family="monospace", fontsize=7.6,
            color=SLATE, va="center", zorder=9)
    callout(ax, iso((N_PAIRS-1)*BAY, PY_FRONT, PIVOT_Z*0.6, S, ox, oy),
            (iso((N_PAIRS-1)*BAY, PY_FRONT, PIVOT_Z*0.6, S, ox, oy)[0]+62,
             iso((N_PAIRS-1)*BAY, PY_FRONT, PIVOT_Z*0.6, S, ox, oy)[1]-22), "1")

def draw_pivot_mech(ox, oy):
    """STAGE 3 — THE HEADLINE. One pivot crossing shown IN CONTEXT on the left,
    with a large EXPLODED axle detail spread horizontally across the right so
    each engineered-steel part is separated + clearly labeled:
    through-bolt + steel bushings (sleeve BOTH post & frame holes) + positive
    wind-lock pin/strut (frame->post) + flexible braided-Cu bond jumper + UV
    PV service loop."""
    x0c, y0c, wc, hc = tier_rect(2)

    # ---- LEFT: one post pair IN CONTEXT, with the crossing assembled ----
    # local origin for this context vignette (left third of the card); sit it
    # LOWER so the structure + labels clear the header + chip row.
    cox = x0c + wc*0.34
    coy = y0c + hc*0.22
    Xc = 0.0
    post_top = PIVOT_Z + 0.20
    ground_plane(cox, coy, x_lo=Xc-0.7, x_hi=Xc+0.7, alpha=0.40)
    for Y in (PY_BACK, PY_FRONT):
        zb = 4 + (0 if Y == PY_BACK else 4)
        box(ax, Xc - S66/2, Y - S66/2, 0.0, S66, S66, post_top,
            AMBER_SOFT, AMBER, lw=1.6, S=S, ox=cox, oy=coy, z=zb)

    pcx, pcy = iso(Xc, PY_FRONT, PIVOT_Z, S, cox, coy)   # front-post pivot point
    pbx, pby = iso(Xc, PY_BACK,  PIVOT_Z, S, cox, coy)   # back-post pivot point

    # --- rotating 2x10 frame rail stub bearing on the axle (tilted up-back) ---
    ang = np.deg2rad(48)
    rail_len = 0.85
    r_lo = iso(Xc, PY_FRONT - 0.10, PIVOT_Z - rail_len*0.30*np.sin(ang), S, cox, coy)
    r_hi = iso(Xc, PY_FRONT - 0.10, PIVOT_Z + rail_len*np.sin(ang), S, cox, coy)
    ax.add_line(Line2D([r_lo[0], r_hi[0]], [r_lo[1], r_hi[1]], color=PINE_STK,
                       lw=11, zorder=24, solid_capstyle="butt"))
    ax.add_line(Line2D([r_lo[0], r_hi[0]], [r_lo[1], r_hi[1]], color=AMBER,
                       lw=7, zorder=24.1, solid_capstyle="butt"))
    ax.text(r_hi[0] + 14, r_hi[1] + 2, "rotating\n2×10 rail",
            family="monospace", fontsize=6.8, color=AMBER, va="center",
            ha="left", zorder=28, weight="bold")

    # --- POSITIVE WIND-LOCK pin/strut: frame -> back post (clay, mandatory) ---
    lock_top = iso(Xc, PY_FRONT - 0.10, PIVOT_Z + rail_len*0.62*np.sin(ang), S, cox, coy)
    lock_bot = iso(Xc, PY_BACK, 0.62, S, cox, coy)
    ax.add_line(Line2D([lock_top[0], lock_bot[0]], [lock_top[1], lock_bot[1]],
                       color=CLAY, lw=3.6, zorder=25, solid_capstyle="round"))
    for fr in (0.0, 0.5, 1.0):
        ph = iso(Xc, PY_BACK, 0.55 + fr*0.55, S, cox, coy)
        ax.add_patch(Circle((ph[0], ph[1]), 2.4, facecolor=SURFACE,
                            edgecolor=CLAY, linewidth=1.3, zorder=25.5))

    # --- flexible braided-Cu BOND jumper across the pivot (pine) ---
    bj_x = np.linspace(pcx - 8, pbx + 8, 40)
    bj_y = np.linspace(pcy, pby, 40) + 8*np.sin(np.linspace(0, np.pi, 40))
    ax.add_line(Line2D(bj_x, bj_y, color=PINE, lw=2.4, zorder=26))
    for lx0, ly0 in ((bj_x[0], bj_y[0]), (bj_x[-1], bj_y[-1])):
        ax.add_patch(Circle((lx0, ly0), 3.0, facecolor=PINE, edgecolor=PINE_STK,
                            linewidth=1.0, zorder=26.1))

    # --- UV PV SERVICE LOOP at the crossing (slate coil), below the axle ---
    loop_cx, loop_cy = pcx - 6, pcy - 30
    th = np.linspace(0, 2.4*np.pi, 80)
    rr = 2 + th*1.2
    slx = loop_cx + rr*np.cos(th)
    sly = loop_cy + rr*np.sin(th)*SIN30
    ax.add_line(Line2D(slx, sly, color=SLATE, lw=2.0, zorder=23.5))
    # pivot point marker
    ax.add_patch(Circle((pcx, pcy), 4.0, facecolor=SLATE_SOFT, edgecolor=SLATE,
                        linewidth=2.0, zorder=27))

    # context callouts (kept short, leadered down-left so they don't cross the
    # exploded detail on the right)
    callout(ax, ((lock_top[0]+lock_bot[0])/2, (lock_top[1]+lock_bot[1])/2),
            (lock_top[0] + 24, lock_top[1] - 18), "3c", color=CLAY)
    ax.text(lock_top[0] + 38, lock_top[1] - 18, "positive wind-lock\n(NOT friction/winch)",
            family="monospace", fontsize=6.8, color=CLAY, va="center",
            ha="left", zorder=31, weight="bold")
    callout(ax, (bj_x[20], bj_y[20]), (bj_x[20] - 70, bj_y[20] + 26), "B", color=PINE)
    ax.text(bj_x[20] - 84, bj_y[20] + 26, "≥10 AWG\nbraided-Cu bond",
            family="monospace", fontsize=6.8, color=PINE, va="center",
            ha="right", zorder=31)
    callout(ax, (slx[40], sly[40]), (loop_cx - 64, loop_cy - 6), "L", color=SLATE)
    ax.text(loop_cx - 78, loop_cy - 6, "PV service\nloop", family="monospace",
            fontsize=6.8, color=SLATE, va="center", ha="right", zorder=31)

    # ---- RIGHT: large EXPLODED axle detail, parts spread horizontally ----
    ex_y = y0c + hc*0.46
    ex_x0 = x0c + wc*0.50
    ex_dx = (wc*0.46) / 4.0       # spacing between the 5 exploded stations
    stations = [ex_x0 + k*ex_dx for k in range(5)]

    # the exploded assembly axis (dashed) tying the stations together
    ax.add_line(Line2D([stations[0]-18, stations[-1]+34], [ex_y, ex_y],
                       color=INK3, lw=1.0, linestyle=(0,(4,3)), zorder=21))

    def part_circle(cx, r, fc, ec, lw=1.8, z=30):
        ax.add_patch(Circle((cx, ex_y), r, facecolor=fc, edgecolor=ec,
                            linewidth=lw, zorder=z))

    # station 0: POST hole (in wood)
    part_circle(stations[0], 13, SURFACE2, INK2, lw=1.6, z=30)
    ax.text(stations[0], ex_y - 26, "POST\nHOLE", family="monospace", fontsize=6.6,
            color=INK2, ha="center", va="center", zorder=32, weight="bold")
    # station 1: steel bushing (post side)
    part_circle(stations[1], 12, SLATE_SOFT, SLATE, lw=2.6, z=30)
    part_circle(stations[1], 6.5, SURFACE, SLATE, lw=1.4, z=30.1)
    ax.text(stations[1], ex_y - 26, "STEEL\nBUSHING", family="monospace", fontsize=6.6,
            color=SLATE, ha="center", va="center", zorder=32, weight="bold")
    # station 2: frame hole + its bushing
    part_circle(stations[2], 13, AMBER_SOFT, AMBER, lw=1.8, z=30)
    part_circle(stations[2], 12, SLATE_SOFT, SLATE, lw=2.6, z=30.1)
    part_circle(stations[2], 6.5, SURFACE, SLATE, lw=1.4, z=30.2)
    ax.text(stations[2], ex_y - 26, "FRAME +\nBUSHING", family="monospace", fontsize=6.6,
            color=SLATE, ha="center", va="center", zorder=32, weight="bold")
    # station 3: washer
    ax.add_patch(Ellipse((stations[3], ex_y), 9, 22, facecolor=SLATE_SOFT,
                         edgecolor=SLATE, linewidth=1.6, zorder=30))
    ax.text(stations[3], ex_y - 26, "WASHER", family="monospace", fontsize=6.6,
            color=SLATE, ha="center", va="center", zorder=32, weight="bold")
    # station 4: hex nut/bolt head
    hexpts = [(stations[4] + 11*np.cos(a), ex_y + 11*np.sin(a))
              for a in np.linspace(0, 2*np.pi, 7)]
    ax.add_patch(Polygon(hexpts, closed=True, facecolor=INK, edgecolor=INK,
                         linewidth=1.0, zorder=31))
    ax.text(stations[4], ex_y - 26, "HEX\nNUT", family="monospace", fontsize=6.6,
            color=INK, ha="center", va="center", zorder=32, weight="bold")

    # the THROUGH-BOLT rod running through all stations
    ax.add_line(Line2D([stations[0]-20, stations[4]], [ex_y, ex_y], color=INK,
                       lw=6.0, zorder=33, solid_capstyle="round"))
    ax.add_line(Line2D([stations[0]-20, stations[4]], [ex_y, ex_y], color=SLATE,
                       lw=2.4, zorder=33.1))

    # exploded callouts (leadered UP, into clear paper above the detail row)
    callout(ax, (stations[0], ex_y), (stations[0], ex_y + 58), "3a", color=SLATE)
    ax.text(stations[0], ex_y + 70, "through-bolt pivot", family="monospace",
            fontsize=7.0, color=INK2, ha="center", va="bottom", zorder=31)
    callout(ax, (stations[1], ex_y), (stations[2], ex_y + 60), "3b", color=SLATE)
    ax.text(stations[2]+18, ex_y + 60, "steel bushings sleeve BOTH\nholes (anti-wallow)",
            family="monospace", fontsize=7.0, color=INK2, ha="left",
            va="center", zorder=31)

    # headline label — top-right corner of the card, clear of the detail
    ax.text(x0c + wc - 16, y0c + hc - 14,
            "ENGINEERED-STEEL PIVOT MECHANISM\nanti-wallow · positive lock · bonded",
            family="monospace", fontsize=8.0, color=INK, weight="bold",
            ha="right", va="top", zorder=12)

# ----------------------------------------------------------------------------
# SHARED TILTED-PLANE GEOMETRY (stages 4 & 5)
#   The rotating frame pivots about the axle at PIVOT_Z. Drawn as a CONTAINED
#   tilted table: a representative plane length + a MODERATED draw angle so the
#   plane reads in iso instead of collapsing to an edge-on sliver. The true
#   55°/4-position tilt is conveyed by the tilt-arc + labels in stage 5.
#   t in [0,1]: 0 = front-LOW edge, 1 = back-HIGH edge.  off lifts along normal.
# ----------------------------------------------------------------------------
PLANE_L  = 1.05                  # representative plane length (drawn, m)
DRAW_ANG = np.deg2rad(24)        # opened draw angle so plane reads, not edge-on

def plane_pt(X, t, ox, oy, lift=0.0):
    s = (t - 0.5) * PLANE_L
    Yc, Zc = DEPTH/2, PIVOT_Z
    Y = Yc + s*np.cos(DRAW_ANG)
    Z = Zc + s*np.sin(DRAW_ANG) + lift
    return iso(X, Y, Z, S, ox, oy)

def context_posts(ox, oy, lw=1.4, z0=3):
    """Pivot-post pairs as built context (amber), with the pivot axis line."""
    post_top = PIVOT_Z + 0.20
    for p in range(N_PAIRS):
        X = p * BAY
        # draw far member first, then near, so overlap reads
        for Y in (PY_BACK, PY_FRONT):
            zb = z0 + (0 if Y == PY_BACK else 0.6)
            box(ax, X - S66/2, Y - S66/2, 0.0, S66, S66, post_top,
                AMBER_SOFT, AMBER, lw=lw, S=S, ox=ox, oy=oy, z=zb)
            # pivot-hole mark at the axle
            hx_, hy_ = iso(X, Y, PIVOT_Z, S, ox, oy)
            ax.add_patch(Circle((hx_, hy_), 2.6, facecolor=SURFACE,
                                edgecolor=INK2, linewidth=1.0, zorder=zb+1))
    a0 = iso(-0.45, DEPTH/2, PIVOT_Z, S, ox, oy)
    a1 = iso(SPAN_X + 0.45, DEPTH/2, PIVOT_Z, S, ox, oy)
    ax.add_line(Line2D([a0[0], a1[0]], [a0[1], a1[1]], color=SLATE, lw=1.1,
                       linestyle=(0,(5,3)), zorder=2.5))

def draw_rails(ox, oy):
    """STAGE 4 — the ROTATING 2x10 frame (rails/joists). The continuous 2x8
    beam line is largely ELIMINATED (plane bears on the axle); show only ~short
    bearing headers at the pivot posts. Rails drawn TILTED about the pivot."""
    ground_plane(ox, oy, alpha=0.5)
    context_posts(ox, oy, lw=1.5, z0=4)

    # SHORT bearing headers (~0-8) at the pivot crossings — small, not a beam line
    for p in range(N_PAIRS):
        X = p * BAY
        hp = iso(X, DEPTH/2, PIVOT_Z, S, ox, oy)
        ax.add_patch(FancyBboxPatch((hp[0]-9, hp[1]-5), 18, 10,
                                    boxstyle="round,pad=0,rounding_size=2",
                                    facecolor=SURFACE, edgecolor=INK, linewidth=1.4,
                                    zorder=12))

    # the rotating 2x10 frame as iso rail BOXES tilted about the axle.
    # Rails run X-wise (across the width, the long members the panels bolt to);
    # joists run front->back. We draw 2 long rails (top & bottom edge of plane)
    # + intermediate purlins, all as thin tilted slabs so it reads as a frame.
    edge_t = (0.0, 1.0)
    # two long rails spanning full width at the low + high edges
    for t in edge_t:
        a = plane_pt(-0.20, t, ox, oy); b = plane_pt(SPAN_X + 0.20, t, ox, oy)
        # thickness offset (down the normal) to give the rail a visible depth
        a2 = plane_pt(-0.20, t, ox, oy, lift=-0.10)
        b2 = plane_pt(SPAN_X + 0.20, t, ox, oy, lift=-0.10)
        face = [a, b, b2, a2]
        ax.add_patch(Polygon(face, closed=True,
                             facecolor=tuple(v*K_RIGHT for v in hx(AMBER_SOFT)),
                             edgecolor=AMBER, linewidth=1.6, zorder=9.6))
        ax.add_line(Line2D([a[0], b[0]], [a[1], b[1]], color=PINE_STK, lw=2.2,
                           zorder=10, solid_capstyle="butt"))
    # mid purlin (lighter)
    a = plane_pt(-0.20, 0.5, ox, oy); b = plane_pt(SPAN_X + 0.20, 0.5, ox, oy)
    ax.add_line(Line2D([a[0], b[0]], [a[1], b[1]], color=AMBER, lw=2.0,
                       zorder=9.8, solid_capstyle="butt"))
    # front->back joists at each column (the 2x10 the panels sit on)
    for X in [p*BAY for p in range(N_PAIRS)]:
        lo = plane_pt(X, 0.0, ox, oy); hi = plane_pt(X, 1.0, ox, oy)
        lo2 = plane_pt(X, 0.0, ox, oy, lift=-0.09); hi2 = plane_pt(X, 1.0, ox, oy, lift=-0.09)
        side = [lo, hi, hi2, lo2]
        ax.add_patch(Polygon(side, closed=True,
                             facecolor=tuple(v*K_LEFT for v in hx(AMBER_SOFT)),
                             edgecolor=AMBER, linewidth=1.5, zorder=10.3))
        ax.add_line(Line2D([lo[0], hi[0]], [lo[1], hi[1]], color=PINE_STK, lw=2.0,
                           zorder=10.5, solid_capstyle="butt"))

    # labels — placed in the OPEN LEFT of the card, clear of the geometry
    x0c, y0c, wc, hc = tier_rect(3)
    ax.text(x0c + 60, y0c + hc*0.50,
            "ROTATING 2×10 FRAME\n(one tilt sub-bay)",
            family="monospace", fontsize=8.4, color=AMBER, va="center",
            ha="left", weight="bold", zorder=14)
    ax.text(x0c + 60, y0c + hc*0.27,
            "long 2×8 beam line\nELIMINATED — the\nplane bears on the\naxle (~0–8 short\nbearing headers)",
            family="monospace", fontsize=7.8, color=INK2, va="center",
            ha="left", zorder=14)
    # callouts
    callout(ax, plane_pt((N_PAIRS-1)*BAY, 0.62, ox, oy),
            (plane_pt((N_PAIRS-1)*BAY, 0.62, ox, oy)[0]+64,
             plane_pt((N_PAIRS-1)*BAY, 0.62, ox, oy)[1]+22), "2")
    ax.text(plane_pt((N_PAIRS-1)*BAY, 0.62, ox, oy)[0]+78,
            plane_pt((N_PAIRS-1)*BAY, 0.62, ox, oy)[1]+22, "2×10 rail",
            family="monospace", fontsize=7.2, color=INK2, va="center", zorder=31)
    hp = iso((N_PAIRS-1)*BAY, DEPTH/2, PIVOT_Z, S, ox, oy)
    callout(ax, (hp[0], hp[1]), (hp[0]+58, hp[1]-30), "H", color=INK)
    ax.text(hp[0]+72, hp[1]-30, "bearing header",
            family="monospace", fontsize=7.2, color=INK2, va="center", zorder=31)

def draw_panels(ox, oy):
    """STAGE 5 — attach blocks + 14x Waaree on the ROTATING plane. Tilt arc +
    'A SAIL · POSITIVE LOCK MANDATORY' safety note. Module-bolt+WEEB callout."""
    ground_plane(ox, oy, alpha=0.45)
    context_posts(ox, oy, lw=1.2, z0=3)
    # context rails (light) on the tilted plane
    for X in [p*BAY for p in range(N_PAIRS)]:
        lo = plane_pt(X, 0.0, ox, oy); hi = plane_pt(X, 1.0, ox, oy)
        ax.add_line(Line2D([lo[0], hi[0]], [lo[1], hi[1]], color=RULE2, lw=2.0,
                           zorder=6))

    # PANELS on the tilted plane: 2 columns (X) x 2 rows (lower/upper along slope)
    rows = [(0.0, 0.5), (0.5, 1.0)]
    for col in range(2):
        x_a = col * BAY + 0.02
        x_b = (col+1) * BAY - 0.02
        for ri, (ta, tb) in enumerate(rows):
            quad = [plane_pt(x_a, ta, ox, oy, 0.04), plane_pt(x_b, ta, ox, oy, 0.04),
                    plane_pt(x_b, tb, ox, oy, 0.04), plane_pt(x_a, tb, ox, oy, 0.04)]
            ax.add_patch(Polygon(quad, closed=True, facecolor=PINE,
                                 edgecolor=PINE_STK, linewidth=1.6, zorder=14))
            for tt in (0.25, 0.5, 0.75):
                ga = plane_pt(x_a + (x_b-x_a)*tt, ta, ox, oy, 0.05)
                gb = plane_pt(x_a + (x_b-x_a)*tt, tb, ox, oy, 0.05)
                ax.add_line(Line2D([ga[0], gb[0]], [ga[1], gb[1]],
                                   color=PINE_LITE, lw=1.0, alpha=0.85, zorder=15))
            gm = 0.5
            ga = plane_pt(x_a, ta + (tb-ta)*gm, ox, oy, 0.05)
            gb = plane_pt(x_b, ta + (tb-ta)*gm, ox, oy, 0.05)
            ax.add_line(Line2D([ga[0], gb[0]], [ga[1], gb[1]],
                               color=PINE_LITE, lw=1.0, alpha=0.85, zorder=15))
        # inter-panel seam
        sa = plane_pt(x_a, 0.5, ox, oy, 0.06); sb = plane_pt(x_b, 0.5, ox, oy, 0.06)
        ax.add_line(Line2D([sa[0], sb[0]], [sa[1], sb[1]], color=SURFACE, lw=3.0, zorder=16))
        ax.add_line(Line2D([sa[0], sb[0]], [sa[1], sb[1]], color=PINE_STK, lw=1.4, zorder=16.1))

    # module-attach blocks at the lower edge
    for p in range(N_PAIRS):
        X = p * BAY
        bp = plane_pt(X, 0.0, ox, oy, 0.0)
        ax.add_patch(Circle((bp[0], bp[1]), 3.2, facecolor=AMBER_SOFT,
                            edgecolor=AMBER, linewidth=1.0, zorder=17))

    # TILT ARC about the pivot — placed in the OPEN LEFT of the card, clear of
    # the plane, the bottom strip + scale bar.
    pivot_scr = iso(-1.05, DEPTH/2, PIVOT_Z - 0.10, S, ox, oy)
    R = 0.74 * S
    for adeg, lab in ((22, "30°"), (44, "45°"), (66, "55°")):
        d = np.array([np.cos(np.deg2rad(adeg)), np.sin(np.deg2rad(adeg))])
        tip = (pivot_scr[0] + R*d[0], pivot_scr[1] + R*d[1])
        ax.add_line(Line2D([pivot_scr[0], tip[0]], [pivot_scr[1], tip[1]],
                           color=INK3, lw=1.0, linestyle=(0,(3,2)), zorder=18))
        ax.text(tip[0]+4, tip[1]+3, lab, family="monospace", fontsize=6.8,
                color=INK3, va="center", zorder=18)
    arc = Arc((pivot_scr[0], pivot_scr[1]), 2*R, 2*R, angle=0,
              theta1=22, theta2=66, color=INK2, lw=1.4, zorder=18)
    ax.add_patch(arc)
    ax.add_patch(Circle((pivot_scr[0], pivot_scr[1]), 3.5, facecolor=INK,
                        edgecolor=SURFACE, linewidth=1.2, zorder=19))
    ax.text(pivot_scr[0]+R*0.5, pivot_scr[1]-16, "tilt range\n(4-position)",
            family="monospace", fontsize=6.8, color=INK2, ha="center",
            va="top", zorder=19)

    # SAFETY note banner — pill at the TOP of the card, above the plane, full
    # clear of geometry.
    x0c, y0c, wc, hc = tier_rect(4)
    bx_c, by_c = x0c + wc*0.50, y0c + hc - 60
    btxt = "⚠  ≈390 ft² PLANE = A SAIL · POSITIVE LOCK MANDATORY"
    ax.add_patch(FancyBboxPatch((bx_c - 198, by_c - 13), 396, 26,
                                boxstyle="round,pad=0,rounding_size=6",
                                facecolor="#F1DDD5",
                                edgecolor=CLAY, linewidth=1.3, zorder=19.5))
    ax.text(bx_c, by_c, btxt, family="monospace", fontsize=8.6, color=CLAY,
            weight="bold", ha="center", va="center", zorder=20)

    # module bolt + WEEB callout (lower-right edge of the plane)
    am = plane_pt((N_PAIRS-1)*BAY, 0.0, ox, oy, 0.04)
    callout(ax, am, (am[0] + 58, am[1] - 26), "M")
    ax.text(am[0] + 72, am[1] - 26, "¼\" bolt + WEEB", family="monospace",
            fontsize=7.2, color=INK2, va="center", zorder=31)
    # attach-block callout (lower-left edge of the plane)
    ab = plane_pt(0.0, 0.0, ox, oy, 0.0)
    callout(ax, ab, (ab[0] - 60, ab[1] - 24), "A", color=AMBER)
    ax.text(ab[0] - 74, ab[1] - 24, "attach block", family="monospace",
            fontsize=7.2, color=AMBER, ha="right", va="center", zorder=31)

    # ---- face-on INSET: one 2-high pair (countable) ----
    iw, ih = 84, 146
    ix0 = x0c + wc - iw - 26
    iy0 = oy - 0.10 * S
    ax.add_patch(FancyBboxPatch((ix0, iy0), iw, ih,
                                boxstyle="round,pad=0,rounding_size=4",
                                facecolor=SURFACE2, edgecolor=RULE2,
                                linewidth=1.0, zorder=25))
    ax.text(ix0 + iw/2, iy0 + ih + 9, "1 PAIR (2-HIGH)", family="monospace",
            fontsize=6.8, color=INK3, ha="center", va="center", zorder=27)
    pad = 8; pw = iw - 2*pad; ph = (ih - 3*pad) / 2.0
    for r in range(2):
        py0 = iy0 + pad + r*(ph + pad)
        ax.add_patch(FancyBboxPatch((ix0+pad, py0), pw, ph,
                                    boxstyle="round,pad=0,rounding_size=2",
                                    facecolor=PINE, edgecolor=PINE_STK,
                                    linewidth=1.2, zorder=26))
        for cc in range(1, 6):
            gx = ix0 + pad + pw*cc/6.0
            ax.add_line(Line2D([gx, gx], [py0+2, py0+ph-2],
                               color=PINE_LITE, lw=0.8, alpha=0.8, zorder=27))
        for rr in range(1, 4):
            gy = py0 + ph*rr/4.0
            ax.add_line(Line2D([ix0+pad+2, ix0+pad+pw-2], [gy, gy],
                               color=PINE_LITE, lw=0.8, alpha=0.8, zorder=27))
    sy = iy0 + pad + ph + pad/2.0
    ax.add_line(Line2D([ix0+pad, ix0+pad+pw], [sy, sy], color=INK2, lw=1.6, zorder=28))

# ----------------------------------------------------------------------------
# RENDER THE 5 STAGES
# ----------------------------------------------------------------------------
stages = [
    (1, "FOOTINGS", "~6–8 holes · ≈ HALF the base · post pairs", ["F"], draw_footings),
    (2, "PIVOT POSTS", "~6–8 dedicated ≥6×6 pairs · NO 4×4 fronts", ["1"], draw_pivot_posts),
    (3, "PIVOT MECHANISM", "through-bolt · steel bushings · positive lock · bond · loop",
        ["3a", "3b", "3c", "B", "L"], draw_pivot_mech),
    (4, "RAILS / JOISTS (ROTATING)", "2×10 frame · 2×8 beam line eliminated",
        ["2", "H"], draw_rails),
    (5, "BLOCKS + PANELS (ON PLANE)", "attach blocks · 14× Waaree 575 W · tilt arc",
        ["A", "M"], draw_panels),
]

for idx, (num, name, sub, calls, fn) in enumerate(stages):
    x0, y0, w, h = draw_card(idx, num, name, sub)
    stage_label_chip(x0, y0, h, calls)
    if fn.__name__ in STAGE_Z3:                 # iso stages center via the loop origin
        zlo, zhi = STAGE_Z3[fn.__name__]
        ox, oy = centered_origin((x0, y0, w, h), zlo, zhi, fx=0.48)
    else:                                       # footings & pivot-mech self-place
        ox, oy = vignette_origin(x0, y0, w, h)
    fn(ox, oy)
    if idx < len(stages) - 1:
        down_arrow(idx)

# ============================================================================
# RIGHT COLUMN — BOM LEGEND BOX (reconciliation consensus)
# ============================================================================
BOX_X = RIGHT_X
BOX_W = 1300 - RIGHT_X - 18
BOX_Y = 96 + YSHIFT
BOX_H = 1394
ax.add_patch(FancyBboxPatch((BOX_X, BOX_Y), BOX_W, BOX_H,
                            boxstyle="round,pad=0,rounding_size=6",
                            facecolor=SURFACE, edgecolor=RULE2, linewidth=1.2,
                            zorder=10))
hb_h = 30
ax.add_patch(FancyBboxPatch((BOX_X, BOX_Y + BOX_H - hb_h), BOX_W, hb_h,
                            boxstyle="round,pad=0,rounding_size=6",
                            facecolor=SURFACE2, edgecolor=RULE2, linewidth=1.0,
                            zorder=11))
ax.text(BOX_X + BOX_W/2, BOX_Y + BOX_H - hb_h/2, "BILL OF MATERIALS · OPTION 3",
        family="monospace", fontsize=10.5, color=INK3, weight="bold",
        ha="center", va="center", zorder=12)

# MOST MATERIAL-INTENSIVE banner just under the header
ban_h = 40
ax.add_patch(FancyBboxPatch((BOX_X + 6, BOX_Y + BOX_H - hb_h - ban_h - 4), BOX_W - 12, ban_h,
                            boxstyle="round,pad=0,rounding_size=4",
                            facecolor=AMBER_SOFT, edgecolor=AMBER, linewidth=1.2,
                            zorder=11.5))
ax.text(BOX_X + BOX_W/2, BOX_Y + BOX_H - hb_h - ban_h/2 - 4,
        "MOST MATERIAL-INTENSIVE OPTION\n$360 video = disclaimed light-panel build",
        family="monospace", fontsize=6.8, color=AMBER, weight="bold",
        ha="center", va="center", zorder=12)

AMB = ("amber", AMBER, AMBER_SOFT)
BM  = ("beam", INK, SURFACE)
PN  = ("pine", PINE, PINE_SOFT)
HW  = ("slate", SLATE, SLATE)
FT  = ("foot", INK2, SURFACE2)

# badge, name, spec, qty, family, e-set flag
bom = [
    ("C1", "Pivot posts", "≥6×6 MCA pairs", "~6–8", AMB, True),
    ("C2", "Rails / joists", "2×10 MCA · rotates", "~14", AMB, False),
    ("C3", "Pivot through-bolts", "Ø/grade · SS/HDG", "~6–8", HW, True),
    ("C4", "Steel bushings", "sleeve BOTH holes", "~12–16", HW, True),
    ("C5", "Positive-lock assy", "pins/struts → post", "~6–8", HW, True),
    ("C6", "Cross-pivot bond", "braided-Cu ≥10 AWG", "~6–8", PN, False),
    ("C7", "PV service loops", "UV PV 8 + 10AWG EGC", "/cross", HW, False),
    ("F", "Footings", "30 MPa Ø320 · ≈half base", "~18–24", FT, True),
    ("B", "Beams (headers)", "2×8 mostly ELIMINATED", "~0–8", BM, False),
    ("A", "Module blocks", "offcut MCA", "~56", AMB, False),
    ("M", "Module bolts+WEEB", "¼\"×2½\" SS + WEEB", "~56", HW, False),
]

row_top = BOX_Y + BOX_H - hb_h - ban_h - 18
note_zone = 210
row_area = (row_top - (BOX_Y + note_zone))
rh = row_area / len(bom)

for i, (badge, name, spec, qty, fam, eset) in enumerate(bom):
    ry = row_top - i * rh - rh/2
    if i > 0:
        ax.add_line(Line2D([BOX_X + 10, BOX_X + BOX_W - 10],
                           [ry + rh/2, ry + rh/2], color=RULE,
                           lw=0.8, zorder=11))
    ax.add_patch(Circle((BOX_X + 26, ry + 8), 11, facecolor=SURFACE,
                        edgecolor=INK2, linewidth=1.3, zorder=13))
    ax.text(BOX_X + 26, ry + 8, badge, family="monospace",
            fontsize=9.5 if len(badge) == 1 else 7.6,
            color=INK, ha="center", va="center", zorder=14)
    ax.add_patch(Circle((BOX_X + 26, ry - 12), 4.5, facecolor=fam[1],
                        edgecolor="none", zorder=14))
    ax.text(BOX_X + 46, ry + 11, name, family="monospace", fontsize=10.2,
            color=INK, weight="bold", va="center", zorder=13)
    ax.text(BOX_X + 46, ry - 5, spec, family="monospace", fontsize=7.8,
            color=INK2, va="center", zorder=13)
    # qty (right) — and e-set tag if applicable
    ax.text(BOX_X + BOX_W - 12, ry + 6, qty, family="monospace", fontsize=9.0,
            color=INK, weight="bold", ha="right", va="center", zorder=13)
    if eset:
        ax.text(BOX_X + BOX_W - 12, ry - 9, "⚙ ENG-SET", family="monospace",
                fontsize=6.6, color=CLAY, ha="right", va="center", zorder=13,
                weight="bold")

ny = BOX_Y + note_zone - 8
ax.add_line(Line2D([BOX_X + 10, BOX_X + BOX_W - 10], [ny, ny],
                   color=RULE2, lw=1.0, zorder=12))
note = ("structure INHERITED (Vandewetering/\n"
        "Hayibo/Pearce 2022 6(3):41) + video,\n"
        "via tilting-bom-reconciliation.md —\n"
        "estimate, NOT stamped. Pivot/lock/\n"
        "bushing = ENGINEER-SET (A7).\n"
        "Energy = 15_tilt_sweep.py (computed).")
ax.text(BOX_X + 14, ny - 12, note, family="monospace", fontsize=7.6,
        color=INK3, va="top", zorder=12)

# swatch key
keys = [("amber=timber", AMBER), ("beam=2×8", INK), ("pine=panel/bond", PINE),
        ("slate=steel/elec", SLATE), ("clay=positive lock", CLAY)]
kx = BOX_X + 14
ky = BOX_Y + 14
for lbl, col in keys:
    ax.add_patch(Polygon([(kx, ky), (kx+12, ky), (kx+12, ky+10), (kx, ky+10)],
                         closed=True, facecolor=col, edgecolor=INK2,
                         linewidth=0.6, zorder=13))
    ax.text(kx + 16, ky + 5, lbl, family="monospace", fontsize=7.4,
            color=INK2, va="center", zorder=13)
    ky += 17

# ============================================================================
# ASSEMBLY-ORDER RIBBON — far left margin
# ============================================================================
rib_x = 16
ax.add_line(Line2D([rib_x, rib_x], [TIER_BOT, TIER_TOP], color=RULE2,
                   lw=1.4, zorder=2))
ribbon = ["FOOTINGS", "PIVOT POSTS", "PIVOT MECH", "ROT. FRAME", "PANELS"]
for idx, lbl in enumerate(ribbon):
    x0, y0, w, h = tier_rect(idx)
    yc = y0 + h/2
    ax.add_patch(Circle((rib_x, yc), 7, facecolor=SURFACE, edgecolor=INK2,
                        linewidth=1.2, zorder=3))
    ax.text(rib_x, yc, str(idx+1), family="monospace", fontsize=8, color=INK,
            ha="center", va="center", zorder=4)
    ax.text(rib_x + 12, yc, lbl, family="monospace", fontsize=7.6, color=INK3,
            rotation=90, ha="center", va="center", zorder=4)

# ============================================================================
# BOTTOM STRIP — provenance + scale bar
# ============================================================================
prov = ("PROVENANCE: SOLAR_RACKING_HANDOFF §5 + tilting-pivot-mount.md / "
        "tilting-bom-reconciliation.md (inherited estimate) · energy 15_tilt_sweep.py · "
        "matplotlib/Agg · 2026-06-06")
ax.text(34, 60, prov, family="monospace", fontsize=8.0, color=INK3, va="center")

sb_x = 34
sb_y = 30
seg = 0.5 * S
n_seg = 4
ax.text(sb_x, sb_y + 4, "0", family="monospace", fontsize=7.5, color=INK3,
        ha="right", va="center")
for k in range(n_seg):
    col = INK2 if k % 2 == 0 else SURFACE
    ax.add_patch(Polygon([(sb_x + k*seg, sb_y), (sb_x + (k+1)*seg, sb_y),
                          (sb_x + (k+1)*seg, sb_y + 8), (sb_x + k*seg, sb_y + 8)],
                         closed=True, facecolor=col, edgecolor=INK,
                         linewidth=1.0, zorder=5))
ax.text(sb_x + n_seg*seg + 8, sb_y + 4,
        f"{n_seg*0.5:.1f} m / ~{n_seg*0.5*3.281:.1f} ft  · VIGNETTE SCALE",
        family="monospace", fontsize=7.5, color=INK3, ha="left", va="center")

# ----------------------------------------------------------------------------
# EXPORT
# ----------------------------------------------------------------------------
import os
OUT = "/Users/jasonkrueger/Library/CloudStorage/GoogleDrive-jasonkrue@gmail.com/My Drive/prsm/electrical-review/artifacts/racking/exploded"
png = os.path.join(OUT, "exploded_opt3_final.png")
svg = os.path.join(OUT, "exploded_opt3_final.svg")
fig.savefig(png, dpi=200, facecolor=PAPER, bbox_inches="tight", pad_inches=0.22)
fig.savefig(svg, facecolor=PAPER, bbox_inches="tight", pad_inches=0.22)
print("wrote", png)
print("wrote", svg)