diff --git a/.ciagent/CHECKPOINT.json b/.ciagent/CHECKPOINT.json
index 493749d..7405cc8 100644
--- a/.ciagent/CHECKPOINT.json
+++ b/.ciagent/CHECKPOINT.json
@@ -1,37 +1,19 @@
{
- "phase": 2,
+ "phase": 3,
"stage": "complete",
"milestone": "v1.30",
- "phase_role": "final",
+ "phase_role": "execution",
"attempts": 0,
- "updated_at": "2026-08-20T14:40:00Z",
+ "updated_at": "2026-08-20T15:30:00Z",
"project": "acdl",
"projects": ["acdl", "nova-blockchain-exchange"],
"active_milestone": "v1.30",
- "milestone_branch": null,
- "phase_branch": null,
+ "milestone_branch": "milestone/v1.30-leadership-deck",
+ "phase_branch": "phase/03-deck-polish",
"tag_line": "v1.29.x",
- "phase_name": "final-review-ship",
+ "phase_name": "deck-polish",
"milestone_type": "feature",
"reqs_covered": ["REQ-372.1","REQ-372.2","REQ-372.3","REQ-372.4","REQ-372.5","REQ-372.6","REQ-372.7","REQ-372.8","REQ-372.9","REQ-372.10","REQ-372.11","REQ-372.12"],
"reqs_partial": [],
- "previous_milestone": {
- "milestone": "v1.30",
- "tag": "v1.29.3",
- "complete": true,
- "merged_to_main": "96765fe",
- "branches_deleted": true,
- "releases_created": true,
- "release_ids": [811, 812, 813]
- },
- "ship": {
- "tag": "v1.29.3",
- "release_id": 813,
- "release_url": "https://git.cloudinit.dev/continuous-intelligence/acdl/releases/tag/v1.29.3",
- "merged_to_main": true,
- "merge_commit": "96765fe",
- "branches_deleted": ["phase/02-final-review-ship", "milestone/v1.30-leadership-deck"],
- "local_only": false
- },
- "notes": "v1.30 MILESTONE COMPLETE. Tag v1.29.3 (milestone release), Gitea release id=813. Merged to main 96765fe. All milestone branches deleted (local + remote). REQ-372.1..12 all complete. CAP-042 + D-241..D-243 recorded. 3 Gitea releases: 811 (P0 v1.29.1), 812 (P1 v1.29.2), 813 (P2 v1.29.3). Checkpoint cleared — next run starts a new milestone."
+ "notes": "v1.30 P3 (deck-polish) COMPLETE. D-244 (cover), D-245 (slide 7 what-works-now), D-246 (7 mermaid diagrams). F1-F5 polish fixes. 7 S&P-themed diagrams rendered via local mmdc (vendored Chromium libs). All checks pass. Next: SHIP (v1.29.4)."
}
\ No newline at end of file
diff --git a/.ciagent/CLARIFY.md b/.ciagent/CLARIFY.md
index c6fbecd..ac916f7 100644
--- a/.ciagent/CLARIFY.md
+++ b/.ciagent/CLARIFY.md
@@ -206,8 +206,118 @@ at the v1.30 ship wave (CAP-042 row + intake assumption correction).
---
+## Polish-phase decisions (D-244, D-245, D-246 — full autonomy)
+
+The first draft (v1.29.3) passed all 12 REQs but the rendered PPTX
+has 7 polish issues (vertical imbalance, text clipping, double bullet
+markers, no visual variety, near-invisible footer, slide-6 red wall,
+blockquote grey). The user requested: (1) polish all 7 issues, (2)
+add a "What works now" emphasis to slide 7, (3) generate mermaid
+diagrams for every slide. These decisions record the deviations from
+prior binding decisions.
+
+### D-244 — Slide 1 becomes a black-background cover (overrides grill G-1)
+
+**Polish issue #4.** Grill G-1 locked "all 7 slides as white content
+slides." The user approved making slide 1 a black-background cover
+(strong opener for a live leadership presentation) while keeping
+slides 2-7 as white content slides. The on-slide body text from the
+Slide Content Map is preserved verbatim — only the visual treatment
+(background + text color) changes. REQ-372.7's "deviation requires
+CLARIFY" clause applies to *content* deviations; this is a
+presentation polish, not a content change. The source change: slide
+1's `## The friction...` → `# The friction...` (H1) + `` directive, which triggers `render_title_slide` (black bg,
+red top bar, white text).
+**Confidence:** 1.0 (user-confirmed).
+
+### D-245 — Slide 7 "What works now" content addition (deviation from Slide Content Map)
+
+**User request.** A new "What works now" block is added to slide 7's
+on-slide body, before the existing "What we ask" block. This is a
+*content* deviation — the Slide Content Map specifies slide 7 as:
+What we ask / Why now / What comes back + closer. The new block
+emphasizes what's proven today (L1/L2 stack deployment works, live
+apply to the sandbox/dev AWS account, confidence 0.800) and the
+next-steps arc (ingest greenfield pilot projects → promote from
+sandbox to production → integrate with the SPGE constitutional
+library → serve as the infrastructure layer).
+
+**Factual grounding (verified in RESEARCH):**
+- 13 L1 primitives + 2 L2 modules in `modules/registry.json` (all at
+ `1.0.0`): alb, cloudfront, dynamodb, ecr, ecs-cluster, ecs-service,
+ iam-role, kms-key, rds, s3, uptime, vpc, waf + microservice, static-
+ assets.
+- CAP-031: live `terraform apply` against AWS account `581513795199`
+ (the dev environment, `mode: full`, dev-only per D-209), producing
+ real ARNs (ALB, ECS, DynamoDB, S3, KMS) at confidence 0.800. This is
+ a **live AWS apply**, NOT "tested locally" and NOT a "sandbox"
+ account (the codebase has no sandbox account; the pilot-readiness
+ policy D-208 blocks placeholder accounts).
+
+**"sandbox" colloquial term note:** The user explicitly chose to keep
+"sandbox" as a colloquial term for the dev AWS account after I flagged
+that the codebase fact is "dev account `581513795199`, live apply."
+The slide uses "sandbox" (colloquial); the speaker notes carry the
+precise facts (dev account, live apply, confidence 0.800).
+
+**"SPGE constitutional library" unverified-in-repo note:** "SPGE"
+and "constitutional library" appear nowhere in the codebase (0
+matches across all `.md`/`.py`/`.json`/`.yml`). The user confirmed it
+is an external system the audience recognizes. The slide includes it
+as a named next-step integration target, not as a current capability.
+The speaker notes do not assert it exists in the codebase.
+
+The Slide Content Map in PROJECT.md is updated to reflect the new
+slide 7 structure.
+**Confidence:** 0.90.
+
+### D-246 — Mermaid diagrams on all 7 slides
+
+**User request.** Each of the 7 slides gets a leader-friendly,
+non-technical mermaid diagram covering its core concept. Different
+mermaid shapes per slide for visual variety.
+
+**Render path (verified):** The local `mmdc` (mermaid-cli) is blocked
+by missing Chromium shared libraries (10+ system libs absent, no
+root). Resolution: 24 Debian bookworm `.deb` packages fetched from the
+Debian mirror, `.so` files extracted to `/home/opencode/tmp/so2/`,
+`LD_LIBRARY_PATH` set to include them. Chromium (puppeteer cache at
+`/home/opencode/.cache/puppeteer/chrome/.../chrome`) launches with
+the vendored libs; `mmdc` renders with the S&P theme (`sp-theme.json`)
++ 2x scale + transparent background. Verified: test diagram renders
+to a valid PNG with all 4 S&P colors present. The `.mmd` source files
++ rendered PNGs are committed for reproducibility; a future
+environment with system Chromium can re-render via the existing
+`scripts/render_slides.sh` mermaid step.
+
+**Theme:** Inline `%%{init: {...}}%%` directive in each `.mmd` with
+S&P theme variables (`primaryColor:#1B1B1B`, `primaryBorderColor:
+#D6002A`, `primaryTextColor:#fff`, `secondaryColor:#fff`,
+`tertiaryColor:#F0F0F0`, `lineColor:#1B1B1B`) — only the 4 S&P tokens.
+Transparent background. No component names, no technical jargon —
+concepts only (leader-friendly).
+
+**Diagram-type-per-slide mapping:**
+1. flowchart LR — 3 frictions → Nova absorbs them
+2. flowchart TB — Central IT golden image → Nova one layer up
+3. flowchart LR with subgraphs — two principles → everything inherits
+4. flowchart LR (3 columns) — Live · Attested · Stays human
+5. flowchart LR (two halves) — In Nova's lane / Outside Nova's lane
+6. timeline — the 18-month shape (α→β→γ→δ)
+7. flowchart LR — what works now → next steps arc
+
+The Slide Content Map in PROJECT.md is updated to include a "Diagram"
+field per slide.
+**Confidence:** 0.88.
+
+---
+
## Requirements impact
-No requirements are added, removed, or re-scoped by these decisions.
-D-241–D-243 are load-bearing context for executing REQ-372.1–.12 as
-written. The spec is locked (v1.0, 2026-08-20); no spec text changes.
\ No newline at end of file
+No requirements are added, removed, or re-scoped by D-241–D-243.
+D-244–D-246 are polish-phase decisions that record deviations from
+prior binding decisions (G-1) and the Slide Content Map (slide 7).
+The spec is locked (v1.0, 2026-08-20); no spec text changes. The
+Slide Content Map in PROJECT.md is updated to reflect D-245 (slide 7
+structure) and D-246 (diagram field per slide).
\ No newline at end of file
diff --git a/docs/presentations/assets/mmd/leadership-slide-1.mmd b/docs/presentations/assets/mmd/leadership-slide-1.mmd
new file mode 100644
index 0000000..21d11e0
--- /dev/null
+++ b/docs/presentations/assets/mmd/leadership-slide-1.mmd
@@ -0,0 +1,23 @@
+%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%%
+flowchart LR
+ subgraph F["The friction"]
+ F1["Authored by
non-specialists"]
+ F2["Every change
gated"]
+ F3["Compliance
checked late"]
+ end
+ F1 --- F2
+ F2 --- F3
+ F --> N
+ subgraph N["Nova absorbs all three"]
+ N1["Owned
building blocks"]
+ N2["Separation
of concerns"]
+ N3["Attested compliance
up front"]
+ end
+ style F fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
+ style N fill:#1B1B1B,color:#fff,stroke:#D6002A
+ style F1 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
+ style F2 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
+ style F3 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
+ style N1 fill:#1B1B1B,color:#fff,stroke:#D6002A
+ style N2 fill:#1B1B1B,color:#fff,stroke:#D6002A
+ style N3 fill:#1B1B1B,color:#fff,stroke:#D6002A
\ No newline at end of file
diff --git a/docs/presentations/assets/mmd/leadership-slide-2.mmd b/docs/presentations/assets/mmd/leadership-slide-2.mmd
new file mode 100644
index 0000000..3d34143
--- /dev/null
+++ b/docs/presentations/assets/mmd/leadership-slide-2.mmd
@@ -0,0 +1,10 @@
+%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%%
+flowchart TB
+ CIT["Central IT curates
the golden OS image"]
+ NOVA["Nova curates
the cloud infrastructure"]
+ CONSUMER["Consumer declares
a contract"]
+ CIT -->|own, patch, ship| NOVA
+ NOVA -->|own, patch, attest| CONSUMER
+ style CIT fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
+ style NOVA fill:#1B1B1B,color:#fff,stroke:#D6002A
+ style CONSUMER fill:#fff,color:#1B1B1B,stroke:#D6002A
\ No newline at end of file
diff --git a/docs/presentations/assets/mmd/leadership-slide-3.mmd b/docs/presentations/assets/mmd/leadership-slide-3.mmd
new file mode 100644
index 0000000..22aac2e
--- /dev/null
+++ b/docs/presentations/assets/mmd/leadership-slide-3.mmd
@@ -0,0 +1,17 @@
+%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%%
+flowchart LR
+ T1["Sovereign
boundary"]
+ T2["Lower autonomous
higher attested"]
+ I1["Four-layer
model"]
+ I2["HITL
gates"]
+ I3["Contract
schema"]
+ T1 --> I1
+ T1 --> I3
+ T2 --> I2
+ T2 --> I3
+ T1 -.-> T2
+ style T1 fill:#1B1B1B,color:#fff,stroke:#D6002A
+ style T2 fill:#1B1B1B,color:#fff,stroke:#D6002A
+ style I1 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
+ style I2 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
+ style I3 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
\ No newline at end of file
diff --git a/docs/presentations/assets/mmd/leadership-slide-4.mmd b/docs/presentations/assets/mmd/leadership-slide-4.mmd
new file mode 100644
index 0000000..cbff1d7
--- /dev/null
+++ b/docs/presentations/assets/mmd/leadership-slide-4.mmd
@@ -0,0 +1,9 @@
+%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%%
+flowchart LR
+ L["Live today
41 capabilities
12 domains"]
+ A["Attested on
promotion
named human approver"]
+ H["Stays human
by design
escalation below threshold"]
+ L --> A --> H
+ style L fill:#1B1B1B,color:#fff,stroke:#D6002A
+ style A fill:#1B1B1B,color:#fff,stroke:#D6002A
+ style H fill:#1B1B1B,color:#fff,stroke:#D6002A
\ No newline at end of file
diff --git a/docs/presentations/assets/mmd/leadership-slide-5.mmd b/docs/presentations/assets/mmd/leadership-slide-5.mmd
new file mode 100644
index 0000000..b895af6
--- /dev/null
+++ b/docs/presentations/assets/mmd/leadership-slide-5.mmd
@@ -0,0 +1,23 @@
+%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%%
+flowchart LR
+ subgraph IN["In Nova's lane"]
+ I1["Infrastructure
primitives"]
+ I2["Operational
guardrails"]
+ I3["CVE response
infra layer"]
+ end
+ C["The
contract"]
+ subgraph OUT["Outside Nova's lane"]
+ O1["Application
business logic"]
+ O2["IDE & sprint
workflows"]
+ O3["AppSec &
dependency review"]
+ end
+ IN --- C --- OUT
+ style IN fill:#1B1B1B,color:#fff,stroke:#D6002A
+ style OUT fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
+ style C fill:#fff,color:#D6002A,stroke:#D6002A
+ style I1 fill:#1B1B1B,color:#fff,stroke:#D6002A
+ style I2 fill:#1B1B1B,color:#fff,stroke:#D6002A
+ style I3 fill:#1B1B1B,color:#fff,stroke:#D6002A
+ style O1 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
+ style O2 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
+ style O3 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
\ No newline at end of file
diff --git a/docs/presentations/assets/mmd/leadership-slide-6.mmd b/docs/presentations/assets/mmd/leadership-slide-6.mmd
new file mode 100644
index 0000000..5f817b7
--- /dev/null
+++ b/docs/presentations/assets/mmd/leadership-slide-6.mmd
@@ -0,0 +1,11 @@
+%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%%
+flowchart LR
+ A["α
Now → Q4'26
Operating model +
federated governance"]
+ B["β
Q1'27
Auto-published
infra observability"]
+ G["γ
Q2'27
Runbook generation
from telemetry"]
+ D["δ
Q3'27 → Q4'27
Audit ledger
tamper-resistant"]
+ A --> B --> G --> D
+ style A fill:#1B1B1B,color:#fff,stroke:#D6002A
+ style B fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
+ style G fill:#1B1B1B,color:#fff,stroke:#D6002A
+ style D fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
\ No newline at end of file
diff --git a/docs/presentations/assets/mmd/leadership-slide-7.mmd b/docs/presentations/assets/mmd/leadership-slide-7.mmd
new file mode 100644
index 0000000..b116909
--- /dev/null
+++ b/docs/presentations/assets/mmd/leadership-slide-7.mmd
@@ -0,0 +1,13 @@
+%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1B1B1B','primaryBorderColor':'#D6002A','primaryTextColor':'#fff','secondaryColor':'#fff','secondaryBorderColor':'#D6002A','secondaryTextColor':'#1B1B1B','tertiaryColor':'#F0F0F0','lineColor':'#1B1B1B','fontFamily':'"Helvetica Neue","Arial",sans-serif'}}}%%
+flowchart LR
+ W["What works now
L1 & L2 stacks live
confidence 0.800"]
+ N1["Ingest greenfield
pilot projects"]
+ N2["Promote sandbox
to production"]
+ N3["Integrate SPGE
constitutional library"]
+ N4["Serve as the
infrastructure layer"]
+ W --> N1 --> N2 --> N3 --> N4
+ style W fill:#1B1B1B,color:#fff,stroke:#D6002A
+ style N1 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
+ style N2 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
+ style N3 fill:#F0F0F0,color:#1B1B1B,stroke:#D6002A
+ style N4 fill:#1B1B1B,color:#fff,stroke:#D6002A
\ No newline at end of file
diff --git a/docs/presentations/assets/png/leadership-slide-1.png b/docs/presentations/assets/png/leadership-slide-1.png
new file mode 100644
index 0000000..d6a037d
Binary files /dev/null and b/docs/presentations/assets/png/leadership-slide-1.png differ
diff --git a/docs/presentations/assets/png/leadership-slide-2.png b/docs/presentations/assets/png/leadership-slide-2.png
new file mode 100644
index 0000000..1c4c8a5
Binary files /dev/null and b/docs/presentations/assets/png/leadership-slide-2.png differ
diff --git a/docs/presentations/assets/png/leadership-slide-3.png b/docs/presentations/assets/png/leadership-slide-3.png
new file mode 100644
index 0000000..94a4cef
Binary files /dev/null and b/docs/presentations/assets/png/leadership-slide-3.png differ
diff --git a/docs/presentations/assets/png/leadership-slide-4.png b/docs/presentations/assets/png/leadership-slide-4.png
new file mode 100644
index 0000000..24fd85f
Binary files /dev/null and b/docs/presentations/assets/png/leadership-slide-4.png differ
diff --git a/docs/presentations/assets/png/leadership-slide-5.png b/docs/presentations/assets/png/leadership-slide-5.png
new file mode 100644
index 0000000..672fb78
Binary files /dev/null and b/docs/presentations/assets/png/leadership-slide-5.png differ
diff --git a/docs/presentations/assets/png/leadership-slide-6.png b/docs/presentations/assets/png/leadership-slide-6.png
new file mode 100644
index 0000000..d19a496
Binary files /dev/null and b/docs/presentations/assets/png/leadership-slide-6.png differ
diff --git a/docs/presentations/assets/png/leadership-slide-7.png b/docs/presentations/assets/png/leadership-slide-7.png
new file mode 100644
index 0000000..4dde44e
Binary files /dev/null and b/docs/presentations/assets/png/leadership-slide-7.png differ
diff --git a/docs/presentations/nova-leadership-deck-marp.md b/docs/presentations/nova-leadership-deck-marp.md
index 2af7634..92eebf9 100644
--- a/docs/presentations/nova-leadership-deck-marp.md
+++ b/docs/presentations/nova-leadership-deck-marp.md
@@ -14,6 +14,11 @@
Vision grounding: [1] citations in slides 3, 5, 7 speaker notes
resolve to docs/vision.md (the spec's acdl-vision.md reference).
+ Diagrams: 7 S&P-themed mermaid diagrams (D-246), one per slide,
+ rendered via scripts/render_leadership_diagrams.sh (mmdc + vendored
+ Chromium libs). Sources: assets/mmd/leadership-slide-N.mmd.
+ PNGs: assets/png/leadership-slide-N.png.
+
Render: python3 scripts/render_pptx.py docs/presentations/nova-leadership-deck-marp.md \
--output docs/presentations/nova-leadership-deck.pptx
Smoke test: bash scripts/check_leadership_deck.sh
@@ -42,7 +47,10 @@ style: |
@media print { section { overflow: hidden; } }
---
-## The friction every delivery team lives today
+
+
+
+# The friction every delivery team lives today
> *Velocity is up; the coordination surface around each change is up faster.*
@@ -52,6 +60,8 @@ style: |
> *Nova absorbs all three — owned building blocks, separation of concerns, attested compliance up front.*
+
+
\ No newline at end of file
diff --git a/docs/presentations/nova-leadership-deck.pptx b/docs/presentations/nova-leadership-deck.pptx
index 20774fb..120bed2 100644
Binary files a/docs/presentations/nova-leadership-deck.pptx and b/docs/presentations/nova-leadership-deck.pptx differ
diff --git a/scripts/render_leadership_diagrams.sh b/scripts/render_leadership_diagrams.sh
new file mode 100755
index 0000000..cebba80
--- /dev/null
+++ b/scripts/render_leadership_diagrams.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+# render_leadership_diagrams.sh — render the 7 leadership-deck mermaid
+# diagrams to PNG via mmdc. Single-shot helper (NOT a CI gate, per
+# D-241/D-246). Uses vendored Chromium shared libraries (D-246).
+#
+# Usage: bash scripts/render_leadership_diagrams.sh
+set -euo pipefail
+
+cd "$(git rev-parse --show-toplevel)"
+
+MMD_DIR="docs/presentations/assets/mmd"
+PNG_DIR="docs/presentations/assets/png"
+THEME_JSON="$MMD_DIR/sp-theme.json"
+PUPPETEER_CFG="docs/presentations/assets/puppeteer-config.json"
+
+# Vendored Chromium shared libraries (D-246) — extracted from Debian
+# bookworm .deb packages to /home/opencode/tmp/so2/
+VENDORED_SO="/home/opencode/tmp/so2"
+export LD_LIBRARY_PATH="${VENDORED_SO}:${LD_LIBRARY_PATH:-}"
+
+export CHROME_PATH="/home/opencode/.cache/puppeteer/chrome/linux-152.0.7977.42/chrome-linux64/chrome"
+
+echo "=== Rendering leadership deck mermaid diagrams (S&P theme, 2x, transparent) ==="
+for mmd in "$MMD_DIR"/leadership-slide-*.mmd; do
+ name="$(basename "$mmd" .mmd)"
+ png="$PNG_DIR/$name.png"
+ echo " $name.mmd → $name.png"
+ npx --yes @mermaid-js/mermaid-cli@11.16.0 \
+ --configFile "$THEME_JSON" \
+ --puppeteerConfigFile "$PUPPETEER_CFG" \
+ -s 2 -b transparent \
+ --input "$mmd" \
+ --output "$png" 2>&1 | grep -v "^$" || true
+done
+echo " Rendered $(ls "$PNG_DIR"/leadership-slide-*.png 2>/dev/null | wc -l) leadership PNGs."
\ No newline at end of file
diff --git a/scripts/render_pptx.py b/scripts/render_pptx.py
index 79391ef..a1ddbef 100644
--- a/scripts/render_pptx.py
+++ b/scripts/render_pptx.py
@@ -31,7 +31,7 @@ try:
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE
- from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
+ from pptx.enum.text import PP_ALIGN, MSO_ANCHOR, MSO_AUTO_SIZE
from pptx.oxml.ns import qn
except ImportError:
print("ERROR: python-pptx not installed.", file=sys.stderr)
@@ -206,11 +206,13 @@ def _add_title_bar(slide):
return bar
-def _add_footer(slide, text: str):
+def _add_footer(slide, text: str, dark_bg: bool = False):
"""Right-aligned footer textbox at the bottom of every slide.
REQ-372.5 / D-242: the python-pptx path does not read the Marp
`footer:` directive, so the footer is rendered as a textbox.
+ F4 polish: dark text on white content slides, light text on the
+ black cover slide.
"""
if not text:
return None
@@ -225,7 +227,7 @@ def _add_footer(slide, text: str):
r.text = text
r.font.name = FONT_NAME
r.font.size = Pt(10)
- r.font.color.rgb = GREY_HEADER
+ r.font.color.rgb = GREY_HEADER if dark_bg else BODY_TEXT
return tb
@@ -308,9 +310,15 @@ def _strip_inline_emphasis(text: str) -> str:
return text
-def _inline_runs(p, text: str, *, size: int = 18, base_color: RGBColor = BODY_TEXT):
- """Add inline runs to paragraph `p`, rendering **bold** as red strong,
- `code` as monospace, *italic* as italic. Other text is plain."""
+def _inline_runs(p, text: str, *, size: int = 18, base_color: RGBColor = BODY_TEXT,
+ bold_color: RGBColor = None):
+ """Add inline runs to paragraph `p`, rendering **bold** as strong,
+ `code` as monospace, *italic* as italic. Other text is plain.
+ F5(b): bold_color defaults to RED (legacy behavior) but can be
+ overridden to base_color to de-emphasize bold on slides with many
+ bold sections (avoids a red wall)."""
+ if bold_color is None:
+ bold_color = RED
# Tokenize on `**...**`, `*...*`, `` `...` ``
tokens = re.split(r"(\*\*[^*]+\*\*|`[^`]+`|\*[^*]+\*)", text)
for tok in tokens:
@@ -322,7 +330,7 @@ def _inline_runs(p, text: str, *, size: int = 18, base_color: RGBColor = BODY_TE
r.font.name = FONT_NAME
r.font.size = Pt(size)
r.font.bold = True
- r.font.color.rgb = RED
+ r.font.color.rgb = bold_color
elif tok.startswith("`") and tok.endswith("`"):
r = p.add_run()
r.text = tok[1:-1]
@@ -586,7 +594,7 @@ def parse_slide(slide_src: str):
}
-def render_title_slide(prs, slide_data, footer_text: str = ""):
+def render_title_slide(prs, slide_data, deck_dir: Path, footer_text: str = ""):
slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank
_set_bg(slide, BLACK)
# red top bar
@@ -635,7 +643,12 @@ def render_title_slide(prs, slide_data, footer_text: str = ""):
r_b.font.italic = True
r_b.font.color.rgb = WHITE
cur_top += Inches(0.85)
- _add_footer(slide, footer_text)
+ elif kind == "image":
+ # image on title slide (D-246 diagrams on cover)
+ img_path = deck_dir / item[1]
+ _add_picture(slide, img_path, top=cur_top)
+ cur_top += Inches(3.6)
+ _add_footer(slide, footer_text, dark_bg=True)
def render_content_slide(prs, slide_data, deck_dir: Path, footer_text: str = ""):
@@ -644,63 +657,102 @@ def render_content_slide(prs, slide_data, deck_dir: Path, footer_text: str = "")
_add_title_bar(slide)
_add_title_text(slide, slide_data["title"], color=RED, size=28, top=0.25,
bold=True, height=0.7)
- cur_top = Inches(1.05)
+ # F5(b): if a slide has >=4 lead/bold-prefixed blocks, de-emphasize
+ # them to black+bold (section labels) instead of red+bold (avoids a
+ # red wall on milestone-timeline slides like slide 6). Count both
+ # explicit lead blocks (**...** on own line) and plain blocks that
+ # start with **bold** inline.
+ def _is_bold_block(item):
+ if item[0] == "lead":
+ return True
+ if item[0] == "plain" and item[1].lstrip().startswith("**"):
+ return True
+ return False
+ bold_count = sum(1 for item in slide_data["body"] if _is_bold_block(item))
+ lead_color = BODY_TEXT if bold_count >= 4 else RED
+ # F1: vertical balance — pre-compute the content height to center
+ # the block between the title (bottom ~0.95") and the footer
+ # (top ~7.12"). Content starts at 1.05" by default; if the total
+ # content height is short, push it down to vertically center.
+ INC = {"lead": 0.50, "plain": 0.45, "quote": 0.55, "bullet": 0.40,
+ "ordered": 0.40, "image": 4.10, "table": 0.42, "code": 0.50,
+ "benefit": 0.75}
+ total_h = sum(INC.get(item[0], 0.40) for item in slide_data["body"])
+ content_start = 1.05
+ available = 7.12 - content_start # footer at 7.12
+ if total_h < available - 0.5 and total_h > 0:
+ offset = (available - total_h) / 2.0
+ offset = min(max(offset, 0), 1.2) # cap at +1.2"
+ content_start = round(content_start + offset, 2)
+ cur_top = Inches(content_start)
for item in slide_data["body"]:
kind = item[0]
if kind == "lead":
tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.5))
tf = tb.text_frame
tf.word_wrap = True
+ tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
- _inline_runs(p, item[1], size=18, base_color=RED)
- # make the whole lead bold-strong-red
+ _inline_runs(p, item[1], size=18, base_color=lead_color,
+ bold_color=lead_color)
+ # make the whole lead bold
for r in p.runs:
r.font.bold = True
- r.font.color.rgb = RED
- cur_top += Inches(0.5)
+ r.font.color.rgb = lead_color
+ cur_top += Inches(0.50)
elif kind == "plain":
tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.4))
tf = tb.text_frame
tf.word_wrap = True
+ tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
- _inline_runs(p, item[1], size=18, base_color=BODY_TEXT)
- cur_top += Inches(0.4)
+ _inline_runs(p, item[1], size=18, base_color=BODY_TEXT,
+ bold_color=lead_color)
+ cur_top += Inches(0.45)
elif kind == "quote":
tb = slide.shapes.add_textbox(
MARGIN_X + Inches(0.3), cur_top, CONTENT_W - Inches(0.3), Inches(0.6)
)
tf = tb.text_frame
tf.word_wrap = True
+ tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
- _inline_runs(p, item[1], size=18, base_color=GREY_TEXT)
+ _inline_runs(p, item[1], size=18, base_color=BODY_TEXT)
# italicize the whole blockquote
for r in p.runs:
r.font.italic = True
- r.font.color.rgb = GREY_TEXT
- cur_top += Inches(0.6)
+ r.font.color.rgb = BODY_TEXT
+ cur_top += Inches(0.55)
elif kind == "bullet":
# accumulate consecutive bullets into one text frame
# (handled below in a second pass; we render single here as fallback)
tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.35))
tf = tb.text_frame
tf.word_wrap = True
+ tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
p.level = item[1]
r = p.add_run()
- prefix = "• " if item[1] == 0 else ("– " if item[1] == 1 else "· ")
- r.text = prefix + _strip_inline_emphasis(item[2])
+ # F3: if the bullet text starts with →, omit the bullet glyph
+ stripped_text = _strip_inline_emphasis(item[2])
+ if item[1] == 0:
+ prefix = "" if stripped_text.lstrip().startswith("→") else "• "
+ else:
+ prefix = "– " if item[1] == 1 else "· "
+ r.text = prefix + stripped_text
r.font.name = FONT_NAME
r.font.size = Pt(18 if item[1] == 0 else 16)
r.font.color.rgb = BODY_TEXT
- cur_top += Inches(0.35)
+ cur_top += Inches(0.40)
elif kind == "ordered":
tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.35))
tf = tb.text_frame
tf.word_wrap = True
+ tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
r = p.add_run()
@@ -708,7 +760,7 @@ def render_content_slide(prs, slide_data, deck_dir: Path, footer_text: str = "")
r.font.name = FONT_NAME
r.font.size = Pt(18)
r.font.color.rgb = BODY_TEXT
- cur_top += Inches(0.35)
+ cur_top += Inches(0.40)
elif kind == "image":
img_path = deck_dir / item[1]
_add_picture(slide, img_path, top=cur_top)
@@ -721,6 +773,7 @@ def render_content_slide(prs, slide_data, deck_dir: Path, footer_text: str = "")
tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.6))
tf = tb.text_frame
tf.word_wrap = True
+ tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
r = p.add_run()
@@ -751,7 +804,7 @@ def render_deck(md_path: Path, pptx_path: Path):
is_title = (idx == 0) or data["is_title_class"] or data["title_is_h1"]
# The appendix is a content slide (rendered normally)
if idx == 0 and (data["title_is_h1"] or data["is_title_class"]):
- render_title_slide(prs, data, footer_text=footer_text)
+ render_title_slide(prs, data, deck_dir, footer_text=footer_text)
elif data["is_title_class"] and not data["title_is_h1"] and idx != 0:
# Marp _class: title on a non-H1 slide (e.g., appendix) — render as
# content but with a title-style bar. Keep it simple: content slide.