# Teaching Material Repository.

The aim is to create a reusable, well-organised teaching resource repository that supports both traditional course materials and lightweight interactive web-based teaching tools. The Teaching Demos should be easy to embed, visually consistent, accessible, and reusable across multiple courses or teaching contexts.

The repository contains two main types of content. **Content is arranged by the kind of thing it is first, and by topic second.**

1. Teaching Presentations
Taught content — lectures and similar — authored as markdown and built into reveal.js decks by the slide system in framework/.
2. Teaching Demos
These are discrete, interactive, web-based teaching tools. They are intended to be embedded inside teaching presentations, web pages, or learning platforms.

The repository should be organised into clear sections for:

* teaching-presentations/
For taught content. Below it, a folder per topic (cfd/, heat-transfer/, ...), and below that a folder per lecture. Shared figures live in images/, not here; only a figure that genuinely belongs to one lecture alone sits beside it.
* teaching-demos/
For standalone interactive web-based teaching demos. The same topic folders (cfd/, heat-transfer/, ...), so a lecture and the demos it embeds sit at mirrored paths.
* framework/
For shared styling, guidance, and reusable design assets.
* images/
For shared figures. Flat, never topic-subfoldered, because the images that matter most are the ones several subjects want. Every file needs an entry in images/images.json giving its description, source and licence. Referenced from a deck as /images/filename.png — the leading slash means "from the site root" and the builder resolves it.

A new *kind* of content (worksheets, assessments) earns its own top-level folder rather than being filed under an existing one that does not describe it.

Two folders sit outside all of the above and are not governed by this file's guidance:

* cfd_tutorials/ — backups of Ansys lab sheets authored in the separate MEE Labs 2.0 system. Reference only. Not built, styled or maintained by this repository's framework.
* non-teaching/ — unrelated material.

## Framework
This repository will be built by creating content while simultaneously identifying the framework elements that need to be included. The framework will not be built in its entirety at the start. Therefore, it is critical to ensure that structure is maintained within the framework and that elements are not duplicated or unnecessarily added overall.

Any part of this claude.md file, including these header instructions, can be updated as the project progresses. 

When creating or modifying content in this repository:

* Preserve the separation between teaching content and embeddable interactive demos.
* Do not duplicate CSS unnecessarily inside individual demos if a reusable style exists in agTeachingDemos.css.
* If a new reusable style pattern is needed, propose adding it to the shared framework rather than applying inconsistent local styling.
* Use clear file names that describe the teaching topic or demo purpose.
* Keep assets organised in predictable folders.
* Prefer simple, robust implementations over complex ones.

## Teaching Demos

Inside the framework/ folder there is a CSS file called: agTeachingDemos.css
This contains reusable style definitions that should be used by all Teaching Demos to ensure a consistent look and feel.

There is also a guide file: agTeachingDemosGuide.html
This explains how the CSS framework should be used when creating new Teaching Demos.

Teaching Demos should be designed as embeddable components. They should avoid unnecessary surrounding page context, such as full-page titles, headers, navigation bars, footers, or explanatory material that belongs in the surrounding teaching content.

Each Teaching Demo should focus on a single concept, interaction, model, visualisation, or learning activity.

Teaching Demos should:
* Use the shared CSS definitions in framework/agTeachingDemos.css.
* Follow the design guidance in framework/agTeachingDemosGuide.html.
* Have a consistent visual style across the repository.
* Be self-contained enough to function when embedded in other teaching material.
* Avoid hard-coded assumptions about where they will be embedded.
* Be responsive and usable on different screen sizes.
* Be accessible, including keyboard operation where relevant, readable colour contrast, semantic HTML, and useful labels for screen readers.
* Keep explanatory text concise, since fuller explanation may be provided in the surrounding teaching material.
* Use clear, maintainable HTML, CSS, and JavaScript.

## Teaching Presentation Requirements

Teaching presentations may include fuller context, explanations, headings, learning outcomes, instructions, references, and links to embedded Teaching Demos — the fuller explanation a demo deliberately leaves out belongs here. They are written in the slide markdown format, which documents itself in framework/template.md; copy that file to start a deck. Presentations should be structured clearly and consistently. Where appropriate, they should include:

* A short description of the topic.
* Intended audience or level.
* Learning outcomes.
* Prerequisite knowledge.
* Instructions for students.
* Notes for educators.
* Links to relevant Teaching Demos.
* Any required files, data, or assets.

## Golden Rules
These rules apply to every project. They sit above all project-specific instructions.

1. Assume I'm a generalist, not an expert
I tend to know a little about a lot of things. If I suggest a particular approach, don't just follow it — tell me how an expert in that field would tackle the same problem. Flag best practices, common pitfalls, and established patterns I might not be aware of. Help me learn as we go, not just ship.
2. Everything is meta
Treat every specific problem as a representative of a broader class of problems. If we're solving X, help me understand the general shape of "problems like X" — so the solution is durable, transferable, and not just a one-off fix.
3. Record important stuff as we go
In code: Comment generously. Assume the next reader (probably me, six months from now) will need granular explanation of what the code does and why it does it that way. Don't assume intent is obvious.
In this file: Keep this CLAUDE.md updated with key design decisions, architectural choices, and the reasoning behind them as the project evolves. This file is the project's memory — treat it that way.
4. Validate everything — function and fit
Before calling something done, check it on two dimensions:
Does it work? Does the system behave correctly and robustly?
Does it work for people? Consider the full range of users. Is it inclusive, accessible, and appropriate for its context? If the audience includes students, is it pedagogically sound and clear? If it's public-facing, is it welcoming and respectful of different backgrounds and abilities?
Good enough technically is not good enough if it fails the people using it.
5. Don't be a dick
Assume everyone involved — me, users, stakeholders, future maintainers — is doing their best. Lead with generosity, not judgment. People come first in everything we build together. Write code, make decisions, and give feedback as if the person on the other end of it matters. They do.
6. Build secure stuff
Assume everything you build is going to be attacked by students looking to disrupt my processes. Consider where exploits could exist. Run checks on outputs to validate that there are no places where security could be compromised. 

 <!-- Project-specific context begins below this line --> 

## Design decisions log

* **2026-07-07 — agTeachingDemos.css organisation.** The file is split into topic sections (GENERAL, MATERIALS, more as needed). Each section is self-contained: its own `:root` block holding its variables, followed by its classes. No global variables block at the top of the file. All class and variable names carry the `ag-` prefix to avoid collisions with host-environment CSS when demos are embedded.
* **2026-07-07 — SVG styling approach.** Demos use inline SVG (not `<img src="*.svg">`, which page CSS cannot style). SVG elements carry no colours of their own — semantic classes only; all appearance lives in the framework CSS.
* **2026-07-07 — materials are pure fills; outline is a toggle.** Material classes (`.ag-metal`, `.ag-water`, `.ag-air`, `.ag-insulation`) set fill only. Outlining a region is a separate semantic choice made per element by adding `.ag-outline` (colour/weight from `--ag-c-outline`/`--ag-sw-outline`, material-specific, independent of GENERAL line weights). There is no solid/fluid distinction.
* **2026-07-07 — shiny metal via optional gradient defs.** `.ag-metal` uses `fill: url(#agMetalGradient) var(--ag-c-metal)`. A standard `<defs>` gradient block (documented in agTeachingDemosGuide.html) is pasted into an SVG to get the sheen; without it the fill falls back to flat grey automatically.
* **2026-07-07 — ARROWS and GRAPHS sections added.** Arrows are a stroked line/path plus a separate filled polygon head (`.ag-heat-arrow` + `.ag-heat-arrow-head`) rather than SVG `<marker>`s, because browsers cannot reliably re-colour marker content from CSS and that would break single-file theming. The heat arrow's marching-dash speed is per-element overridable via the `--ag-heat-arrow-speed` custom property (demos can tie it to physical magnitude), and all arrow animation stops under `prefers-reduced-motion` for accessibility. GRAPHS defines `.ag-axis`, `.ag-axis-label`, `.ag-gridline` ("gridline" is the standard charting term for light reference lines, even irregularly spaced ones). MATERIALS gained `.ag-brick`.
* **2026-07-07 — PARTICLES, SLIDERS sections; GRAPHS gains curves and labels.** Promotions from the conduction-physics demo: `.ag-particle` (own PARTICLES section — particles will recur in gas/convection demos, so not graph- or material-specific; framework styles the dot, motion is the demo's JavaScript); GRAPHS gains `.ag-default-curve1` (dark red) / `.ag-default-curve2` (dark blue) — the default curve colours unless a demo states otherwise — and `.ag-graph-label` for annotation text that is not an axis label; new SLIDERS section (`.ag-controls`, `.ag-readout`) styles the HTML control panel — the one section styling HTML rather than SVG; markup contract is `<label for>` / `<input id>` pairs for accessibility. Shared font stacks `--ag-font-ui` / `--ag-font-mono` live in GENERAL because multiple sections set text (avoids duplicated font stacks).
* **2026-07-07 — particles fully parameterised.** `.ag-particle` now defines size, body colour, outline colour and outline width, all as variables demos can override. The radius is set from CSS (the `r` property overrides any `r=""` attribute — resize via `--ag-particle-r`, never the attribute). Optional shiny 3-D ball look via a standard `agParticleGradient` radial-gradient defs block (same opt-in pattern as shiny metal); its highlight and shade are derived from the single `--ag-c-particle` variable using `color-mix()`, so one override retints the whole ball. Limitations, by design: with the gradient active, colour is per-SVG (shared gradient), while flat particles can be recoloured per element; `color-mix()` needs 2023+ browsers, older ones get flat particles.
* **2026-07-07 — conduction demo: slider is total heat rate Q, not flux q&Prime;.** Having both q&Prime; and A as inputs made A physically inert (and silently changed Q behind the scenes). Resolved by imposing Q and computing q&Prime; = Q/A, shown live in the relation box. This makes the area slider a demonstration of the heat-rate vs heat-flux distinction — the most common student misconception here: growing A at fixed Q visibly drops the flux, flattens the gradient and calms the particle jiggle. Arrow marching speed encodes q&Prime; (not Q), identical at both ends because steady state. Slider extremes chosen so max Q, max L, min A, min k puts the hot end exactly at the graph top. The alternative (removing A for a leaner three-slider demo) was considered; if the demo ever feels overloaded, clone a stripped version rather than un-teaching Q/A.
* **2026-07-07 — conduction demo stripped to pure visuals.** Slider numeric readouts, the "insulation" and "ambient" labels, and the q&Prime;/dT-dx relation box were all removed: the demo is qualitative and that explanation belongs in the surrounding teaching material, not the embedded component. Pattern for future demos: sliders carry a word label only; equations and terminology live in the teaching material that embeds the demo.
* **2026-07-07 — conduction demo: Q and &Delta;T are linked two-way sliders.** Both sliders are handles on the single relation Q = k·A·&Delta;T/L: moving either drives the other's position plus the figure. "Last-touched wins": whichever of Q/&Delta;T the user moved last is the imposed boundary condition and holds fixed when k, L or A change — letting students discover fixed-flux vs fixed-temperature boundary behaviour. Implementation notes: a slider's `input` event does not fire on programmatic `.value` assignment, so two-way linking cannot loop; when &Delta;T leads, Q can hit its range limits and both values clamp to the last consistent pair (a linked mechanism with a hard stop); when Q leads no clamp is needed because the ranges were sized so worst-case &Delta;T is exactly 10.
* **2026-07-07 — conduction demo: jiggle strictly proportional to absolute temperature.** Particle vibration amplitude was baseline + scale (affine); now it is exactly proportional to the absolute local temperature, interpolated linearly along the bar like the T(x) line. This makes the two visualisations one consistent statement: the graph's x-axis is absolute zero (zero jiggle), and the dashed ambient line's height above the axis corresponds to the cold-end baseline jiggle. Constants: `JIG` px of amplitude per temperature unit; ambient derived from the graph geometry, not set independently.
* **2026-07-07 — motion policy: essential vs decorative animation.** Discovered when particles appeared frozen: the user's own Windows machine has "Animation effects" off (Settings &gt; Accessibility &gt; Visual effects), which browsers surface as `prefers-reduced-motion: reduce`. Key fact: that media query is an honour system — it stops nothing by itself; pages must opt in (most, like typical matter.js sites, never do). Decision after weighing three options: **physics-carrying motion is essential content and always animates** (WCAG 2.3.3 exempts essential animation — the particle jiggle IS the lesson), while **decorative motion respects the preference** (the arrows' marching dashes stop via the framework CSS `@media` rule). An interim "frozen disorder snapshot" fallback (static displacement &prop; local temperature) was built and then removed in favour of this policy; worth resurrecting if a future demo's essential motion is large/sweeping enough to be a genuine vestibular concern. Apply this essential/decorative split to all future animated demos.
* **2026-07-08 — 1D_conduction_sim.html: the quantitative sibling.** Same figure as the qualitative demo minus the particles, real SI units on everything, A fixed at 1 m&sup2; (labelled on the slab, so Q and q&Prime; are numerically equal — deliberately showing both). Design choices: conductivity is a **dropdown of real materials** (copper 400 &rarr; silica aerogel 0.02 W/m&middot;K, Incropera ~300 K values) because 4.5 decades is unusable on a linear slider; Q gets a **logarithmic slider** (1 W&ndash;1 kW) for the same reason; an "impose &Delta;T / impose Q" radio toggle shows one slider and displays the other quantity; the graph x-axis is fixed 0&ndash;1 m with the slab drawn to scale, and the **y-axis autoscales** to a nice 1/2/5&times;10&#8319; ceiling above the hot-face temperature — insulators under imposed Q legitimately reach thousands of kelvin and the axis follows, because that IS the lesson. Cold face pinned at 20 &deg;C ambient.
* **2026-07-08 — demo sizing for iframe embedding.** Both conduction demos were unbounded and overflowed short windows (the graph fell below the fold). Pattern for all demos: the component gets an overall `max-width` (~860px); the SVG gets a smaller flex-basis (340px), a `max-width` (~600px), **and `max-height: 85vh`** — inside an iframe `vh` is the iframe's own height, so the figure shrinks to fit whatever box the host page provides instead of clipping. ViewBoxes trimmed of dead bottom space. Suggested host embed size ~840&times;460 for these two demos.
* **2026-07-08 — `.ag-axis-label-vertical` modifier added to GRAPHS.** Y-axis titles were colliding with tick labels (seen on the 1D sim). The fix is a framework *modifier* class stacked on `.ag-axis-label`: it only rotates (90&deg; anticlockwise about the text's own centre via `transform-box: fill-box`), so font/colour stay defined once on the base class. Usage: position the text at its desired centre point. The guide's example graph now demonstrates it; prefer this over per-element `transform="rotate(...)"` attributes.
* **2026-07-09 — convection_physics.html: qualitative convection explainer.** Air particles enter cold (blue) on the left, advect over a heated plate, and are coloured individually blue&rarr;red by their temperature — which is exactly why this demo deliberately OMITS the `agParticleGradient` defs block: the shiny gradient is shared per-SVG and cannot tint individual particles, so flat particles with per-element `--ag-c-particle` overrides are required. The motion encodes real boundary-layer physics qualitatively: no-slip (speed grows with height above the plate), a thermal boundary layer (only particles within ~45px of the surface heat up, at a rate fading with distance), and buoyancy (hot particles rise, forming a plume that exits right). The A slider (0.1&ndash;1 m&sup2;, maths kept internal — no numbers shown, per the qualitative-demo pattern) sets the plate length; a longer heating run makes the exit plume hotter and taller. T&#8734; is fixed (inlet always cold). An h control is planned (user has a specific idea); the natural hook is `HEAT_RATE` (and possibly `VX_FREE`), noted in the code. Solid plate keeps the conduction demo's vocabulary: metal slab with two rows of hot red strongly-jiggling lattice particles.
* **2026-07-09 — convection demo: full circulation + surface-temperature slider.** The one-way "heat up and rise" picture became a circulation: vertical velocity is temperature-set in BOTH directions — hot particles rise &prop; how hot they are, cold particles sink &prop; how cold they are (displaced by the rising warm air) — so fresh cold air keeps reaching the surface and convection covers the whole plate. A "surface temperature" slider (20&ndash;100 &deg;C internal, no numbers shown) maps to sHot &isin; [0,1]: boundary-layer particles relax toward the SURFACE temperature (not blindly to max, so a cooler surface heats less and can even cool overheated particles), and the entire circulation strength scales with sHot — at 20 &deg;C (= fixed T&#8734;) heating, buoyancy and sinking all vanish naturally rather than as a special case. The solid lattice's colour and jiggle amplitude also track the surface temperature (blue and calm at ambient). Air block resized to match the plate length, with the active particle count scaled to keep density constant. h control still pending (user's "novel idea"); hook remains HEAT_RATE / VX_FREE.
* **2026-07-09 — convection demo: heat-rate arrows off the surface.** Vertical `.ag-heat-arrow`s march from the plate's top surface up into the air, encoding Q = h&middot;A&middot;&Delta;T across three visual channels: arrow COUNT scales with plate length (one per ~95px, so more area = more arrows), marching SPEED scales with &Delta;T via `--ag-heat-arrow-speed` set on the group (h fixed until the planned h control), and at T&#8347; = T&#8734; the arrows are hidden entirely (visibility, not speed-zero — a stopped dashed arrow would still assert a heat-flow direction when there is none). Drawn before the particle groups so air particles pass in front. No framework CSS changes needed.
* **2026-07-09 — convection demo: h built from two sliders.** The user's design: h has two deliberately jargon-free components — "fluid mechanics" and "material properties" (internally Reynolds- and Prandtl-flavoured) — each a slider spanning 0.25&ndash;0.5, summed to give h &isin; [0.5, 1]. Additive combination is a knowing simplification (real correlations are multiplicative, Nu &prop; Re&#775;&#7511;&middot;Pr&#8319;; flagged to the user, their call). h multiplies the heat flux: particle heating rate and arrow marching speed. Each component has a visible physical tell: fluid mechanics scales the whole flow speed &plusmn;25%; material properties scales the air-particle radius &plusmn;25% via the framework's `--ag-particle-r` group override (no CSS changes). The combined h shows on a **read-only meter** — a filled bar with no thumb (the missing thumb signals "display, not control"), `role="meter"` for screen readers. Meter styling was demo-local at first; promoted to the framework the same day (see next entry).
* **2026-07-09 — read-only meter promoted to the framework.** SLIDERS section gains `.ag-meter` / `.ag-meter-fill` / `.ag-meter-label` (label shares the `.ag-controls label` rule via a combined selector — no duplicated styling). It is the standard for "a value the user cannot adjust", e.g. a quantity computed from other sliders: a filled bar with NO thumb (the missing thumb is the display-not-control signal). Markup contract: `role="meter"` + `aria-valuemin/max/now`, with aria-valuenow updated from JS together with the fill width. Variables: `--ag-c-meter-track`, `--ag-c-meter-border`; fill uses `--ag-c-control-accent`. Documented in the guide with a rendered example; convection demo migrated onto it. Remaining framework candidates: results box, dropdown styling, radio-row styling (all local to 1D_conduction_sim.html).
* **2026-07-09 — convection demo: Q shown on a speedometer-style dial.** The output quantity Q = h&middot;A&middot;&Delta;T gets an instrument of its own (user's idea; dial chosen over a second linear bar so the OUTPUT reads differently from the input controls): a small inline SVG gauge in the control panel — semicircular arc with five ticks (framework `.ag-axis`/`.ag-axis-label` classes), swinging needle (demo-local `.demo-needle`, coloured via `--ag-c-curve1`), labelled "0" to "max". Full deflection is well-defined because every factor is normalised to its slider maximum: qFrac = h &times; A &times; sHot &isin; [0,1]. The needle updates from all three input paths (updateH, updateTemp, rebuildPlate) and the gauge carries `role="meter"` + aria values like the h bar. Needle/hub styling is a declared framework candidate ("gauge" pattern) if it recurs in other demos.
* **2026-07-10 — conductivity_scale.html: the scale of k as physical thickness.** Answers a colleague's problem: students never *feel* the five decades of k, because the standard fix (log-axis chart) is precisely what hides the drama. The mapping: every material gets a wall passing the SAME heat, q&Prime;/&Delta;T = k/L = 0.72 W/m&sup2;&middot;K (&equiv; 1 m of common brick, a real U-value students can anchor to), so L = k/0.72 turns five decades of k into five decades of *length* — 28 mm of phenolic foam vs 557 m of copper. Eleven materials (the "Use = Y" rows of the user's thermodynamic_properties.xlsx, then softwood swapped in for vermiculite and magnesium dropped as adding nothing). Design: a fixed window pans/zooms over a true-scale scene (widths only; wall height deliberately NOT to scale — the variation lives in one dimension). Zoom slider is logarithmic (~5 decades — same fix as the 1D sim's Q slider; even the controls are forced log by the data). Engineered teaching moments: a wall under 1 px genuinely **vanishes** (leaving a coloured ground-tick, so "gone" reads as "too thin to see") — zoomed out to copper, everything up to brick has vanished; an adaptive ruler re-grades itself (1-2-5 steps, mm&rarr;m&rarr;km); "fly to" buttons animate a 1.5 s pan-and-zoom flight (the 1-pixel-wealth "journey" without the laborious scrolling — considered and rejected as annoying for students). Gaps between walls are true world distances (15% of the wider neighbour), keeping the scene one affine mapping so the ruler is honest everywhere; screen-fixed gaps were considered and dropped for exactly that reason. Opens framed on brick (the familiar baseline) with an invitation to find copper's far edge. Per the motion policy: the fly-to flight is the sweeping whole-view motion the reduced-motion preference exists for, so under `prefers-reduced-motion` it becomes an instant jump. Button styling is demo-local — a declared framework candidate.
* **2026-07-10 — MATERIALS gains .ag-wood, .ag-plastic, .ag-stone, and named tints.** New fills: `.ag-wood` (softwood tan), `.ag-plastic` (milky off-white) and `.ag-stone` (granite grey). New pattern, promoted from the conductivity demo: **named material tints** — framework variables (`--ag-c-metal-titanium/-iron/-aluminium/-gold/-copper`) used as per-element overrides of a flat material fill (`style="--ag-c-metal: var(--ag-c-metal-copper)"`). Keeps override colours consistent across demos instead of demo-local inventions; new tints are added only as demos need them (magnesium and a mineral-insulation tint were added and same-day removed when their materials left the cast). Works only on FLAT fills (per-SVG gradients ignore per-element overrides — the flat-vs-shiny particle rule), so a demo needing distinguishable metals omits the metal-gradient defs block. Gotcha documented in the guide: SVG gradient references resolve *document-wide*, so a page containing the defs block anywhere (like the guide itself) makes every `.ag-metal` shiny; the guide's tint example forces `fill: var(--ag-c-metal)` to simulate a defs-free demo. Iron's tint is dark enough that labels on it need light text (the demo uses `#fff`).
* **2026-07-10 — the Ansys lab sheets use the MEE Labsheets markdown system, not this repo's demo framework.** *(Folder is now `cfd_tutorials/`, and as of 2026-09-11 it is a backup of work that lives in another system rather than live content here — see that entry. `cfd/laminar-flow-conversion-notes.md`, referenced at the end of this entry, is no longer present.)* It holds Ansys tutorial sheets in MEE Markdown (docs: https://mee-labsheets.shef.ac.uk/docs/ — frontmatter with DoA code, `!!!` callouts, `[ui]` tags, stepped slide layout where `##` starts a step card and `#` is a title card). Conventions observed/established: one sheet per folder named like the sheet title (md + figure images together); a topic splits into (Geometry)/(Meshing)/(Fluent) sheets with one DoA code each; figure numbering runs continuously across a series. `Laminar Flow.docx` was converted this way into ACME813/814/815 ("Laminar flow in a 2D pipe" Geometry/Meshing/Fluent); items needing human review are in `cfd/laminar-flow-conversion-notes.md`.
* **2026-08-07 — lecture slides: markdown source, reveal.js output, homebrew converter.** Slides move to a markdown format with a browser-based converter emitting reveal.js. Full design in `slide-markdown-spec.md` (repo root). Headline decisions: `##` starts a slide and `#` is a section divider (matches MEE Labs 2.0 *and* Quarto — this is the portability hinge); pandoc spelling for everything bespoke (`{.class}` heading attributes, `::: {.columns}` fenced divs, `::: {.incremental}` fragments) so the files stay readable by pandoc/Quarto and a renderer swap stays cheap; HTML comments are speaker notes (matches the Labs 2.0 teacher-note convention, at the cost of losing ordinary comments and needing a find-and-replace if ever migrated); MathJax-SVG over KaTeX because KaTeX's ~20 sidecar font files break single-file offline decks; embedded teaching demos are first-class via `::: {.demo src=...}`. Quarto was evaluated properly and rejected on workflow cost (148 MB runtime on a managed desktop, build step in a save-and-refresh loop), not capability; Marp was ruled out because it breaks on `---` and not on `##`. **Resolved 2026-08-08 — output packaging and theming.** Output is **content only**: decks link to shared assets, nothing is inlined. Chosen because the decks are part of a website of interlinked teaching material uploaded as a tree; the accepted trade-off is that a deck needs the network, which is tolerable since a dropout takes out the VLE too. (Self-contained output *was* achievable — `<iframe srcdoc>` can inline a whole demo — but it was rejected as the wrong shape for a linked site, and it would have forced the converter to a directory picker in order to read images and demos.) Theme is `framework/agSlides.css`, layered *over* a stock reveal theme (`white.css`) rather than replacing it: reveal 4+ exposes `--r-*` custom properties, so the house style is mostly variable overrides and the stock theme keeps spacing, lists, tables, code and the print stylesheet. reveal.js itself goes in `framework/reveal/`. Deck-to-framework paths resolve via a `base:` front-matter field — root-relative paths were rejected because they break when a deck is opened locally by double-clicking. Local Drive structure mirrors the server so relative paths behave identically in both. Build starts by retargeting the existing `PresentationTemplate\builder\build.html` (725 lines, already has file picker, preview and marked.js), not from scratch. *(**Superseded 2026-08-08** — see the entry below; `build.html` is to be written from scratch.)*
* **2026-08-08 — slide format is documented by `framework/template.md`, not a spec file.** The template deck *is* the documentation: it demonstrates every construct and explains it in its own syntax, readable either as markdown or as the deck it generates. The separate spec and feature-list files were folded into this log and deleted, so there is one place for decisions (here) and one for format reference (the template). **Converter pipeline**, in order: strip and stash fenced code blocks → split front matter → split slides on `##` and `#` → parse heading attributes `{.class key="value"}` → parse fenced divs `::: {.name key="value"}` → capture HTML comments into `<aside class="notes">` → marked.js for the slide body → restore code blocks → assemble the reveal document. **The stash step is not optional** — `template.md` deliberately contains `:::`, `##` and `<!-- -->` inside code fences as examples, so a parser that does not stash fences first will mangle its own documentation. That also makes the template a good first test case: if it renders correctly the parser is sound. **Output structure** is flat `<section>` per slide inside `<div class="reveal"><div class="slides">`, with no nested sections since vertical slides are out of scope. **Out of scope for v1** (each addable later without breaking existing decks): vertical slides, citations/bibliography, footnotes, executable code, auto-animate, mermaid diagrams, PPTX/Word export, and writing back to disk via the File System Access API. **Build order:** ~~reveal.js into `framework/reveal/`~~ (done 2026-08-08) → write `build.html` **from scratch** → write `framework/agSlides.css` → write one real lecture and see what hurts. **All four remaining open items resolved 2026-08-08, nothing outstanding.** (1) **`. . .` block-pause marker adopted** — pandoc's spelling, three spaced dots on their own line, pausing between whole blocks where `::: {.incremental}` only handles list items; demonstrated in `template.md`. (2) **No `.img-left` shorthand — closed, not deferred.** Columns are to manage image-beside-text; do not reopen this by adding layout shorthands. (3) **No second comment marker.** Every HTML comment in the markdown is a speaker note, converted by the builder into reveal's speaker-note syntax; the consequence — that there is no way to leave a hidden non-speaker comment — is accepted deliberately, so do not add `<!--- --->` or similar. (4) **reveal.js 6.0.1 pinned and committed to `framework/reveal/`** rather than fetched from a CDN, to avoid a third-party runtime dependency and to keep old decks working when reveal 7 lands; a CDN build remains available per-deck if ever wanted. Verified 2026-08-08 that reveal 6.0.1 still exposes the `--r-*` theme custom properties (27 of them, including `--r-background-color`, `--r-main-color`, `--r-heading-color`, `--r-link-color`, `--r-main-font`, `--r-heading-font`, `--r-main-font-size`), so the layer-over-a-stock-theme approach holds against the current major version.
* **2026-08-08 — reveal.js 6.0.1 installed; `build.html` to be written from scratch.** reveal.js 6.0.1 is committed to `framework/reveal/` — 48 files, 4.9 MB, taken from the npm distribution tarball (`registry.npmjs.org/reveal.js/-/reveal.js-6.0.1.tgz`) and extracted with `tar`, **not** Windows Explorer, which silently truncates large archives (it did exactly that to a Quarto install earlier the same day — 204 of several thousand files, with no error). Only `dist/` was kept: the package's `css/` and `js/` are SCSS and ES-module *source* and are not needed on the website. `index.html` was kept as `reference-deck.html` — the canonical minimal reveal markup, i.e. a worked example of what the builder must emit. **Path change worth knowing: reveal 6 moved plugins to `dist/plugin/`**, where reveal 5 had them at the package root, so any wiring copied from older examples or tutorials will be wrong. Available plugins: `highlight`, `markdown`, `math`, `notes` (this is the one powering speaker view), `search`, `zoom`. 14 stock themes including `white.css` as chosen, plus `white-contrast.css` and `black-contrast.css` accessibility variants. **`build.html` is to be written from scratch**, reversing the earlier plan to retarget `PresentationTemplate\builder\build.html`: too much has changed — different output target, different input dialect, different asset model — for the old file to be a useful starting point. Treat it as prior art at most. The test deck `conductionslides.md` was deleted as a placeholder; `framework/template.md` is the parser's first test case instead, and a demanding one, since it contains `:::`, `##` and `<!-- -->` inside code fences.
* **2026-08-08 — `build.html` written and verified against `template.md`.** `framework/` now holds `build.html` (the converter, 67 KB, **fully self-contained**), `mathjax/tex-svg.js` (3.2.2), `agSlides.css` (deliberately empty) and `reveal/`. **marked.js 15.0.7 (MIT) is inlined into `build.html`** rather than loaded as a sibling — verified afterwards to have zero external `script src` or `link href` references, so there is no sibling to lose and no `file://` subresource loading that could be blocked. The inlined block carries a header comment with its version, source URL and upgrade instructions; the standalone `marked.min.js` was deleted as redundant. Note that marked handles *standard* markdown only — the builder itself handles slide splitting, heading attributes, fenced divs, speaker notes, pauses and definition lists (marked has no native definition lists). The builder takes a `.md` by picker or drag-drop, shows a per-slide parse report plus warnings, and downloads the deck; it reads via `<input type="file">` so it **cannot** write to the source, which is why `template.md` is safe as the source of truth. A **Rebuild** button re-reads the same `File` handle, so the loop is save-in-editor then click Rebuild with no re-picking. **Verified end to end:** 26 slides + title from `template.md`, 1 correctly skipped, **zero warnings**, reveal initialises with all five stylesheets loading, and MathJax renders 7 SVG containers from the local file (no network, no font sidecars — the reason SVG output was chosen). **The parser trap is defeated:** `template.md` contains `::: {.demo src=...}`, `##` and HTML comments inside code fences, and the build reports `demos: 0` with those fences intact, proving the stash-fences-first step works. Two behaviours worth knowing, both correct rather than bugs: **`. . .` makes everything after it a fragment** (pandoc's semantics — not just the next block, but any images, code and equations too), so pauses belong near the bottom of a slide; and **two-column slides stack** until `agSlides.css` gains `.reveal .columns{display:flex}`, which is the first rule to write. Definition lists are handled by a custom pre-pass because marked has none. One testing-only artefact: reveal's `hash: true` throws inside an `about:srcdoc` iframe (`replaceState` on an opaque origin), which affects iframe-based testing only, never a real deck.
* **2026-08-09 — slide overflow fixed: 16:9 canvas, top alignment, 30px base font.** Reported symptom was content running off the bottom of most slides. Measured against `template.md`: **16 of 27 slides overflowed**, worst by 715px on a 700px canvas. Root cause is reveal.js's **fixed-canvas model** — it scales a fixed-size slide to the window but does *not* shrink content to fit, so anything taller than the canvas is clipped. This is inherent to reveal and is the same failure mode as the old homegrown engine; switching to reveal did not solve it. Three settings had been left at their defaults and each made it worse. Fixes, with measured effect: **(1) canvas 960x700 → 1280x720** in `build.html` — reveal's default is roughly *4:3*, which both letterboxes on a 16:9 projector and wastes room; 16 → 11 overflowing. **(2) `center: false`** — centred content that overruns is clipped at *both* ends, top-aligned only loses the bottom; no change to the count but halves the damage per overflow. **(3) base font 42px → 30px** via `--r-main-font-size` in `agSlides.css` — 11 → 1. **This third one dominates: it is the single number to tune** (raise for a bigger room, lower for denser slides; everything else in reveal is sized in em so headings, lists and code rescale together). Combined result **0 of 27 overflowing**. `agSlides.css` also gained, at the same time, the `.reveal .columns{display:flex}` rule that two-column slides had always needed, `.demo-frame` sizing, smaller code/table sizing (the two content types that overflow disproportionately), definition-list styling (reveal has none at all), and two per-slide escape hatches usable as heading attributes with no parser support: **`{.smaller}`** (0.78em) and **`{.scrollable}`** (scroll rather than clip — fine for reference slides, poor for presented ones since the audience cannot scroll a projection). The file still contains **no Sheffield branding** — colours, fonts and the `.card`/`.stat`/`.grid`/`.timeline`/`.team`/`.closing` classes remain to be written.
* **2026-08-09 — four semantic blocks defined; the format now names roles, not appearances.** Prompted by the user asking whether the system had any *semantic* controls or only stylistic ones — the example being a centred bold purple standalone statement in an existing Google Slides deck, where the formatting carried a meaning ("emphasised point") that the markup did not record. Answer at the time: the **mechanism** was already semantic (`::: {.name}` and `{.class}` name a role, CSS decides appearance) but **no vocabulary had ever been defined**, so in practice everything was being done stylistically. Four blocks now exist, styled in `agSlides.css` §6 and documented in `template.md`: **`.keypoint`** (the thing to remember — centred, bold, brand violet, deliberately *unboxed* because a box would reduce rather than increase prominence), **`.question`** (something actually being asked of the room — centred italic electric violet), **`.definition`** (formal definition — left rule, pale violet wash), **`.top-tip`** (practical aside off the main line of argument — left rule, electric violet wash, with an uppercase "TOP TIP" label injected by CSS `::before`; delete that one rule if it ever feels like clutter). **Four is deliberate** — beyond about six nobody remembers which is which. Renamed from the originally proposed `.warning`, which was wrong for teaching material. Inventing further blocks needs no parser change, and the builder's existing "classes agSlides.css does not style yet" warning doubles as the to-do list. **Side effect: `agSlides.css` now carries the Sheffield palette** as `--ag-*` custom properties (a single source of truth, lifted from `PresentationTemplate/presentation_files/pres_global.css` so the two systems cannot drift), but it is applied **only** to these four blocks — body text, headings and links remain stock reveal, and deck-wide branding is still an open job. Two related gaps noted at the same time and not yet acted on: `**strong**`/`_em_` are semantic in HTML but were being used to mean "violet", and the reserved slide classes (`card`, `grid`) describe appearance rather than meaning, unlike `stat` and `timeline`. Pandoc's inline bracketed spans (`[thermal conductivity]{.term}` — the equivalent of the MEE labsheet `[term]...[/term]`) would give the same idea at word level and are roughly twenty lines of parser work, not yet implemented.
* **2026-08-09 — command-line converter added, sharing one parser with the browser tool.** `convert.js` + `convert.bat` in `framework/` give `convert slides.md [out.html] [--watch]`, so the loop can be edit-in-VS-Code, Ctrl+S, done. **They contain no parser.** `convert.js` reads `build.html`, extracts its `<script id="ag-marked">` and `<script id="ag-parser">` blocks by id, and runs them in a Node `vm` context — so the browser tool and the CLI are literally the same engine, with no build step and nothing to keep in step by hand. Verified by **identical SHA-256 over the generated deck** from both routes (`9e38e473…5c63`, 21,980 bytes) — the check to repeat after any parser change. **This required one refactor:** the parser's only browser dependency was `DOMParser`, used by `withWrapper`/`markFragments`/`markIncremental` for fragment marking. Those are now pure string operations (`mapTopLevel` tracks tag depth; `addClass` merges into any existing class attribute). Regex-on-HTML is normally wrong, and is safe *only* because the input is always marked.js output, which is well-formed — do not point those helpers at arbitrary HTML. **Reintroducing `document`, `window` or `DOMParser` into the `ag-parser` block breaks the CLI immediately**; that is the intended alarm. `build.html`'s script is now three tagged blocks (`ag-marked`, `ag-parser`, `ag-ui`) and the ids are load-bearing. **Landmine found while testing:** this machine has **node v10.16.0 (2019) on PATH** at `C:\Program Files\nodejs`, alongside the portable v24 in AppToolkit. marked 15 uses optional chaining, which v10 cannot parse, so naively taking the first node on PATH fails with a baffling `Unexpected token .`. `convert.bat` therefore version-checks every candidate, requires major >= 18, and prefers the AppToolkit node over PATH.
* **2026-08-11 — `agSlides.css` branding completed; the "no Sheffield branding" line in the 2026-08-09 entry above is superseded.** *Recorded 2026-09-11 by reading the files rather than from the session that produced them, so the **what** below is reliable and the **why** is largely not recorded.* `agSlides.css` is now ~25 KB and carries the University brand ported from the old `PresentationTemplate` system. The structural idea worth keeping is a **two-layer colour model**: &sect;0 names the raw palette, &sect;1b names *roles* (`--ag-heading`, `--ag-emphasis`, `--ag-marker`, &hellip;) which point at the palette, and every rule refers to a role and never to a palette colour directly. That indirection is exactly what lets &sect;1c switch a whole deck to **dark** by re-pointing about seven variables. **If you add a rule to this file, use a role.** The deck footer is also done — 16 rules in the stylesheet, markup emitted by `build.html`, the three University logos in `framework/assets/` (midnight-black / violet / white) swapped by `[data-theme="dark"]`, and the footer suppressed on the title slide. Note that the file's own STATUS header still lists the deck footer as unwritten: that line is stale. Genuinely still open are the slide classes `.card`, `.stat`, `.grid`, `.timeline`, `.team`, `.closing`, and the title slide's *appearance* (the builder already emits `<section class="title-slide">`, so what is missing is styling, not structure). `template.html` (the built template, kept beside `template.md`) and `framework/readme.txt` date from the same window; `readme.txt` is the plain-English orientation to hand a colleague, where this log is the reasoning.
* **2026-09-11 — repository reorganised by kind of content; `teaching-materials/` is now `teaching-presentations/`.** Top level names the *kind* of thing, topic is the level below it: `teaching-presentations/<topic>/<Lecture name>/` and `teaching-demos/<topic>/`. Both trees now carry the same topic folders (`cfd/`, `heat-transfer/`), so a lecture and the widgets it embeds sit at mirrored paths. Renamed because "materials" had drifted into a catch-all that described everything and therefore nothing, while the decks are the actual output; a future *kind* of content (worksheets, assessments) gets its own top-level folder rather than being hidden inside a vague one. **`cfd_tutorials/` was reclassified at the same time: it is a backup of lab sheets that live in the separate MEE Labs 2.0 system, kept for reference only** — not live content, not governed by this framework, and not to be tidied into the new structure. Consequences handled at the time: a deck is now **three** levels down rather than two, so front-matter `base` is `../../../`; `template.md`'s worked example and the rebuilt `template.html` were updated to match. **Worth knowing for every future deck — `Pretty convincing` was carrying `base: "../"`, inherited unchanged from `template.md` and wrong even at its old depth.** A wrong `base` is silent and total: the deck links *nothing*, so an unstyled deck means check `base` before anything else. Loose files rehomed on the same principle: the root `images/` folder (holding one lecture figure) was deleted and the figure now sits beside the lecture that uses it, matching the one-folder-per-sheet convention already used for lab sheets; `cell_refinment.xlsx` moved next to the demo it feeds, its spelling corrected to `cell_refinement.xlsx`, and the demo's source comment updated to match.
* **2026-09-11 — cfd_cell_refinement.html: what "more cells" actually buys you — and the first demo needing no framework changes at all.** One fixed flow domain (inlet 10 m/s, both walls at 0) subdivided 1&times;1 &rarr; 2&times;2 &rarr; 4&times;4 &rarr; 8&times;8 &rarr; 16&times;16 on a five-step slider, each cell carrying a single number: the horizontal velocity through its right-hand face. **The values are prescribed, not solved** — authored per resolution in `cell_refinement.xlsx` and copied in verbatim, with no coarsening, averaging or interpolation between levels, so each resolution is an honest authored statement rather than a fake solution — and the caption says so on the component. That is deliberate: the claim being taught is about *discretisation* (one value per cell, so more cells means more of the variation can be described) and about mass conservation, not about CFD accuracy, and a demo that implied it had solved the flow would teach the wrong lesson. A live "Total flow: in 100% &rarr; out 100%" readout gives the invariant students can check at every refinement. **Optional "colour by velocity" uses a FIXED 0&ndash;25 m/s scale** — a legend that rescaled per refinement would make the colours mean something different at each step and destroy the comparison the demo exists to make. The **"obstruction" toggle is enabled only at 8&times;8 and 16&times;16**, with a visible hint saying so: a coarser mesh has too few cells to represent the blockage at all, and that restriction is itself the point. The two wall lines are drawn as *boundaries, not mesh rows* (commented in the source), because a cell adjacent to a stationary wall still has a nonzero face-average velocity — the standard misreading. **Accessibility:** 16&times;16 is 256 numbers, so the cell values are `aria-hidden` and an equivalent visually-hidden data table carries them instead, which keeps 256 items out of the keyboard sequence; the figure is a labelled scrollable region. **Notable for the framework:** this is the first demo in a new subject area to need **zero** additions to `agTeachingDemos.css` — it reuses `.ag-line-thin/medium/thick`, `.ag-graph-label`, `.ag-outline`, `.ag-metal` (re-tinted to the axis colour for the hatched solid), `--ag-fs-axis-label` and `--ag-c-control-accent`, with local CSS confined to arrangement and projection sizing. The framework is now carrying its weight. It is destined for the "Pretty Convincing?" CFD lecture, which is still a notes dump.
* **2026-09-11 — images centralised at the site root with a registry; a leading "/" now means "from the site root" everywhere.** **This reverses the "figures live with their content" line in the reorganisation entry above**, which was the inconsistent choice: the repo had already decided on 2026-08-08 that decks are content-only and link to shared assets at the root — that is what `base:` exists for and how `framework/` already works — so images are that same decision, and a deck was never self-contained anyway. Subjects overlap, so one figure is often wanted by several lectures. **The argument that actually settles it is attribution:** a figure copied into three lecture folders carries three copies of its credit, which diverge or get lost, and the question that bites once material is on a public site is not "did I credit it" but "am I allowed to use it". **The folder is flat, deliberately** — topic subfolders are wrong here for the same reason centralising is right, since an image used by both CFD and heat transfer has no correct topic, and a filename that never has to change is worth more than a tidy listing (moving an image silently breaks every deck referencing it). `images/images.json` records per file: **description** (doubles as the default alt text and caption, because the slide system already fuses those — so a good entry has to work both read aloud and printed under the figure), **source** (including whether it was cropped or annotated, which CC BY-SA and friends require be declared), **obtained** (a date), and **licence**, kept separate from source because credit and permission are different questions. **The registry is a record to consult, not a gate** — see the entry below on why the licence field is deliberately not policed.
* **2026-09-11 — site-root path resolution in the builder, and the demo `src` bug it exposed.** A leading slash on an image or a demo `src` means "from the site root"; the builder rewrites it **at build time** into a path relative to that deck. So the generated deck still contains nothing but relative paths and a double-clicked local file still works — which was the original 2026-08-07 reason for rejecting root-relative paths in the *output*, an objection that does not apply once the rewriting happens during the build. A path without the slash still means "next to the deck", which stays right for a one-off figure and keeps plain markdown previewing correctly in an editor. **This exposed a live bug:** `template.md` had always claimed a demo's `src` was "relative to the site root", but `renderDiv` emitted it verbatim with no `base`, so every embedded demo would have been broken in any deck below the site root. Nothing used it yet, so correcting the spelling to `/teaching-demos/...` cost nothing. **The registry's completeness is checked, which is the only reason it will survive a year** — `convert.js` warns about an image used but unregistered, files in `images/` with no entry, and entries whose file has gone; the same to-do-list role the unstyled-classes warning already plays. **The licence field is deliberately NOT checked, and a check must not be added.** One was built and removed the same day: about 90% of these images are Andrew's own work, so a nag about paperwork fires constantly and is wrong almost every time — and a warning that is usually wrong is precisely how people are trained to ignore warnings, which would eventually cost the completeness checks their credibility too. The field exists to be consulted when a question arises, not to gate a build. *(Related, recorded because it will come up again: UK law has no "fair use" — that is the US doctrine. The applicable exception is fair dealing for illustration for instruction, CDPA 1988 s.32, which needs non-commercial purpose, sufficient acknowledgement and proportionate use. It is strongest for showing something in a lecture and weakest for republishing it on an open website — which matters here, because the whole architecture is "upload the tree to the website". Behind a VLE login is a materially safer position than a public URL, and the University has a copyright officer and CLA cover for the harder cases.)* **`build.html` cannot make that check and says so:** a browser page opened through a file picker cannot read a sibling file off the disk, and giving the tool a directory picker was deliberately rejected on 2026-08-07. That is why the check lives in the **host** layer (`convert.js`) and not in the shared parser — the parser only records the image paths it saw onto `ctx.images`. `resolveRoot` and `rewriteImages` are pure string operations, keeping the CLI parity rule intact; `rewriteImages` runs a regex over marked's output, acceptable for exactly the reason `mapTopLevel` is, and it deliberately does **not** re-escape, since marked has already escaped the attribute and doing so again would turn `&amp;` into `&amp;amp;`. Verified end to end with a throwaway deck at real lecture depth: shared image and demo both rewritten to `../../../` and confirmed to resolve, deck-local and `https:` paths untouched, and both registry warnings firing.
* **2026-09-11 — figures and captions; the third place the docs described behaviour that did not exist.** `template.md` had always claimed "the alt text becomes the caption", and it never did: nothing emitted `<figure>`, and `agSlides.css` styled neither `figure` nor `figcaption`, so images rendered as a bare `<img>` with no visible caption at all. Same family as the `base` and demo-`src` mismatches found the same day, and the pattern is worth noticing — in all three the *documentation* was the correct statement of intent and the implementation was what had drifted, which is the opposite of the usual assumption. **Two ways to caption, and the difference is pedagogical rather than cosmetic.** An image alone on a line becomes a `<figure>` whose caption defaults to the alt text — pandoc's `implicit_figures`, so it stays portable, and every image already written gains a caption without being touched. A following `: caption` line overrides it. **The second form is the one to encourage:** alt and caption do different jobs — alt describes the picture for someone who cannot see it, the caption adds context for someone who can — and one text doing both usually does neither well. It is also where a credit line belongs, since fair dealing wants the acknowledgement visible. **Accessibility detail worth keeping:** an auto-derived caption is emitted `aria-hidden="true"`, because it repeats the alt word for word and a screen reader would otherwise announce the same sentence twice; an explicit `: ` caption is never hidden, because it carries something the alt does not. **Ordering is load-bearing:** `figures()` runs BEFORE `definitionLists()`. `isTermLine` does not exclude a leading `!`, so `![alt](src)` followed by `: text` would otherwise be swallowed into a `<dl>` — consuming it in the figure pass removes the ambiguity instead of patching `isTermLine` to dodge it. Detection needs the image alone on the line at column 0, so inline images and images inside list items are left alone. The `src` is emitted as written and `rewriteImages` resolves it afterwards, so site-root paths and the registry check keep working. **CSS note:** `figure img` is capped at `max-height: 460px`, in px and not vh on purpose — reveal scales a fixed 1280x720 canvas, so a `vh` would be measured against the real window and the image would come out a different size on every screen. Verified against all six cases: auto caption, explicit caption with markdown in it, empty alt, definition lists still intact, inline images untouched, and site-root resolution still correct.
* **2026-09-11 — image fit is the stylesheet's job, sized by context with `:has()`.** First real slide with two figures in a 50% column overflowed badly — the Trent XWB cutaway and the Iron Man figure together ran off the bottom of the canvas and behind the footer. Root cause was a single fixed `max-height: 460px` on `figure img`, which cannot serve both a full-width hero image and two stacked figures in a half-width column. **The fix is contextual capping rather than one number**, because reveal never shrinks content to fit — anything taller than the 720px canvas is clipped — so the cap has to anticipate the space actually available: 420px for a full-width figure, 340px inside a column, 200px when figures are siblings sharing a box, 120px for three or more. `:has(figure ~ figure)` does the counting, so the stylesheet works it out and the author never sizes anything. Matching on *sibling* figures is deliberate: one figure in each of two columns is not sharing a box, so each keeps the full column cap. Numbers derive from the canvas budget — subtract slide title, column heading, a line of text and one caption per figure, then divide the remainder — and are deliberately conservative, since a clipped slide is a worse failure than a slightly small image. `:has()` was already a dependency here (the title-slide footer rule); where unsupported, images fall back to the single-figure cap, which is small in a crowded column but never broken. **The general principle, worth applying to everything that follows: fit problems belong to the framework, not to the person writing the lecture.** If a deck overflows, the answer is a rule in agSlides.css, not a note asking the author to resize something.
* **2026-09-11 — `{.full-bleed}` slide class: the first slide-level layout in the vocabulary.** One image, edge to edge, no margins, with the title and caption laid over it — for the slide where the picture *is* the point and framing only weakens it. **No parser change was needed**, which is the thing to notice: heading attributes already carry arbitrary classes through, so a new slide layout is pure CSS, and the builder's "classes agSlides.css does not style yet" warning is what tells you when one is missing. Expect future layouts to arrive the same way. **Design decisions:** the image is **cropped** to the canvas with `object-fit: cover` rather than letterboxed, because fitting it inside would reinstate the margins the class exists to remove — the consequence, accepted, is that a picture with its subject hard against an edge loses some of it, so pick the picture for the treatment rather than the treatment for the picture. Title and caption sit on **gradient scrims**, which are structural rather than decorative: white text straight onto a photograph is unreadable as soon as there is a light area behind it, and a busy painting has them everywhere. The usual title rule (`border-bottom`) is suppressed, since it would draw a line across the image. The figure resets `max-width`/`max-height` because the contextual figure caps would otherwise stop it reaching the edges. **One coupling introduced, and it is worth knowing about:** the canvas size now exists in two places — `build.html` sets reveal's config, and `agSlides.css` needs it in CSS to size the bleed, since a stylesheet cannot read reveal's config. It is named `--ag-canvas-w` / `--ag-canvas-h` next to the base font size with a comment on both sides; change one, change the other. Documented in `template.md` under Full-bleed images.
* **2026-09-12 — diagrams get their own identity: `diagrams/`, `agDiagrams.css`, `agDiagramsGuide.html`.** Standalone SVG diagrams, linked from decks rather than inlined, each carrying `<?xml-stylesheet href="../framework/agDiagrams.css"?>` — the standard way an SVG document links CSS, and the reason a diagram drawn today can be restyled in three years. **Linking works only because the SVG is embedded as a document (`<object>`), never as `<img>`**: SVG in `img` renders in a secure static mode that refuses all external resources, so the stylesheet is silently ignored. The reference point was the illustration set on flashcards.dwarkesh.com. **The conclusion drawn from studying it is the one worth keeping: what separates professional diagrams from amateur ones is HIERARCHY, not palette.** In those drawings most of the picture is pale grey and only the subject is at full strength; amateur diagrams draw everything at one weight, so the eye has nowhere to land. Hence a five-step neutral ramp named for its job — **ink / muted / faint / ghost / hairline** — which is the most-used part of the file, and the guide demonstrates it with the same six boxes drawn twice. Supporting conventions lifted from the same source: one line weight carrying ~90% of a drawing (three exist, deliberately not equal citizens), muted rather than pure hues, small quiet labels, a warm off-white ground rather than white, and **labels drawn in the colour of the thing they name**, which removes the need for a legend. **Colour is applied as TWO classes — a role and a hue** (`class="ag-d-box ag-d-blue"`): 16 classes instead of the 36+ that one-class-per-combination would need, and both are short enough to type into Inkscape's XML editor. Each hue supplies tint/base/shade, so "pale fill, base outline, dark text" is a combination that never needs a contrast check. **Three tiers of colour, kept apart on purpose:** three *core* colours (blue, terracotta, green) reused constantly, since reuse is what creates identity; twelve *specific* hues for depiction rather than distinction (a Ferrari has to be red) — explicitly not for differentiating parts of a schematic; and one *highlight* (pink), with the rule that exactly one thing per diagram wears it, because its whole power is scarcity. **Violet is deliberately excluded** — it belongs to the slide chrome, and diagrams competing with headings would muddy both; swapping it in is three lines if that judgement turns out wrong. **The standard box is `viewBox="0 0 1200 675"`, and the load-bearing half of that is the WIDTH, not the ratio:** stroke widths and font sizes are in user units, so a diagram 600 wide renders every line at double the apparent weight. **Corrected same day:** the first draft said "vary height freely, never width", which was too permissive and undercut the point — the user pushed back, rightly, that a shared frame is most of what makes a set read as a set. The rule is now that 1200x675 is THE box, not a default to adjust; a cramped drawing is a reason to simplify the drawing or split it in two, not to grow the box; and the genuine exceptions are shapes rather than sizes. A menu of four was then reinstated at the user's request — 1200x675 (the default), 600x675 (half width, for a column), 1200x400 (long and thin), 600x1000 (tall and narrow) — framed so that leaving the default needs a reason about the SHAPE of the content. **That exposed an error in "always keep the width at 1200":** the rule exists to keep apparent line weight constant, and a 1200-wide viewBox displayed in a half-width column is scaled by a half, so every line renders at half the thickness of the rest of the set. The correct rule is that the viewBox width FOLLOWS THE DISPLAY WIDTH — 1200 at full width, 600 at half — and the height follows the content. The half-width size is documented as deliberately rare, with the user's reasoning recorded: columns stack on a phone and lose the pairing that justified them, written notes want explanation above and below a figure rather than beside it, and in a lecture the thing next to a diagram should be your voice. **The ground is a classed rect, not a fill attribute**, so ground colour, corner radius (via `rx`, a CSS geometry property — the same trick `.ag-particle` already uses for `r`) and hairline border are all changeable across every diagram at once; setting `--ag-d-ground` to `transparent` converts the whole collection retrospectively. `agDiagrams.css` **imports** `agTeachingDemos.css` rather than redefining anything, and re-points its variables (`--ag-c-line`, `--ag-sw-*`) at the diagram palette — so materials, heat arrows, axes and particles are all available, and a diagram and a demo on neighbouring slides cannot drift. **Two palettes coexist and must not bleed:** materials are the *physics* palette (grey because it is steel), the new hues are the *schematic* palette (blue because it is not the green one); mixing them teaches the reader that colour means nothing. **The failure mode to keep warning about is Inkscape**, which writes `style="fill:#000000"` onto every object without being asked; an inline style beats a stylesheet, so such a shape opts out of the house style permanently. The `?` (unset) button in Fill & Stroke is the fix, and a mechanical strip-inline-paint script is the intended safety net — deterministic rather than AI, because an LLM rewriting SVG could nudge geometry invisibly. Dark mode was raised and **deliberately dropped**: a linked SVG cannot see the deck's `data-theme`, and at ~100 diagrams the user judged a generated light/dark pair with a JS swap the cheaper answer than constraining the palette. Switching from linked to inlined later would change the builder only, not one `.svg`, so that decision stays reversible.
* **2026-09-12 — the builder embeds SVG as `<object>`, not `<img>`, and that one line is what makes linked diagrams work at all.** Embedding the first three diagrams into a real deck exposed the gap immediately: `figures()` emitted every image as an `<img>`, and a browser renders SVG-in-`img` in a **secure static mode that refuses to load any external resource**. The diagrams carry their appearance in a linked stylesheet, so through an `<img>` they would have arrived as black shapes on white — the stylesheet silently ignored, with no error anywhere. `figures()` now branches on the `.svg` extension and emits `<object type="image/svg+xml" data="...">`, which loads the file as a document so its `<?xml-stylesheet?>` is honoured. Everything else still emits `<img>`. **Accessibility:** the object carries `role="img"` plus `aria-label` from the alt text, so it announces as a picture rather than as an embedded document, and the text between the tags is the fallback if the file cannot be loaded. **Three knock-on changes, all small but none optional:** `rewriteImages` now matches `<object data>` as well as `<img src>` (a `data-*` attribute cannot collide, since those are followed by a hyphen and never by `=`), so site-root `/diagrams/...` paths resolve exactly like image paths; every `figure img` rule in `agSlides.css` became `figure :is(img, object)`, so the contextual height caps and the full-bleed reset apply to diagrams too; and `build.html`'s "shared images used" notice now filters on `/images/` specifically rather than any leading slash, so diagrams no longer report themselves as unregistered images. The registry check needed no change — it already filtered on `/images/`, so `diagrams/` is correctly ignored, which is what the decision not to give diagrams a registry intended. **A genuine gap this surfaced and did NOT close:** there is no way to write a descriptive alt text and *no* visible caption. Alt text auto-becomes the caption, and the only way to suppress it is an empty alt, which throws away the accessibility. A long screen-reader description therefore prints verbatim under the figure. The workaround in use is an explicit short `: caption` line, which keeps the long alt where it belongs — but a proper answer (something like `: -` meaning "no caption") is still owed.
* **2026-09-12 — `diagrams/diagrams.json`, and the two indexes now share one check.** A diagram index was originally declined on the grounds that provenance is what `images.json` is for and diagrams are all own work. That was the wrong test. The user's reason is better and settles it: **the folder is flat by design, so it needs something searchable.** Grep an index rather than opening a hundred SVGs one at a time. Fields are `alt` (what the diagram *shows*, a real sentence, which is also what makes the file searchable) and `caption` (what it *means* — the short line printed under the figure). The two indexes exist for genuinely different reasons, which matters before anyone adds a field: **images.json is about provenance, diagrams.json is about findability.** `convert.js`'s `checkImages` became a generic `checkAssetIndexes` driven by a small spec table, so both folders get the same three reconciliations — used-but-unlisted, on-disk-but-unlisted, listed-but-gone — with no duplicated logic. **Files beginning with an underscore are skipped**, which is now the convention for a template rather than content (`diagrams/_template.svg`); without that rule the template would report itself as an unindexed diagram for ever. The image licence field is still deliberately unchecked, for the reasons already recorded. **One decision worth keeping: the index is a RECORD, not the source.** It was tempting to have the builder inject alt and caption from the index so a diagram carries its own description everywhere and nothing is duplicated — which would also close the "descriptive alt with no visible caption" gap. It was rejected because `build.html` cannot read a sibling file off the disk, so the browser and command-line builders would produce *different decks from the same markdown*. That parity has been load-bearing since 2026-08-09 and is worth more than the duplication costs. The consequence, accepted: alt and caption exist in two places and must be kept in step by hand.
* **2026-09-12 — house rule: alt text and captions end with a full stop.** The user's reasoning, which is the part worth keeping: it is not that these are always sentences. It is that sooner or later one of them *is* two sentences, and then the first has to be terminated — which leaves the second looking wrong unless everything else is punctuated the same way. Deciding once removes a question that would otherwise be re-answered, differently, every time a caption is written. Applied to every piece of that text in the repository: alt text and `: ` captions throughout `prettyConvincing.md` (19 lines), the `description` field in `images/images.json`, `alt` and `caption` in `diagrams/diagrams.json`, and the `<title>` of each diagram — the SVG title is the accessible name a screen reader announces in place of the picture, so it is alt text by another spelling and follows the same rule. Recorded in `framework/template.md` (where an author meets it while writing a deck) and in the FIELDS block of both index `_readme`s (where an author meets it while cataloguing). **Note for anything added later: the auto-caption path inherits this for free**, since a caption derived from alt text carries the alt's punctuation with it.
* **2026-09-28 — `cfd-stack.svg`, and a cascade bug in `agDiagrams.css`.** A "CFD stack" after Jensen Huang's five-layer cake of AI: seven layers, ordered by one rule — **each layer turns the problem into something the layer below can handle** (question → case → PDEs + closures → algebra on a mesh → numbers → work shared across processors → chips). A candidate that cannot be placed by that rule is not a layer; meshing sits inside Discretisation for that reason. Groups reuse the cfd-history colours (terracotta engineering, blue theory, green computing) so a deck showing both stays consistent. **The bug it exposed:** `.ag-d-label-hue` sat in section 3, above `.ag-d-label` in section 4; both are single-class selectors, so on an element with both classes (the documented way to get hue AND label size) the later rule won and every hue label rendered in ink — including the conservation diagrams since they were drawn. The rule now lives at the end of section 4 with a comment saying its position is load-bearing. **General lesson for this file: when classes are designed to be stacked, source order is part of the API.** Also noted: `diagrams/_template.svg` is referenced by the readme and guide but does not exist.
* **2026-09-28 — `.learning-outcomes` panel, for every deck.** `::: {.learning-outcomes}` renders a bordered box with a solid powder-violet header band reading "Learning outcomes" and a target icon, deliberately matching the MEE Labs 2.0 learning-outcomes callout so lab sheets and lectures agree. **It is a different kind of block from the four semantic ones** (`keypoint`, `question`, `definition`, `top-tip` mark something inside the argument; this frames the whole session), which is why it is the only boxed-with-header design and why adding it does not break the "about four, never more than six" rule for in-argument roles. **The label is CSS `::before` text, not markdown** — the wording is one decision for every deck, and authors must not type it inside the block. **The icon is a CSS mask** (`--ag-icon-target`, an SVG data URI drawn for the file), not a background image, because a mask takes its colour from `background-color` and so follows the `--ag-panel-head-text` role; browsers without mask support simply show no icon. Three new ROLE variables (`--ag-panel-head`, `--ag-panel-head-text`, `--ag-panel-body`) keep the two-layer colour model intact: the header stays powder violet with dark text in both themes, and only the body is re-pointed for dark. `build.html`'s `KNOWN_DIVS` gained the class so it does not warn as unstyled; `convert.js` inherits that automatically since it runs the same parser. Verified by rendering light and dark in headless Edge. Demonstrated in `template.md`, which also carries the pedagogical point: outcomes should start with an assessable verb, and "understand" is the usual trap.
* **2026-09-28 — `cfd-workflow-1/-2/-3` series, and the second cascade bug in `agDiagrams.css`.** Redrawn from Andrew's own red/green flowcharts: (1) the eight-step simulation pipeline, (2) the six-step study around it with a loop from "perform alternative tests" back to the modelling strategy (not to the aim — the question usually stays, the approach is revised), (3) both side by side with faint exploded-view lines showing the whole pipeline is the single green "actually do the CFD" box. Colours reuse the CFD set: green = the CFD itself, terracotta = engineering judgement. 1 and 2 are each centred on their own slide; 3 uses IDENTICAL box sizes (only positions shift) so the reader recognises them — the offsets are recorded in each file's header comment. "Define physics" carries its detail as a small second line rather than a tiny inline parenthesis. **The bug:** the neutral classes (`.ag-d-muted/-faint/-ghost/-ink`) set `stroke` directly and sit in section 2, before the roles in section 3, so on a stacked element (`ag-d-box ag-d-ghost`, `ag-d-connector ag-d-muted`) the role's stroke won and the grey was silently ignored — which broke the guide's own hierarchy example, the most important picture in it. Fixed by having each neutral ALSO set `--ag-d-h-base`, so it behaves like a hue on a role and no longer depends on source order. Same family as the `.ag-d-label-hue` fix earlier the same day; **between them the rule is: in a stack-your-classes system, a modifier should set a variable the base class reads, not a property the base class also sets.**
* **2026-09-28 — quotations get attributions and a house style.** A quote is still a plain markdown blockquote (`>`); what is new is the **attribution**: if the quote's last line starts with `—` (or `--`/`---`, because an em dash is awkward to type), `build.html`'s `quoteAttributions` lifts that line out and emits `<figure class="quote"><blockquote>…</blockquote><figcaption>…</figcaption></figure>`. That is the WHATWG-recommended markup: the source is not part of the quoted words, so it belongs outside the blockquote, and the figure is what ties the two together for a screen reader. **A single `- ` is deliberately not accepted** — markdown reads it as a list, and a quote may legitimately end with one (the user's first attempt used `> - url`, which is exactly this trap). It runs on marked's output, innermost blockquotes only, as a regex over well-formed HTML like `rewriteImages`, so CLI parity holds. Styling is `agSlides.css` §5d. **Revised the same day at the user's request** — the first version was left-aligned and unboxed with an opening mark only; the user wanted both marks, everything centred, and a background so a quote stands out. Now: a rounded box filled with the `--ag-panel-body` role (shared with learning-outcomes, so dark mode comes free), shrink-to-fit up to 85% width and centred on the slide; the quote text centred and italic; large “ and ” in `--ag-accent` hung in the box's side padding; the attribution stays **right-aligned** (the user's call — it reads as a signature), upright, in `--ag-emphasis`, with the dash drawn by CSS so every deck shows the same one. The box is on the `<figure>` when there is an attribution and on the bare `<blockquote>` when there is not. It stays distinct from the semantic blocks because those carry a left rule and mark the lecturer's own argument, where a quote is somebody else's words. Both marks carry CSS alt text (`content: "“" / ""`) so screen readers do not announce them. **Knock-on fix:** a quote is now a `<figure>`, so the image-sharing caps (`:has(figure ~ figure)`) had to become `figure:not(.quote)`, otherwise one picture plus one quote would shrink the picture to the two-figure cap. Verified: a quote with no attribution, one ending in a list, a dash mid-quote, the no-blank-line form, and image-plus-quote all behave; rendered in headless Edge on the "CFD stands for" slide.
* **2026-09-28 — how `. . .` actually behaves: one click per BLOCK, grouped with `::: {.fragment}`.** Clarifying the 2026-08-08 entry, which calls this "pandoc's semantics" — it is not quite. `markFragments` marks every top-level block after a pause as its own `.fragment`, and reveal gives each fragment its own click, so three paragraphs after one pause take three clicks. Pandoc would show them together. **To group several blocks onto one click, wrap them in `::: {.fragment}`** (already in `KNOWN_DIVS`; needs no `. . .` before it) — documented in `template.md` under "The source for a pause". A pandoc-style change (per-slide `data-fragment-index` grouping, deliberately not a wrapping div because that breaks `section > figure` sizing rules) was built and **reverted the same day at the user's request**: the current behaviour is intended, and the user only wanted the grouping syntax. Do not reintroduce it unasked.
* **2026-09-28 — the title slide is the old PresentationTemplate cover, ported.** Andrew designed the cover in `G:\My Drive\Publication and output\PresentationTemplate` (`pres_title.css`) with some care and wanted it instead of reveal's plain title. It is reproduced closely: kicker and crest along the top, large title, serif-italic subtitle, short violet rule, frosted Presenter/Details cards along the bottom, and `Diamond.jpg` fading in from the right under a brand-violet wash (the original two-mask fade, exactly). **It is built entirely from front matter**, so no author writes a title slide: `title`, `subtitle`, `kicker` (new), `author` + `institute` (Presenter card), `details` (new; originally named `venue`, renamed the same day because a module code is not a venue, and `venue` is still accepted) + `date` (Details card; ISO dates are printed as "8 August 2026", anything else verbatim), and `cover` (new; an optional per-deck photo, site-root `/` paths allowed and registry-checked like any image). Empty parts are omitted, not drawn blank. **Decisions worth knowing:** (1) **Translated, not copied.** The old sizes were `clamp()`/`vw`, which would track the browser window instead of reveal's fixed canvas; each was evaluated at 1280px, where the old deck was designed to be seen, and rewritten as em (type) and px (layout), so the cover renders at its designed size and scales with the canvas. (2) **The photo is an inline `background-image` emitted by the builder**, not a CSS `url()`, so the default and a per-deck `cover:` resolve by the same route as every other deck path. `Diamond.jpg` now lives in `framework/assets/` beside the logos, as a house asset rather than a registry image. (3) **Content sits in an inner `.ag-cover` div, not on the `<section>`**, because reveal writes `display` inline on sections as it navigates, which would overrule a grid on the section itself. The section keeps `class="title-slide"`, which the footer-hiding rule depends on. (4) **On screen the photo is pinned to the WINDOW, not the canvas.** The first version put it inside the slide and bled it past reveal's 4% margin. Andrew then found that in any window that is not 16:9 the whole canvas letterboxes, photo included, leaving white bands, whereas the original always filled the right 65% of the window edge to edge. So `build.html` now also emits `.ag-cover-bg` inside `.reveal` but outside `.slides`. That places it under reveal's scaling, never inside it: `width: 65%` of the window, `background-size: cover` (cropped, never stretched). It fades in only while `section.title-slide.present` exists and not in overview, the same `:has()` trick the footer uses. The in-slide copy (`.ag-cover__accent`) is kept **for print only**, since a window-pinned layer means nothing on paper; each is hidden in the other medium. Consequence, accepted: in a very wide or very tall window the title text (on the canvas) sits further from the photo's fade than in the original, whose text was also window-positioned. (5) Dark theme follows the original: darker photo treatment, white crest, and no cards (text straight onto the ground in powder violet). Web fonts are still not fetched, so the subtitle uses Source Serif 4 where installed and Georgia otherwise. Verified in headless Edge in light and dark against a render of the original.
* **2026-09-28 — crisp text: decks scale with CSS `zoom`, not reveal's `transform`.** Andrew noticed text in reveal decks looked uneven next to the old PresentationTemplate in the same Chrome: varying stroke thickness and softer weight. **Cause:** reveal fits its fixed 1280x720 canvas to the window with `transform: translate(-50%,-50%) scale(S)`. On a real GPU, Chrome draws the slide into a texture at canvas size and resamples it by a non-whole factor, so some stems land on one pixel and others smear across two, and ClearType drops to greyscale antialiasing. The old template sized everything to the window (`vw`/`clamp`), so text was drawn at native pixels. **Headless Edge cannot reproduce this** (it draws in software), which is why a screenshot comparison first showed no difference. Don't use headless renders to judge text quality. **Fix:** a small deck script from `build.html` swaps the transform for `zoom: S` after every reveal layout, so the browser re-lays the slide out at real size and draws text on screen pixels. The canvas model is unchanged: every px in `agSlides.css` still means canvas px. The slide is centred by **measuring** it (`getBoundingClientRect`, including a 100px probe to learn how margins behave under zoom) rather than by computing margins, because margin-under-zoom semantics have differed between browser versions. A first attempt that computed margins put the slide in the wrong place. A `MutationObserver` on the `.slides` style catches every relayout, including ones reveal makes without an event, and ignores our own writes because they never contain `scale(`. **Left to reveal, deliberately:** `?print-pdf`, the speaker view's preview frames (`?receiver`), the overview grid, a scale of exactly 1, and browsers without `zoom` (Firefox before 126). **Side effect, accepted:** an embedded demo iframe now lays out at its real on-screen size, as it would on a web page, rather than as a shrunken picture of a larger layout. Its responsive layout may therefore differ slightly from before. Verified in headless for position (cover, columns, fragments, demo slide all placed exactly as before). **Tested by Andrew the same day:** everything works, probably crisper, but **not fully solved, and parked by choice.** As the window shrinks, text goes crisp → splotchy → crisp. Zoom produces fractional font sizes (e.g. 30px × 0.743 ≈ 22.3px), and ClearType fits stems cleanly at some sizes and not others. Matching the old template fully would mean abandoning reveal's fixed canvas, which is not worth it. **Separately, the cover's Presenter card is still softer than the prototype.** Likely cause: `backdrop-filter` on `.ag-cover__card` forces greyscale antialiasing. Cheap thing to try next: replace the frosted blur with a near-opaque plain background.
* **2026-09-28 — section dividers are the old PresentationTemplate design, ported.** From `pres_section.css`: a diagonal violet gradient filling the window (light: wave-white → pale → powder violet; dark: dark → brand → electric violet), a serif label with a hairline in front, a very large title (89.6px), and an optional serif-italic sub-heading. Markdown is unchanged: `# Heading`, and any paragraph under it becomes the sub-heading. **The label is automatic** ("Part One", "Part Two"… spelled out as in the original). It is numbered by `build.html` and emitted on the `<h1>` as `data-kicker`; `# Title {kicker="Interlude"}` replaces it, and the count still advances. **Two gotchas learned building it:** (1) a CSS counter numbered every divider "Part One", because reveal sets `display:none` on slides away from the current one and counters skip hidden elements; (2) CSS `attr()` reads only the element's own attributes, so a `kicker` on the `<section>` could not label its `<h1>::before`. That is why the builder puts it on the heading, the one heading attribute not passed to the section. **The gradient is `.reveal::before`, pinned to the window** and faded in while a `section-slide` is current, for the same reason as the cover photo: in the canvas it would letterbox. In print it goes onto the slide instead. Vertical centring uses `align-content: center` on the block section (2024 CSS; older browsers top-align), not flex, because reveal writes `display` inline on sections. Colours go through three new roles (`--ag-section-bg/-label/-sub`) re-pointed in the dark block. **Consequence for existing decks:** a `#` heading with bullets or images under it (e.g. "Some of my work" and "Commercial software" in *Pretty Convincing?*) now renders as a divider with content under a huge title and overflows. Those belong as a `#` divider followed by a `##` slide.
* **2026-10-05 — `cfd-geometry-rocket-fluid-domain.svg`: the external-flow geometry is the air.** Added at the placeholder on the "Geometry creation" slide in *Pretty Convincing?*. A quiet grey rocket is paired with a blue, three-dimensional cutaway enclosure containing an identical rocket-shaped cavity. The shared profile stays at the same scale and orientation in both views; the arrow explains enclosure creation and solid subtraction. The air is the volume to mesh, and the cavity surface is the rocket wall boundary. This is about external aerodynamic drag: solid CAD remains useful for making the subtraction, and the drawing cutaway is not a physical domain boundary. Uses only existing `agDiagrams.css` classes, the standard 1200 x 675 canvas, geometry-only clipping and an even-odd hole; no CSS changes or local styling. Recorded alt text and caption in `diagrams/diagrams.json` and the lecture Markdown, with full stops, and creation date and construction rationale in the SVG header.
* **2026-10-05 — `cfd-residuals-convergence.svg`: a Fluent-style residual history in the diagram house style.** Added to the "Residuals" slide in *Pretty Convincing?*, using Andrew's supplied screenshot as a reference for the overall pattern rather than exact data. Seven illustrative curves show initial reduction, slower decline, fluctuations and plateaux on a logarithmic residual axis. Continuity remains highest and energy lowest. **Revised the same day at Andrew's explicit request:** every curve uses a different existing hue from the twelve specific hues (continuity slate, k lime, x-velocity indigo, y-velocity orange, z-velocity magenta, epsilon teal, energy red), overriding the general core-palette convention for this data plot. The illustrative plateaux are spaced at least 45 SVG units apart, allowing matching-colour labels directly beside the endpoints without leaders. All curves use the existing solid regular-weight line role. Uses the standard 1200 x 675 canvas, flat `ag-d-ground` and existing graph/diagram classes only; no CSS edits, inline paint, new colours or gradients. Alt text and caption are registered in `diagrams/diagrams.json` and the lecture. The SVG header records creation, revision and construction rationale; speaker notes distinguish numerical convergence from physical accuracy.
* **Legacy:** `teaching-demos/heat-transfer/composite-wall.html` predates the framework and uses its own local `theme.css`; it does not yet follow the embeddable-component guidance (it has a full-page header). Migration to agTeachingDemos.css is pending.




