diff --git a/examples/showcase.md b/examples/showcase.md
index a0b0f7d..e12f7e9 100644
--- a/examples/showcase.md
+++ b/examples/showcase.md
@@ -39,6 +39,12 @@ Time check: should be at 04:00 by end of this slide.
---
+
+
+> A wet thing, an alive thing, an old thing.
+
+---
+
## Part two: how to look
---
@@ -75,6 +81,23 @@ flowchart LR
---
+```mermaid
+flowchart TB
+ L["Land plants"]
+ L --> B["Bryophytes
~20,000 species"]
+ L --> V["Vascular plants
~300,000 species"]
+ B --> M["Mosses"]
+ B --> H["Hornworts"]
+ B --> LW["Liverworts"]
+ M --> M1["Sphagnopsida
peat mosses"]
+ M --> M2["Polytrichopsida
hair-caps"]
+ M --> M3["Bryopsida
~95% of all moss species"]
+```
+
+> Three classes of moss inside a much larger family.
+
+---
+
## Two genera to know
:::columns
@@ -144,6 +167,22 @@ A short list of reliable spots:
---
+```python
+# How long to undo a drained peatland?
+# Rough field rule: about 250 years per centimeter of
+# restored peat depth, under ideal hydrology.
+def peat_recovery_years(centimeters_lost: float) -> int:
+ base_rate = 250
+ drought_penalty = climate.drought_index * 1.4
+ return int(centimeters_lost * base_rate * drought_penalty)
+
+print(peat_recovery_years(45)) # ~16,000 years
+```
+
+> What we drain in a season, we can't restore in a lifetime.
+
+---
+
## Where to look next
- Crum, *Mosses of the Great Lakes Forest*
diff --git a/packages/core/layout.ts b/packages/core/layout.ts
index 3b5e642..827f97a 100644
--- a/packages/core/layout.ts
+++ b/packages/core/layout.ts
@@ -5,8 +5,11 @@ export type Layout =
| "content"
| "section"
| "code-focus"
+ | "code-full"
| "diagram-focus"
+ | "diagram-full"
| "image-focus"
+ | "image-full"
| "two-column";
const HEADINGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
@@ -132,14 +135,27 @@ export function detectLayout(
// After stripping headings + caption, focus layouts require exactly one
// dominant media block. Anything else is regular content flow.
+ //
+ // If the slide also has *no* heading, we upgrade to the full-bleed
+ // variant — the media fills the slide edge to edge with no chrome,
+ // and the optional blockquote becomes an overlay caption instead of
+ // sitting under the figure.
if (mid.length === 1) {
const only = mid[0];
+ const noHeading = heads.length === 0;
if (isMediaBlock(only)) {
+ if (noHeading) {
+ const layout: Layout = isMermaid(only) ? "diagram-full" : "code-full";
+ return { layout, children: [figureFor(only, caption)] };
+ }
const layout: Layout = isMermaid(only) ? "diagram-focus" : "code-focus";
return { layout, children: [...heads, figureFor(only, caption)] };
}
if (isImageOnlyParagraph(only)) {
const img = childrenOf(only as ComarkElement)[0] as ComarkElement;
+ if (noHeading) {
+ return { layout: "image-full", children: [figureFor(img, caption)] };
+ }
return { layout: "image-focus", children: [...heads, figureFor(img, caption)] };
}
if (isElement(only) && only[0] === "columns") {
diff --git a/packages/core/templates/partials/slide-styles.eta b/packages/core/templates/partials/slide-styles.eta
index 27d1513..489254e 100644
--- a/packages/core/templates/partials/slide-styles.eta
+++ b/packages/core/templates/partials/slide-styles.eta
@@ -315,6 +315,105 @@
align-self: center;
}
+ /* ---- Full-bleed layouts (image-full / code-full / diagram-full) ----
+ * Triggered when a slide has no heading and exactly one media block —
+ * authored simply as a slide containing nothing but the image / code
+ * fence / mermaid block (plus an optional trailing blockquote that
+ * becomes the caption beneath it).
+ *
+ * The figure is a flex column: media grows to fill, caption sits
+ * underneath at its intrinsic height. No card, no chrome — DESIGN.md
+ * is explicit that captions are typographic marginalia (Recursive's
+ * slnt axis at -8°, Muted reading color), not the frosted-glass
+ * cliché. The base-color section behind the caption gives a single
+ * uniform reading surface for all three media types.
+ */
+ morkdeck-slide[data-layout$="-full"] figure {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ gap: 0;
+ flex: none;
+ min-height: 0;
+ min-width: 0;
+ max-width: none;
+ max-height: none;
+ }
+
+ morkdeck-slide[data-layout$="-full"] figure > :is(img, pre, .mermaid) {
+ flex: 1 1 auto;
+ min-height: 0;
+ width: 100%;
+ margin: 0;
+ }
+
+ /* Image: cover the slide. Some cropping of off-axis content is the
+ price of the full-bleed look; users who want letterboxing instead
+ should use the image-focus layout (heading present). */
+ morkdeck-slide[data-layout="image-full"] figure > img {
+ object-fit: cover;
+ display: block;
+ height: 100%;
+ }
+
+ /* Code: pre fills the available height with generous padding so the
+ code itself doesn't kiss the slide edges. Background matches the
+ section base for visual continuity; long files scroll within the
+ pre rather than blowing up the layout. */
+ morkdeck-slide[data-layout="code-full"] figure > pre {
+ padding: var(--morkdeck-dimension-space-xl);
+ border-radius: 0;
+ background: var(--morkdeck-color-base);
+ overflow: auto;
+ font-size: var(--morkdeck-dimension-size-code);
+ line-height: var(--morkdeck-number-leading-relaxed);
+ }
+
+ /* Diagram: SVG fills the available space with a small margin so
+ labels at the viewBox edges aren't clipped by the slide letterbox. */
+ morkdeck-slide[data-layout="diagram-full"] figure > .mermaid {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--morkdeck-dimension-space-md);
+ background: transparent;
+ }
+
+ morkdeck-slide[data-layout="diagram-full"] figure > .mermaid > svg {
+ width: 100%;
+ height: 100%;
+ max-width: 100% !important;
+ max-height: 100%;
+ }
+
+ /* Caption: bare typographic marginalia. No background, no border,
+ no blur, no card — the Recursive slnt axis carries the affect, the
+ Muted color carries the quietness. Mirrors the blockquote rule in
+ DESIGN.md (slnt -8°, typographic indent). */
+ morkdeck-slide[data-layout$="-full"] figcaption {
+ flex: 0 0 auto;
+ margin: 0;
+ padding:
+ var(--morkdeck-dimension-space-sm)
+ var(--morkdeck-dimension-space-lg)
+ var(--morkdeck-dimension-space-md);
+ background: transparent;
+ border: none;
+ border-radius: 0;
+ color: var(--morkdeck-color-muted);
+ font-size: var(--morkdeck-dimension-size-caption);
+ font-weight: 400;
+ font-variation-settings: "slnt" -8, "CASL" 0.4;
+ letter-spacing: var(--morkdeck-dimension-tracking-wide);
+ line-height: var(--morkdeck-number-leading-normal);
+ text-align: center;
+ max-width: none;
+ align-self: stretch;
+ }
+
/* ---- Columns --------------------------------------------------------
* Two even columns with a hairline divider centered in the gap. The
* divider stretches with the columns so the line reads as a rule, not
diff --git a/packages/wc/components/slide.ts b/packages/wc/components/slide.ts
index 1027afb..680fb88 100644
--- a/packages/wc/components/slide.ts
+++ b/packages/wc/components/slide.ts
@@ -112,6 +112,25 @@ export class Slide extends MorkdeckElement {
content: none;
}
+ /* Full-bleed variants: no padding, the media reaches every edge of
+ the 16:9 letterbox. position: relative anchors the optional
+ caption overlay defined in slide-styles.eta. The slide-number
+ marginalia is suppressed — there's no breathing room around the
+ media to host it. */
+ :host([data-layout="image-full"]) > section,
+ :host([data-layout="code-full"]) > section,
+ :host([data-layout="diagram-full"]) > section {
+ padding: 0;
+ justify-content: stretch;
+ align-items: stretch;
+ position: relative;
+ }
+ :host([data-layout="image-full"]) > section::after,
+ :host([data-layout="code-full"]) > section::after,
+ :host([data-layout="diagram-full"]) > section::after {
+ content: none;
+ }
+
/* Two-column: stretch content across columns. */
:host([data-layout="two-column"]) > section {
justify-content: center;