Created
May 25, 2026 22:04
-
-
Save mcanouil/068cd6fa7976e0918f85fdc61390e3e7 to your computer and use it in GitHub Desktop.
Carousel: 2026-05-25 — Quarto Offset Headings v0.1.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Carousel: 2026-05-25 — Quarto Offset Headings v0.1.0 | |
| #set document(title: "Quarto Offset Headings v0.1.0", author: "Mickaël Canouil") | |
| #set text(lang: "en") | |
| #set page(width: 21cm, height: 18cm, margin: 0cm) | |
| // ── palette ────────────────────────────────────────────────────────────── | |
| #let bone = rgb("#f3f1ea") | |
| #let bone-2 = rgb("#e8e5db") | |
| #let ink = rgb("#19222b") | |
| #let ink-soft = rgb("#5c6772") | |
| #let line = rgb("#c7ccd2") | |
| #let vermilion = rgb("#e8431f") | |
| #let slab = rgb("#141b22") | |
| #let slab-ink = rgb("#eef1f4") | |
| #let slab-soft = rgb("#7e8b97") | |
| #let slab-accent = rgb("#ff7a52") | |
| // ── fonts ────────────────────────────────────────────────────────────────── | |
| #let display = "Space Grotesk" | |
| #let body = "Inter" | |
| #let mono = "JetBrains Mono" | |
| #set text(font: body, fill: ink) | |
| // ── helpers ────────────────────────────────────────────────────────────── | |
| // Slide wrapper: full page with generous padding. | |
| #let slide(fill: bone, body) = page(fill: fill, { | |
| block(width: 100%, height: 100%, inset: (x: 1.7cm, y: 1.7cm), body) | |
| }) | |
| // Footer line: "project · version · date", pinned to the bottom of a slide. | |
| #let footer(dark: false) = place( | |
| bottom + left, | |
| dy: 0.7cm, | |
| text( | |
| font: mono, | |
| size: 10pt, | |
| fill: if dark { slab-soft } else { ink-soft }, | |
| tracking: 0.4pt, | |
| "mcanouil · offset-headings v0.1.0", | |
| ), | |
| ) | |
| // A single hash glyph row at a given depth (1..6), drawn in mono. | |
| #let hashrow(depth, label: none, active: false, dark: false) = { | |
| let mark = "#" * depth | |
| let mc = if active { vermilion } else if dark { slab-soft } else { ink-soft } | |
| let lc = if active { if dark { slab-ink } else { ink } } else if dark { slab-soft } else { ink-soft } | |
| grid( | |
| columns: (2.6cm, 1fr), | |
| align: (left + horizon, left + horizon), | |
| text(font: mono, size: 17pt, weight: if active { "bold" } else { "regular" }, fill: mc, mark), | |
| text(font: body, size: 15pt, weight: if active { "semibold" } else { "regular" }, fill: lc, label), | |
| ) | |
| } | |
| // Level gauge: a vertical rail labelled H1..H6 with one level lit. | |
| #let gauge(lit: 1, dark: false) = { | |
| let rc = if dark { slab-soft } else { line } | |
| let tc = if dark { slab-soft } else { ink-soft } | |
| stack( | |
| dir: ttb, | |
| spacing: 0.42cm, | |
| ..range(1, 7).map(n => { | |
| let on = n == lit | |
| box( | |
| grid( | |
| columns: (auto, 0.5cm), | |
| align: (right + horizon, center + horizon), | |
| column-gutter: 0.35cm, | |
| text( | |
| font: mono, | |
| size: 12pt, | |
| weight: if on { "bold" } else { "regular" }, | |
| fill: if on { vermilion } else { tc }, | |
| "H" + str(n), | |
| ), | |
| box( | |
| width: 0.5cm, | |
| height: 0.5cm, | |
| radius: 50%, | |
| fill: if on { vermilion } else { rgb(0, 0, 0, 0) }, | |
| stroke: if on { none } else { 1.5pt + rc }, | |
| ), | |
| ), | |
| ) | |
| }), | |
| ) | |
| } | |
| // Code slab: dark cool block, light mono text. | |
| #let codeslab(body) = block( | |
| width: 100%, | |
| fill: slab, | |
| inset: (x: 22pt, y: 20pt), | |
| radius: 12pt, | |
| text(font: mono, size: 14pt, fill: slab-ink, body), | |
| ) | |
| // Small eyebrow tag. | |
| #let eyebrow(t, fill: vermilion) = text( | |
| font: mono, | |
| size: 12pt, | |
| weight: "medium", | |
| fill: fill, | |
| tracking: 2pt, | |
| upper(t), | |
| ) | |
| // ════════════════════════════════════════════════════════════════════════ | |
| // SLIDE 1 — cover | |
| // ════════════════════════════════════════════════════════════════════════ | |
| #slide({ | |
| // Faint oversized hash ladder as a background motif, top-right. | |
| place( | |
| top + right, | |
| dx: 1.1cm, | |
| dy: -0.6cm, | |
| text(font: mono, size: 150pt, fill: bone-2, weight: "bold", tracking: -6pt, "###"), | |
| ) | |
| place(top + left, eyebrow("Quarto extension · v0.1.0")) | |
| place( | |
| left + horizon, | |
| dy: -1.2cm, | |
| block(width: 15cm, { | |
| text(font: display, size: 62pt, weight: "bold", fill: ink, tracking: -1pt, "Offset") | |
| linebreak() | |
| text(font: display, size: 62pt, weight: "bold", fill: vermilion, tracking: -1pt, "Headings") | |
| v(0.5cm) | |
| block(width: 13cm, text( | |
| font: body, | |
| size: 20pt, | |
| fill: ink-soft, | |
| "Shift heading levels by any amount, in any output format. As a filter, not a final pass.", | |
| )) | |
| }), | |
| ) | |
| // A small shift cue near the bottom: ## slides to ###. | |
| place(bottom + left, dy: -1.5cm, { | |
| grid( | |
| columns: (auto, auto, auto), | |
| column-gutter: 0.5cm, | |
| align: horizon, | |
| text(font: mono, size: 26pt, fill: ink-soft, weight: "bold", "##"), | |
| text(font: mono, size: 22pt, fill: vermilion, "──▶"), | |
| text(font: mono, size: 26pt, fill: vermilion, weight: "bold", "###"), | |
| ) | |
| }) | |
| footer() | |
| }) | |
| // ════════════════════════════════════════════════════════════════════════ | |
| // SLIDE 2 — the gap | |
| // ════════════════════════════════════════════════════════════════════════ | |
| #slide({ | |
| eyebrow("The gap") | |
| v(0.5cm) | |
| block(width: 16cm, text( | |
| font: display, | |
| size: 33pt, | |
| weight: "bold", | |
| fill: ink, | |
| "Pandoc shifts headings last.", | |
| )) | |
| v(0.4cm) | |
| block(width: 16cm, text( | |
| font: body, | |
| size: 19pt, | |
| fill: ink-soft, | |
| "'shift-heading-level-by' runs as a final post-processing step. By then every other extension has already seen the original levels and moved on.", | |
| )) | |
| v(0.9cm) | |
| grid( | |
| columns: (1fr, 1fr), | |
| column-gutter: 0.8cm, | |
| block( | |
| fill: bone-2, | |
| width: 100%, | |
| radius: 12pt, | |
| inset: 18pt, | |
| { | |
| eyebrow("Built-in", fill: ink-soft) | |
| v(0.3cm) | |
| text(font: body, size: 16pt, fill: ink, "One number for the whole document. Applied at the end. No extension can react.") | |
| }, | |
| ), | |
| block( | |
| fill: slab, | |
| width: 100%, | |
| radius: 12pt, | |
| inset: 18pt, | |
| { | |
| eyebrow("This extension", fill: slab-accent) | |
| v(0.3cm) | |
| text(font: body, size: 16pt, fill: slab-ink, "Runs mid-filter. Per heading or per document. Other filters see the shifted levels.") | |
| }, | |
| ), | |
| ) | |
| footer() | |
| }) | |
| // ════════════════════════════════════════════════════════════════════════ | |
| // SLIDE 3 — document-level offset | |
| // ════════════════════════════════════════════════════════════════════════ | |
| #slide({ | |
| eyebrow("Whole document") | |
| v(0.5cm) | |
| block(width: 16cm, text( | |
| font: display, | |
| size: 31pt, | |
| weight: "bold", | |
| fill: ink, | |
| "One key shifts everything.", | |
| )) | |
| v(0.7cm) | |
| grid( | |
| columns: (1fr, 3.2cm), | |
| column-gutter: 1cm, | |
| align: (left, center), | |
| { | |
| codeslab[```yaml | |
| extensions: | |
| offset-headings: | |
| by: 1 | |
| ```] | |
| v(0.6cm) | |
| stack( | |
| dir: ttb, | |
| spacing: 0.5cm, | |
| hashrow(1, label: "Section → becomes H2", active: true), | |
| hashrow(2, label: "Sub-section → becomes H3", active: true), | |
| hashrow(3, label: "Levels clamp at 1 and 6"), | |
| ) | |
| }, | |
| { | |
| eyebrow("by: +1", fill: vermilion) | |
| v(0.3cm) | |
| gauge(lit: 2) | |
| }, | |
| ) | |
| footer() | |
| }) | |
| // ════════════════════════════════════════════════════════════════════════ | |
| // SLIDE 4 — per-heading control | |
| // ════════════════════════════════════════════════════════════════════════ | |
| #slide(fill: slab, { | |
| text(font: mono, size: 12pt, weight: "medium", fill: slab-accent, tracking: 2pt, upper("Per heading")) | |
| v(0.5cm) | |
| block(width: 16cm, text( | |
| font: display, | |
| size: 31pt, | |
| weight: "bold", | |
| fill: slab-ink, | |
| "Or target a single heading.", | |
| )) | |
| v(0.7cm) | |
| codeslab[```markdown | |
| ## Methods {offset-headings-by="2"} | |
| ### Sampling | |
| ```] | |
| v(0.6cm) | |
| block(width: 16cm, text( | |
| font: body, | |
| size: 18pt, | |
| fill: slab-soft, | |
| "By default the offset cascades to nested headings. Set 'offset-headings-recursive=\"false\"' to move just that one and leave its children where they are.", | |
| )) | |
| v(0.7cm) | |
| grid( | |
| columns: (1fr, 1fr), | |
| column-gutter: 0.8cm, | |
| { | |
| eyebrow("recursive: true", fill: slab-accent) | |
| v(0.25cm) | |
| stack(dir: ttb, spacing: 0.32cm, | |
| hashrow(2, label: "Methods → H4", active: true, dark: true), | |
| hashrow(3, label: "Sampling → H5", active: true, dark: true), | |
| ) | |
| }, | |
| { | |
| eyebrow("recursive: false", fill: slab-soft) | |
| v(0.25cm) | |
| stack(dir: ttb, spacing: 0.32cm, | |
| hashrow(2, label: "Methods → H4", active: true, dark: true), | |
| hashrow(3, label: "Sampling → H3", dark: true), | |
| ) | |
| }, | |
| ) | |
| footer(dark: true) | |
| }) | |
| // ════════════════════════════════════════════════════════════════════════ | |
| // SLIDE 5 — cross-format, paired with quarto-prism | |
| // ════════════════════════════════════════════════════════════════════════ | |
| #slide({ | |
| eyebrow("Cross-format") | |
| v(0.5cm) | |
| block(width: 16.5cm, text( | |
| font: display, | |
| size: 30pt, | |
| weight: "bold", | |
| fill: ink, | |
| "Same heading, two meanings.", | |
| )) | |
| v(0.4cm) | |
| block(width: 16.5cm, text( | |
| font: body, | |
| size: 18pt, | |
| fill: ink-soft, | |
| "In Reveal.js, '# Part' opens a slide section. In an HTML article you usually want it nested under the page title instead.", | |
| )) | |
| v(0.6cm) | |
| codeslab[```markdown | |
| # Part {html:offset-headings-by="1"} | |
| ## Topic | |
| ```] | |
| v(0.35cm) | |
| text(font: mono, size: 13pt, fill: ink-soft, [Prism gates the attribute: it applies in HTML only. #text(fill: vermilion)[github.com/mcanouil/quarto-prism]]) | |
| v(0.7cm) | |
| grid( | |
| columns: (1fr, 1fr), | |
| column-gutter: 0.8cm, | |
| { | |
| eyebrow("revealjs · offset ignored", fill: ink-soft) | |
| v(0.25cm) | |
| stack(dir: ttb, spacing: 0.32cm, | |
| hashrow(1, label: "Part → H1 · slide part"), | |
| hashrow(2, label: "Topic → H2 · slide"), | |
| ) | |
| }, | |
| { | |
| eyebrow("html · +1 applied", fill: vermilion) | |
| v(0.25cm) | |
| stack(dir: ttb, spacing: 0.32cm, | |
| hashrow(1, label: "Part → H2", active: true), | |
| hashrow(2, label: "Topic → H3", active: true), | |
| ) | |
| }, | |
| ) | |
| footer() | |
| }) | |
| // ════════════════════════════════════════════════════════════════════════ | |
| // SLIDE 6 — why it matters | |
| // ════════════════════════════════════════════════════════════════════════ | |
| #slide({ | |
| eyebrow("Why it matters") | |
| v(0.5cm) | |
| block(width: 16.5cm, text( | |
| font: display, | |
| size: 31pt, | |
| weight: "bold", | |
| fill: ink, | |
| "Compose documents without rewriting headings.", | |
| )) | |
| v(0.8cm) | |
| let row(n, t) = grid( | |
| columns: (1.4cm, 1fr), | |
| align: (left + top, left + top), | |
| text(font: mono, size: 22pt, weight: "bold", fill: vermilion, str(n)), | |
| block(width: 14.5cm, text(font: body, size: 18pt, fill: ink, t)), | |
| ) | |
| stack( | |
| dir: ttb, | |
| spacing: 0.7cm, | |
| row(1, "Drop a standalone chapter into a report as a section. Push every heading down one level, no manual edits."), | |
| row(2, "Pull an included file up a level when it sits at the top of its own page."), | |
| row(3, "Stays format-agnostic: HTML, PDF, Typst, docx all honour the same shift."), | |
| row(4, "Composes with other filters, because the levels change before they run."), | |
| ) | |
| footer() | |
| }) | |
| // ════════════════════════════════════════════════════════════════════════ | |
| // SLIDE 7 — ship | |
| // ════════════════════════════════════════════════════════════════════════ | |
| #slide(fill: slab, { | |
| place( | |
| top + right, | |
| dx: 1.0cm, | |
| dy: -0.7cm, | |
| text(font: mono, size: 130pt, fill: rgb("#1c2630"), weight: "bold", tracking: -6pt, "#"), | |
| ) | |
| text(font: mono, size: 12pt, weight: "medium", fill: slab-accent, tracking: 2pt, upper("Ship it")) | |
| v(0.6cm) | |
| block(width: 16cm, text( | |
| font: display, | |
| size: 40pt, | |
| weight: "bold", | |
| fill: slab-ink, | |
| "Add it in one line.", | |
| )) | |
| v(0.8cm) | |
| codeslab[```bash | |
| quarto add mcanouil/quarto-offset-headings | |
| ```] | |
| v(1.1cm) | |
| stack( | |
| dir: ttb, | |
| spacing: 0.42cm, | |
| text(font: body, size: 17pt, fill: slab-ink, [GitHub: #text(fill: slab-accent)[github.com/mcanouil/quarto-offset-headings]]), | |
| text(font: body, size: 17pt, fill: slab-soft, "MIT licensed · v0.1.0 · works in any Quarto output format"), | |
| ) | |
| footer(dark: true) | |
| }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment