// ============================================================================ // template.typ — technical, minimal presentation template // // Usage: // #import "template.typ": * // #show: presentation.with(title: "...", subtitle: "...", author: "...") // // #slide(title: "Title")[ ... ] // two-part slide: title part | content part // #slide[ ... ] // single-part, full-width slide // #img("assets/fig.svg") // centered image, optional caption // ============================================================================ // Palette: warm paper, petrol accent, terracotta counterpoint. // All text/background pairs keep >= 4.5:1 contrast; decorative // elements (bars, markers, watermarks) never carry information. #let paper = rgb("#faf8f3") #let ink = rgb("#2d2a26") #let accent = rgb("#22617f") #let accent-soft = rgb("#4c8fad") #let ember = rgb("#a85430") #let wash = rgb("#edf2f5") #let mono = "DejaVu Sans Mono" // Short vertical accent rule (petrol -> terracotta). #let hairline-v = 3.5pt + gradient.linear(accent, ember, angle: 90deg) // Horizontal hairline for title/section slides. #let hairline-h = 1pt + gradient.linear(accent, ember, angle: 0deg) #let slide-counter = counter("slide") #let deck-title = state("deck-title", none) // Footer: deck title bottom-left, slide counter bottom-right. #let slide-footer() = context { let n = slide-counter.get().first() let total = slide-counter.final().first() set text(font: mono, size: 0.55em, fill: luma(150)) place(bottom + left, dy: -1em, deck-title.get()) place(bottom + right, dy: -1em, { text(fill: accent, weight: "bold", str(n)) text(fill: luma(165), " / " + str(total)) }) } // Tinted content panel with a colored spine — used for paired columns // (Grok = ember, Mistral = petrol). The spine is decorative reinforcement; // the column headers carry the information. #let panel(body, c: accent) = block( width: 100%, fill: c.lighten(95%), stroke: (left: 2.5pt + c), inset: (x: 0.55em, y: 0.45em), radius: (top-right: 2pt, bottom-right: 2pt), { set par(spacing: 0.85em) body }, ) // Thin gradient band across the top edge of every page. #let top-band = place(top + left, rect(width: 100%, height: 2.5pt, fill: gradient.linear(accent, ember, angle: 0deg))) // Import an image, centered; optional caption (never numbered). #let img(path, width: 82%, caption: none) = { let im = image(path, width: width) if caption == none { align(center, im) } else { align(center, figure(im, caption: caption, numbering: none)) } } // Two-part slide: a narrow title part on the left, content on the right. // Without `title`, the body takes the full width. #let slide(body, title: none, ..args) = { slide-counter.update(n => n + 1) page( footer: slide-footer(), ..args, if title == none { body } else { grid( columns: (0.3fr, 1.1fr), column-gutter: 2.2em, align(top, block( stroke: (left: hairline-v), inset: (left: 0.9em, top: 0.4em, bottom: 0.1em), text(font: mono, weight: "bold", size: 0.85em, tracking: 0.1em, fill: accent, upper(title)), )), align(top, body), ) }, ) } // Section divider slide, with a large decorative ghost numeral. #let section-counter = counter("section") #let section(name, note: none) = { section-counter.step() slide[ #align(horizon)[ #block(width: 72%, align(left)[ #context { let n = section-counter.get().first() place(top + right, dy: -1.1em, dx: 0.2em, text(font: mono, size: 4.2em, weight: "bold", fill: accent.lighten(86%))[ 0#str(n) ]) } #text(font: mono, size: 0.65em, weight: "bold", tracking: 0.2em, fill: accent)[SECTION] #v(0.4em) #text(size: 2em, weight: "bold")[#name] #v(0.55em) #box(width: 4.5em, align(left, line(length: 100%, stroke: hairline-h))) #if note != none [#v(0.5em); #text(font: mono, size: 0.7em, fill: luma(110))[#note]] ]) ] ] } // Title slide. #let title-slide(title, subtitle: none, author: none, date: none) = { align(center + horizon)[ #block(width: 72%, align(left)[ #text(size: 2.1em, weight: "bold")[#title] #if subtitle != none [#v(0.4em); #text(size: 1.15em, fill: luma(90))[#subtitle]] #v(0.9em) #line(length: 100%, stroke: hairline-h) #v(0.6em) #let meta = if author == none { date } else if date == none { author } else { author + " · " + date } #text(font: mono, size: 0.7em, fill: luma(110))[#meta] ]) ] pagebreak() } // Main presentation wrapper. All options are named; the document content is // bound to `doc` by the show rule. #let presentation( title: "Presentation", subtitle: none, author: none, date: auto, doc, ) = { set page(paper: "presentation-16-9", margin: (x: 3em, y: 2.6em), fill: paper, footer: none, background: top-band) set text(size: 20pt, fill: ink) set par(justify: false, leading: 0.68em) show strong: set text(fill: accent) show list: set list(marker: ( text(fill: accent-soft)[•], text(fill: accent-soft)[–], )) show raw: set text(font: mono, size: 0.85em) show raw.where(block: true): it => block(width: 100%, fill: wash, inset: 0.9em, radius: 0pt, it) deck-title.update(title) let d = datetime.today() let mm = if d.month() < 10 { "0" + str(d.month()) } else { str(d.month()) } let dd = if d.day() < 10 { "0" + str(d.day()) } else { str(d.day()) } let the-date = if date == auto { str(d.year()) + "-" + mm + "-" + dd } else { date } title-slide(title, subtitle: subtitle, author: author, date: the-date) doc }