Skip to content

Commit 36b0ed3

Browse files
committed
Default decks to light navy-band style; add math rendering and thesis-defence authoring
- Flip ExportOptions.dark_mode default to False: the project deck is now the light navy-band style (white slides, navy header band, navy cover), with --dark-mode / GUI checkbox opting into the dark palette. - Render inline $...$ math as real subscripts/superscripts in pptx. - Add the thesis-deck-author task agent for oral-defence decks built from the candidate's own thesis, plus post-author-audit math/metadata passes. - Add dark-text and overflow audit scripts with regression tests. - Sync rule docs, CLI/GUI/MCP surfaces, and Sphinx docs to the new default.
1 parent 71628aa commit 36b0ed3

26 files changed

Lines changed: 2490 additions & 257 deletions

.claude/agents/rules/deck-design.md

Lines changed: 53 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,25 @@ Already pinned in `pptx.py`:
4848
| Constant | RGB | Use |
4949
|---|---|---|
5050
| `_BRAND_DARK` | `#1F3A66` (deep navy) | Primary text + accent bar |
51-
| `_BRAND_HIGHLIGHT` | `#0E7490` (teal-700) | Text emphasis — KPI values, RQ question callout, "this stands out" headlines. The sanctioned replacement for the banned red accent. |
51+
| `_BRAND_HIGHLIGHT` | `#2563EB` (academic blue-600) | Text emphasis — KPI values, RQ question callout, "this stands out" headlines. The sanctioned replacement for the banned red accent. Same blue family as `_BRAND_DARK` navy for a cohesive white + blue academic look. |
5252
| `_BRAND_ACCENT` | `#C0392B` (warm red) | BANNED for text (see "No red text" contract). Reserved for potential future non-text accent shapes only. |
5353
| `_BRAND_GREY` | `#555555` | Metadata, captions, secondary text, placeholder/error states |
5454
| `_BRAND_LIGHT` | `#AAAAAA` | Rule lines, dividers |
5555

5656
Do NOT introduce new brand colours casually — every additional colour
5757
fights for attention. Reuse the five above unless the user explicitly
58-
adds one. Note the deliberate split: **teal is the headline emphasis,
59-
grey is the label/chrome emphasis** — picking the wrong one (e.g. teal
58+
adds one. Note the deliberate split: **blue is the headline emphasis,
59+
grey is the label/chrome emphasis** — picking the wrong one (e.g. blue
6060
for a figure caption) makes captions compete with KPIs for the eye.
6161

62-
#### Dark-mode palette (default; opt-out with `dark_mode=False` / `--light-mode` / GUI "Light mode")
62+
#### Dark-mode palette (opt-in with `dark_mode=True` / `--dark-mode` / GUI "Dark mode")
6363

64-
**Dark mode is the project default.** OLED projectors and low-light
65-
presentation venues are the common case; bright-white slides glare
66-
under both. The exporter builds with the light palette first, then
67-
runs `_apply_dark_mode(prs)` as a post-build pass.
64+
**The light navy-band deck is the project default; dark mode is opt-in.**
65+
(It used to be the reverse — the default flipped when the light navy-band
66+
chrome landed.) The exporter always builds with the light palette first,
67+
and only when dark mode is requested runs `_apply_dark_mode(prs)` as a
68+
post-build pass — useful for OLED projectors and low-light venues where
69+
bright-white slides glare.
6870
The pass re-colours individual runs / shape fills / cell borders by
6971
looking up their current RGB in two mapping dicts. No builder needs
7072
to know about dark mode at construction time.
@@ -75,7 +77,7 @@ to know about dark mode at construction time.
7577
| `_BRAND_DARK` text | `#1F3A66``#E5E7EB` | Body text near-white |
7678
| `_BRAND_GREY` text | `#555555``#9CA3AF` | Metadata mid grey |
7779
| `_BRAND_LIGHT` text | `#AAAAAA``#6B7280` | Subtle dividers / page numbers |
78-
| `_BRAND_HIGHLIGHT` text | `#0E7490``#2DD4BF` | Teal-700teal-400; brighter teal reads on the dark slide bg without losing the accent identity |
80+
| `_BRAND_HIGHLIGHT` text | `#2563EB``#60A5FA` | Blue-600blue-400, brighter blue reads on the dark slide bg without losing the accent identity |
7981
| `_BRAND_ACCENT` | `#C0392B` (unchanged) | BANNED for text in both modes (see "No red text" contract). If reused for a non-text shape, kept as-is for brand consistency. |
8082
| `_BRAND_DARK` fill (accent bars / table header) | `#1F3A66``#3B5AA0` | Lighter navy reads against the dark slide background |
8183
| `_TABLE_ROW_ALT` | `#F4F6F9``#1F232C` | Dark stripe |
@@ -141,9 +143,9 @@ manual inspection of a single rendered deck.
141143
constant ``_BRAND_ACCENT`` (= ``#C0392B`` warm red) stays in the palette
142144
for potential future non-text accent shapes (sparkline highlight,
143145
status badge, etc.), but every TEXT call site has been migrated off it.
144-
The sanctioned text-emphasis colour is **``_BRAND_HIGHLIGHT`` (teal-700,
145-
``#0E7490``)** — pair with ``run.font.bold = True``. Use ``_BRAND_GREY``
146-
for chrome / label / placeholder emphasis (never tealteal is reserved
146+
The sanctioned text-emphasis colour is **``_BRAND_HIGHLIGHT`` (academic
147+
blue-600, ``#2563EB``)** — pair with ``run.font.bold = True``. Use ``_BRAND_GREY``
148+
for chrome / label / placeholder emphasis (never blueblue is reserved
147149
for "this matters", grey is for "this is context").
148150

149151
Why banned:
@@ -164,8 +166,8 @@ is closest at hand. The four migrated sites split:
164166

165167
| Call site | Role | Replacement |
166168
|---|---|---|
167-
| KPI value (`_add_kpi_lines`) | "the slide's punch line" headline | `_BRAND_HIGHLIGHT` (teal) |
168-
| RQ question (`_add_rq_result_slide`) | "the question being answered" headline | `_BRAND_HIGHLIGHT` (teal) |
169+
| KPI value (`_add_kpi_lines`) | "the slide's punch line" headline | `_BRAND_HIGHLIGHT` (blue) |
170+
| RQ question (`_add_rq_result_slide`) | "the question being answered" headline | `_BRAND_HIGHLIGHT` (blue) |
169171
| Paper-table caption (`_add_paper_table_slides`) | caption label below subhead | `_BRAND_GREY` (muted) |
170172
| Figure-unavailable fallback (`_add_figure_image`) | placeholder / error state | `_BRAND_GREY` (muted) |
171173

@@ -176,7 +178,7 @@ Implementation contract:
176178
3. For emphasis on a value (e.g. a KPI number) use:
177179
``run.font.bold = True`` + ``run.font.color.rgb = _BRAND_HIGHLIGHT``.
178180
4. For caption / placeholder / chrome text, use ``_BRAND_GREY`` — not
179-
teal, not navy. Reserving teal for headlines is what makes headlines
181+
blue, not navy. Reserving blue for headlines is what makes headlines
180182
actually read as headlines.
181183
5. Regression test ``test_pptx_no_red_text_runs`` walks every run on
182184
a default-rendered deck and fails if any run uses ``#C0392B``.
@@ -185,7 +187,7 @@ Implementation contract:
185187
pass wouldn't quietly map it; the run would carry red through to
186188
the dark deck where the regression test fires.
187189
7. The audit script's ``_ACCEPTED_DARK_RUN_COLORS`` set includes the
188-
dark-mode teal variant ``#2DD4BF``; if you introduce another accent
190+
dark-mode blue variant ``#60A5FA``; if you introduce another accent
189191
colour, update both the map AND the audit set in the same commit.
190192

191193
If a future "non-text accent" use of red comes up (e.g. a tiny status
@@ -225,14 +227,14 @@ stayed light. White-on-white. Fixed by adding the mapping
225227
reports failure-mode B — run it on a rendered deck during manual
226228
inspection.
227229

228-
Exposure surfaces (dark is default; the toggles flip to LIGHT):
229-
- CLI: `--light-mode` opt-out flag (when absent → dark)
230-
- GUI: Deck tab `deck.light_mode_label` checkbox (unchecked → dark)
231-
- Programmatic: `ExportOptions(dark_mode=False)` to opt out
232-
- Regen script: pass `dark_mode=False` per variant — see
233-
`scripts/regen_speculative_decoding_zh_tw.py` which ships both the
234-
default dark deck (`<key>-zh-tw.pptx`) and a light opt-out
235-
(`<key>-zh-tw-light.pptx`).
230+
Exposure surfaces (light is default; the toggles opt IN to DARK):
231+
- CLI: `--dark-mode` opt-in flag (when absent → light)
232+
- GUI: Deck tab `deck.dark_mode_label` checkbox (unchecked → light)
233+
- Programmatic: `ExportOptions(dark_mode=True)` to opt in
234+
- Regen script: pass `dark_mode=` in `ExportOptions` per variant —
235+
`scripts/regen_fang2026.py` is the worked example (it sets
236+
`dark_mode=True` for the dark variant). The default omits the field
237+
(or passes `dark_mode=False`) for the light navy-band deck.
236238

237239
### Table styling (the second-biggest "AI-generated" tell after Calibri)
238240

@@ -269,16 +271,27 @@ flows through this single helper, so the change applies uniformly.
269271

270272
### Accent geometry (the "this is a designed deck" tell)
271273

272-
Every content slide gets a thin top accent bar:
273-
- Position: `left=0, top=0, width=_SLIDE_WIDTH (13.333"), height=Inches(0.08)`
274-
- Fill: `_BRAND_DARK` solid
275-
- Name: `accent_top` (semantic name so `pptx_edit` can target it)
276-
277-
The cover slide gets a left vertical band:
278-
- Position: `left=0, top=0, width=Inches(0.4), height=_SLIDE_HEIGHT (7.5")`
279-
- Fill: `_BRAND_DARK` solid
280-
- Name: `accent_left`
281-
- Cover textboxes shift right by `Inches(0.4)` worth of margin to clear it.
274+
Every content slide gets a **full-width navy header band** (the deck's
275+
signature chrome) with the white title sitting inside it and a thin blue
276+
accent rule along its bottom edge:
277+
- Band position: `left=0, top=0, width=_SLIDE_WIDTH (13.333"), height=_HEADER_BAND_HEIGHT (1.18")`
278+
- Band fill: `_BRAND_DARK` solid; name `accent_top` (kept on the band so
279+
`pptx_edit` / audits still target the content-slide accent by name even
280+
though it's now a band, not a hairline).
281+
- Accent rule: `left=0, top=1.18", width=_SLIDE_WIDTH, height=_ACCENT_RULE_HEIGHT (0.06")`,
282+
fill `_HEADER_ACCENT_FILL` (= `_BRAND_HIGHLIGHT` blue); name `accent_rule`.
283+
- The title (`_new_section_slide`) is WHITE (`_HEADER_TITLE_FG`, the same
284+
white as `_TABLE_HEADER_FG`) and middle-anchored inside the band — navy
285+
`_BRAND_DARK` would be navy-on-navy = invisible. `_BODY_TOP` (1.5") sits
286+
below the band so body content never moves.
287+
288+
The cover slide gets a **full-bleed navy panel** (not a left band):
289+
- Position: `left=0, top=0, width=_SLIDE_WIDTH, height=_SLIDE_HEIGHT (7.5")`
290+
- Fill: `_BRAND_DARK` solid; name `accent_left` (kept for the cover-accent
291+
semantic name even though it now spans the whole slide).
292+
- Cover title is WHITE; subtitle / meta are near-white (`_DARK_BODY_TEXT`) —
293+
these light colours are correct in BOTH modes because the cover stays
294+
navy either way.
282295

283296
Section-divider slides may use a larger top band (`height=Inches(0.6)`)
284297
with the section title overlaid in light text — but this is optional
@@ -314,26 +327,26 @@ provided every slide ends up with:
314327

315328
The exporter inserts figures as PNGs via the `figures=` field (`_add_figure_image`); it does **not** draw native charts. So figure *quality* is an authoring responsibility — a default-matplotlib plot or a low-res screenshot undoes the brand discipline the rest of the deck earns.
316329

317-
- **Dark-mode adaptation is mandatory.** The slide background is `_DARK_SLIDE_BG` (`#12151B`) by default. A white-background PNG dropped onto it shows a glaring white rectangle — the figure equivalent of `rgb=None` text on dark. Export plots with a **transparent background** (`savefig(..., transparent=True)`) and light foreground (axes / labels / lines in near-white or brand teal `#2DD4BF`), OR place the figure on a card whose fill has a `_LIGHT_TO_DARK_FILL` entry. Never a bare white PNG on the dark slide.
330+
- **Dark-mode adaptation is mandatory when the deck is rendered dark.** In dark mode the slide background is `_DARK_SLIDE_BG` (`#12151B`). A white-background PNG dropped onto it shows a glaring white rectangle — the figure equivalent of `rgb=None` text on dark. Export plots with a **transparent background** (`savefig(..., transparent=True)`) and light foreground (axes / labels / lines in near-white or brand blue `#60A5FA`), OR place the figure on a card whose fill has a `_LIGHT_TO_DARK_FILL` entry. Never a bare white PNG on the dark slide.
318331
- **Strip chartjunk.** No default matplotlib grey panel, no spines on all four sides, no dense gridlines, no 3-D bars / pies, no drop shadows. Top + right spines off, at most one light horizontal gridline set. Data-ink first.
319-
- **Brand palette, not library defaults.** Series colours come from the deck palette (navy / teal / grey), never matplotlib's `C0` blue / `C1` orange — default colours read as "pasted from a notebook". (Red stays banned here too, per the no-red contract.)
332+
- **Brand palette, not library defaults.** Series colours come from the deck palette (navy / blue / grey), never matplotlib's `C0` blue / `C1` orange — default colours read as "pasted from a notebook". (Red stays banned here too, per the no-red contract.)
320333
- **Don't encode meaning by colour alone.** Teal vs navy is hard for some colour-blind viewers and *indistinguishable* in a black-and-white printout. When two series must be told apart, encode them twice — colour **plus** a marker shape / line style (solid vs dashed) or a direct end-of-line label. The winning series can also be the only solid/heavy one. (The 4-colour brand palette is small precisely so it can't carry many simultaneous distinctions — lean on shape and labels.)
321334
- **Readable when projected.** Axis labels + tick labels + legend ≥ ~14pt *in the rendered figure* (a 6pt matplotlib label is unreadable from row 10). Label every axis with its quantity AND unit ("Latency (ms)"), per `paper_rule`'s number-reporting rule.
322335
- **Export at print DPI.** `dpi >= 150` (200 for line-heavy plots). A 72-DPI screenshot pixelates on a projector.
323336
- **Paper screenshots are a last resort.** Re-plotting your own data beats screenshotting the paper's figure — a screenshot carries the paper's off-brand fonts / colours, JPEG artefacts, and usually a white background. Crop tightly; only screenshot when re-plotting is impossible (e.g. a qualitative architecture diagram).
324337

325-
**Anti-pattern:** `plt.savefig("fig.png")` with defaults → grey panel, blue/orange series, 6pt labels, white border, dropped onto the dark slide. **Pattern:** `savefig("fig.png", dpi=200, transparent=True, bbox_inches="tight")` with teal / navy series, 14pt labels, top + right spines removed.
338+
**Anti-pattern:** `plt.savefig("fig.png")` with defaults → grey panel, blue/orange series, 6pt labels, white border, dropped onto the dark slide. **Pattern:** `savefig("fig.png", dpi=200, transparent=True, bbox_inches="tight")` with blue / navy series, 14pt labels, top + right spines removed.
326339

327340
### Visual hierarchy & focal point
328341

329342
Each slide needs one element the eye lands on first — the takeaway from `slide-deck-rules` §9. Size, weight, colour and position build that hierarchy; without it every element competes and the audience reads top-to-bottom hunting for the point.
330343

331-
- **One focal point per slide.** The biggest / boldest / most-saturated element *is* the takeaway — usually the KPI value (teal, bold, large) or the winning row of a table. Exactly one.
332-
- **Hierarchy by size, not just order.** Title > headline number > evidence > caption, each visibly smaller. A KPI value at the same size as its label has no hierarchy. Caption / provenance text uses `_BRAND_GREY` so it recedes — the palette already encodes this (teal emphasises, grey recedes); don't invert it.
344+
- **One focal point per slide.** The biggest / boldest / most-saturated element *is* the takeaway — usually the KPI value (blue, bold, large) or the winning row of a table. Exactly one.
345+
- **Hierarchy by size, not just order.** Title > headline number > evidence > caption, each visibly smaller. A KPI value at the same size as its label has no hierarchy. Caption / provenance text uses `_BRAND_GREY` so it recedes — the palette already encodes this (blue emphasises, grey recedes); don't invert it.
333346
- **Whitespace is not wasted space.** A slide filled edge-to-edge has no focal point. Leave margins and let the KPI block breathe. The `FOOTER_GUARD` (7.05") and per-slide content caps exist partly so content can't sprawl across the whole canvas.
334347
- **Reading order follows the layout.** Assertion title on top, evidence beneath it, provenance / caption last. Don't bury the conclusion in a footnote while the setup sits in the headline.
335348

336-
**Anti-pattern:** title, three KPIs, a table and a caption all the same size and colour — no focal point, the eye wanders. **Pattern:** one KPI value ~2× the size of its label in teal, the table muted beneath it, caption small and grey.
349+
**Anti-pattern:** title, three KPIs, a table and a caption all the same size and colour — no focal point, the eye wanders. **Pattern:** one KPI value ~2× the size of its label in blue, the table muted beneath it, caption small and grey.
337350

338351
## Anti-patterns (instant "AI-generated" tells)
339352

0 commit comments

Comments
 (0)