#let transit-red = rgb("#c53a2f") #import "theme.typ": color-quote, code-block-radius, code-inline-radius, quote-bar-stroke, quote-bar-color, color-body-text, color-block-bg, tw #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, show-header: false, // -- 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"), body-text-color: none, 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 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 -- let body-fill = if body-text-color != none { body-text-color } else { foreground-color } set text( font: body-font, size: font-size, fill: body-fill, 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: if show-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) } } } else { none }, header-ascent: if show-header { line-height / 2 } else { 0pt }, 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 -- let report-heading(it, size, above, below, title-fill: foreground-color) = block( width: 100%, breakable: false, above: above, below: below, )[ #context { if it.numbering != none { let number = counter(heading).display(it.numbering) text(font: heading-font, size: size, weight: "bold")[ #text(fill: accent-color)[#number] #h(0.35em) #text(fill: title-fill)[#it.body] ] } else { text( font: heading-font, size: size, weight: "bold", fill: title-fill, it.body, ) } } ] show heading.where(level: 1): it => { report-heading(it, heading-size, line-height * 2, line-height, title-fill: accent-color) } show heading.where(level: 2): it => { report-heading(it, lead-size, line-height * 2, line-height * 0.85) } show heading.where(level: 3): it => { report-heading(it, font-size, line-height, line-height * 0.75) } show heading.where(level: 4): it => { report-heading(it, caption-size, line-height * 0.85, line-height * 0.65) } // ── 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 * 0.75, weak: true) block(width: 100%)[ #block( width: 100%, inset: (left: tw(3.5)), stroke: (left: quote-bar-stroke + quote-bar-color), )[ #text(font: body-font, fill: color-body-text, 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 * 0.75, weak: true) } // -- Configure tables -- show table: it => { if it.has("label") and it.label == { it } else { [#table( columns: (1fr,) * it.columns.len(), ..it.children, )#label("table-full-width")] } } set table( stroke: none, inset: (x: line-height / 2, y: line-height / 2), fill: (x, y) => if y == 0 { accent-color } else if calc.even(y) { color-block-bg } 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: color-block-bg, radius: code-block-radius, ) 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: color-block-bg, radius: code-inline-radius, ) show raw.where(block: false): set text(fill: foreground-color) // ── Configure links -- show link: it => { if type(it.dest) != str { it } else { text(fill: accent-color, underline(it.body)) } } // ── 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 }