#let transit-red = rgb("#c53a2f") #let metro-blue = rgb("#005f9e") #let deep-teal = rgb("#0f766e") #let burnt-orange = rgb("#c96b2c") #let emerald = rgb("#1f7a4f") #let deep-violet = rgb("#5a4fcf") #let page-numbering-started = state("page-numbering-started", false) #let start-body-page-numbering() = { page-numbering-started.update(true) counter(page).update(1) } #let metropole( // -- Metadata -- title: none, subtitle: none, author: none, date: none, // -- Document -- paper-size: "a4", language: "en", date-format: "[day] [month repr:long] [year]", cover-page: false, inline-title: true, // -- Page margins (override metropole defaults) -- margin-x: none, margin-y: none, header-skip: none, // -- Fonts -- font-size: 11pt, leading-ratio: 1.75, body-font: "Source Serif 4", heading-font: "Source Sans 3", raw-font: "Source Code Pro", // -- Colors -- background-color: rgb("#ffffff"), foreground-color: rgb("000000"), accent-color: transit-red, // -- Body -- body, ) = { // ── Type scale (major third × 1.25) -- let scale = 1.25 let fine-size = font-size * calc.pow(scale, -2) // ~7pt let caption-size = font-size * calc.pow(scale, -1) // ~8.8pt let lead-size = font-size * calc.pow(scale, 1) // ~13.75pt let heading-size = font-size * calc.pow(scale, 2) // ~17.2pt let display-size = font-size * calc.pow(scale, 3) // ~21.5pt let title-size = font-size * calc.pow(scale, 4) // ~26.8pt // ── Derived colors -- let secondary-content-color = color.mix( (foreground-color, 5), (background-color, 3), space: oklab, ) let secondary-accent-color = color.mix( (accent-color, 2), (background-color, 1), space: oklab, ) let block-bg-color = color.mix( (background-color, 20), (foreground-color, 1), space: oklab, ) let link-color = color.mix( (accent-color, 3), (foreground-color, 1), space: oklab, ) // ── Baseline grid -- let line-height = font-size * leading-ratio // ── Spatial system (multiples of line-height) -- let margin-vertical = if margin-y != none { margin-y } else { line-height * 2.2 } let margin-horizontal = if margin-x != none { margin-x } else { line-height * 2.8 } let header-gap = if header-skip != none { header-skip } else { line-height * 1.1 } // ── Configure metadata -- set document(title: title, date: date) if author != none { set document(author: author) } // ── Configure text -- set text( font: body-font, size: font-size, fill: foreground-color, ligatures: true, discretionary-ligatures: true, lang: language, ) show raw: set text(font: raw-font) // ── Configure paragraphs -- set par( leading: line-height - font-size, spacing: line-height, justify: true, justification-limits: ( spacing: ( min: 100% * 2 / 3, max: 100% * 3 / 2, ), tracking: ( min: -0.01em, max: 0.01em, ), ), ) // ── Configure page -- set page( paper: paper-size, fill: background-color, margin: ( x: margin-horizontal, top: margin-vertical, bottom: margin-vertical, ), header: context { if counter(page).get().first() > 1 { v(header-gap) grid( columns: (1fr, 1fr), align(left, text( font: heading-font, size: caption-size, fill: secondary-content-color, weight: "medium", if author != none { author } else { "" }, )), align(right, text( font: heading-font, size: caption-size, fill: secondary-content-color, weight: "medium", title, )), ) v(line-height / 2) } }, header-ascent: line-height / 2, footer: context { v(line-height / 2) if page-numbering-started.at(here()) { align(right, text( font: heading-font, size: caption-size, fill: secondary-content-color, counter(page).display("1 / 1", both: true), )) } }, footer-descent: line-height, ) // ── Configure headings -- show heading.where(level: 1): it => context { v(line-height * 2, weak: true) text( font: heading-font, size: heading-size, weight: "bold", fill: accent-color, it.body, ) } show heading.where(level: 2): it => context { v(line-height * 2, weak: true) text( font: heading-font, size: lead-size, weight: "bold", fill: foreground-color, it.body, ) } show heading.where(level: 3): it => { v(line-height, weak: true) text( font: heading-font, size: font-size, weight: "bold", fill: foreground-color, it.body, ) v(line-height / 2, weak: true) } // ── Configure lists -- set list( indent: 0pt, body-indent: line-height / 2, marker: ( text( font: heading-font, fill: accent-color, weight: "bold", )[•], text( font: heading-font, fill: accent-color, weight: "bold", )[‣], text( font: heading-font, fill: accent-color, weight: "bold", )[–], ), ) set enum( indent: 0pt, body-indent: line-height / 2, numbering: (..nums) => text( font: heading-font, fill: accent-color, weight: "medium", numbering("1.a.i.", ..nums), ), ) set terms( indent: 0pt, separator: h(0.5em, weak: true), ) show terms.item: it => par({ text( font: heading-font, weight: "medium", fill: accent-color, it.term, ) h(0.5em) it.description }) // -- Configure block quotes -- show quote.where(block: true): it => { v(line-height, weak: true) block(width: 100%)[ #place(left + top, dy: -line-height / 2, text( font: heading-font, size: display-size * 2, fill: block-bg-color, weight: "bold", ["], )) #pad(left: lead-size)[ #text(font: body-font, style: "italic", it.body) #if it.attribution != none { v(line-height / 2, weak: true) align(right, text( font: heading-font, size: caption-size, fill: secondary-content-color, [— #it.attribution], )) } ] ] v(line-height, weak: true) } // -- Configure tables -- set table( stroke: none, inset: (x: line-height / 2, y: line-height / 2), fill: (x, y) => if y == 0 { accent-color } else if calc.odd(y) { block-bg-color } else { none }, ) show table.cell.where(y: 0): it => { text( font: heading-font, weight: "bold", size: caption-size, fill: background-color, it, ) } // ── Configure code -- show raw.where(block: true): block.with( inset: (x: line-height, y: line-height / 2), width: 100%, fill: block-bg-color, ) show raw.where(block: true): set par(justify: false) show raw.where(block: false): box.with( inset: (x: 3pt, y: 0pt), outset: (y: 3pt), fill: block-bg-color, ) // ── Configure links -- show link: it => { if type(it.dest) != str { it } else { text(fill: link-color, it) } } // ── Title -- if cover-page { // Full cover page page( margin: 0pt, header: none, footer: none, )[ #place(left + top, rect( width: line-height, height: 100%, fill: accent-color, )) #pad( left: line-height + margin-horizontal, right: margin-horizontal, top: margin-vertical * 2, bottom: margin-vertical, )[ #v(1fr) #text( font: heading-font, size: title-size, weight: "bold", upper(title), ) #v(line-height) #if subtitle != none { text( font: heading-font, size: heading-size, weight: "light", subtitle, ) v(line-height) } #v(2fr) #if author != none { grid( columns: (1fr, 1fr), text( font: heading-font, size: caption-size, weight: "medium", author, ), align(right, if date != none { text( font: heading-font, size: caption-size, fill: secondary-content-color, date.display(date-format), ) }), ) } else if date != none { text( font: heading-font, size: caption-size, fill: secondary-content-color, date.display(date-format), ) } ] ] } else if inline-title { // Inline title block on first page // Accent bar bleeding to page edges place( left + top, dx: -margin-horizontal, dy: -margin-vertical, rect( width: line-height, height: 100% + margin-vertical * 2, fill: accent-color, ), ) // Title block v(line-height) text( font: heading-font, size: display-size, weight: "bold", upper(title), ) v(line-height / 2) if subtitle != none { text( font: heading-font, size: lead-size, weight: "light", subtitle, ) v(line-height / 2) } if author != none { grid( columns: (1fr, 1fr), text( font: heading-font, size: caption-size, weight: "medium", author, ), align(right, if date != none { text( font: heading-font, size: caption-size, fill: secondary-content-color, date.display("[day] [month repr:long] [year]"), ) }), ) } else if date != none { text( font: heading-font, size: caption-size, fill: secondary-content-color, date.display("[day] [month repr:long] [year]"), ) } v(line-height * 2) } // ── Body -- body }