grelin_templates/tech-report/data/metropole-grelin.typ
MORRO d83e6a99a9 Add tech-report and travel-plan templates, replacing feasibility.
Introduce tech-report/ with cover page, custom TOC, and summary flow; add travel-plan/ for trip planning; update README, gitignore, and brand colors.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 13:56:22 +08:00

445 lines
10 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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
}