name: quarto-style description: Style guide for writing and editing Quarto (.qmd) documents in this project. Apply when creating or modifying any .qmd file. user-invocable: false
Quarto Style Guide
This skill defines conventions for all .qmd files in the project. Apply these rules when creating or editing Quarto documents.
Two document classes
This guide serves two kinds of document, with different rendering targets:
- Notebooks (
notebooks/*.qmd): render to HTML + built-in Typst (seenotebooks/_metadata.yml). They may use the full portable article-layout toolkit (margin notes, margin figures, page-width floats, sidenotes). - The paper (
index.qmd): renders to academic-typst (the vendored_extensions/estebandegetau/academic/format). It is single-column and does not support margin layout.
Two consequences shape everything below:
- Article layout is notebook presentation only. Margin and full-width features render in HTML and built-in Typst, but the paper ignores them. Never rely on margin layout to carry meaning that must survive into the paper.
- The bridge to the paper is the figure / table / equation conventions. Label prefixes (
fig-,tbl-,eq-), the caption style,tt()tables, and thetar_read()graduation workflow are identical across both classes, so content drafted in a notebook transplants into the paper unchanged.
Configuration Inheritance
Settings cascade from project-level down. Do not repeat inherited settings in individual .qmd files.
Inheritance chain
_quarto.yml (project-wide defaults)
├── docs/_metadata.yml (docs/ directory defaults)
├── notebooks/_metadata.yml (notebooks/ directory defaults)
└── reports/_metadata.yml (reports/ directory defaults)
└── individual.qmd (only document-specific overrides)
Before writing a .qmd file
Read the relevant config files to know what's already inherited:
- Always read
_quarto.ymlfor project-wide defaults (citations, execute, layout options) - Read the directory's
_metadata.yml(e.g.,reports/_metadata.yml) for format and execute defaults - Do not repeat any setting that is already defined at a higher level
Two settings worth knowing: _quarto.yml already sets reference-location: margin (so footnotes render as sidenotes), and notebooks/_metadata.yml already declares both html and typst formats. Notebook layout defaults belong in notebooks/_metadata.yml (notebook-only), never in _quarto.yml (site-wide).
What individual .qmd files should specify
Only settings unique to that document:
---
title: "Document Title"
subtitle: "Optional Subtitle"
date: today # or explicit date; author's choice
date-format: long # always include this
---
Override directory defaults only when necessary (e.g., number-sections: true for a proposal).
Setup Chunk
Include the targets setup block only when the document reads pipeline data.
Use pacman::p_load() instead of repeated library() calls. It installs missing packages and loads them in one call.
#| label: setup
#| cache: false
pacman::p_load(targets, tidyverse, tinytable, here)
here::i_am("path/to/this-document.qmd")
tar_config_set(store = here("_targets"))
source(here("R/tt_theme.R"))
set_theme(theme_minimal())
# Load data
data <- tar_read(target_name)
Documents that don't use pipeline data (e.g., pure prose proposals) skip this entirely.
Tables
Always use tinytable (tt()) for tables. Never use gt, kableExtra, or markdown tables. tinytable has native Typst support and is the project standard.
Project theme
All tables must end with |> tt_theme_report() (defined in R/tt_theme.R, sourced in setup). This applies booktabs-style formatting: centered, no row lines, header separator and bottom rule only. The helper is format-agnostic, so the same call renders correctly in both HTML and Typst.
Required pattern:
#| label: tbl-descriptive-name
#| tbl-cap: "**Pool summary**. Row counts at each filtering stage."
data |>
rename(`Readable Name` = col1) |>
tt() |>
tt_theme_report()
Rename columns upstream via dplyr::rename() (or setNames()) before piping into tt(). tinytable has no cols_label() analog and uses the data-frame's existing column names.
Rules
- Always set both
label: tbl-{ref}andtbl-cap:chunk options. In Typst, a tinytable caption only renders when the chunk has atbl-label, so the label is mandatory, not optional. - Reference tables in text with
@tbl-{ref}(e.g.,@tbl-descriptive-name) - Do not set captions on the table object; always use Quarto's
tbl-cap:chunk option so Quarto handles numbering and cross-references - Always pipe
tt_theme_report()as the last step in the tinytable chain - Let tables take their natural width; use
tt(width = 0.8)for page-fraction control when needed - Use
tinytable::footnote_tt()for methodological notes - Use
tinytable::style_tt()for conditional formatting when it aids interpretation - Use
tinytable::format_tt(fmt = "%.1f%%"),format_tt(fmt = "%.0f"), etc. (sprintf-style format strings) for consistent number formatting gt()does not render in Typst. Some older notebooks still usegt()(sourcingR/gt_theme.R). When you next edit such a notebook, migrate its tables tott()so it renders to both formats.- tinytable is fragile inside
results: 'asis'chunks. Build tables in a normal (non-asis) chunk withtbl--labelled chunk options so Quarto handles numbering and the caption. If a table genuinely must be emitted from anasis/cat()loop, render to confirm it compiles in both HTML and Typst before relying on it; do not assume it works from the HTML render alone. - Use
group_tt()for grouped, bolded/indented row groups. For self-contained tables with row-group labels, usetinytable::group_tt(i = ...)rather than hand-built markdown headings or manual indentation, so the grouping (bold label + indented members) survives into Typst.
Caption style (cross-format bridge)
Write every fig-cap and tbl-cap as a bold title, a period, then a sentence-case description:
"**Bold title**. Sentence-case description ending in a period."
This matches the academic-typst paper, so captions transplant into index.qmd unchanged. Never put titles in labs(title = ...) or tt(caption = ...); always use the chunk caption so Quarto numbers and cross-references them.
Plots
Set the global theme once in the setup chunk via set_theme(theme_minimal()). Individual plots should not add + theme_minimal().
#| label: fig-descriptive-name
#| fig-cap: "**Surface-form variance**. Distinct C1 forms per gold act (top 25)."
variance_per_act |>
ggplot(aes(n_surface_forms, gold_act_name)) +
geom_col() +
labs(
x = "Distinct surface forms",
y = NULL
)
- Always set both
label: fig-{ref}andfig-cap:chunk options; reference figures with@fig-{ref} - Use the caption style above (
**Title**. Description.); do not put the title inlabs(title = ...) - Always provide axis labels via
labs() theme_minimal()is set globally in setup; do not repeat per plot- Use
scales::comma,scales::percent,scales::dollar_format()for axis formatting - Write draft plot code as a single self-contained pipeline over one input data frame. This lets it lift into a function with no rewrite when the figure graduates to a target (see below).
Figure & Table Lifecycle
Figures and tables move through three stages. Drafting happens in the notebook; once a figure or table is stable and bound for the paper, it graduates into a target that both the notebook and index.qmd read.
Stage 1: Draft (inline)
Author the figure or table directly in the notebook chunk, written as one pipeline over a single input data frame (no references to several loose notebook objects):
#| label: fig-foo
#| fig-cap: "**Title**. Description."
my_input |> # one data frame in
mutate(...) |>
ggplot(aes(...)) +
geom_*()
Stage 2: Graduate (target)
Once the figure/table is stable and paper-bound:
Move the pipeline into a pure function in
R/(e.g.R/figures_c0.R). Figures return a ggplot object; tables return tidy data (applytt() |> tt_theme_report()in the consuming chunk, not in the function).# R/figures_c0.R plot_variance_per_act <- function(variance_per_act) { variance_per_act |> ggplot(aes(n_surface_forms, gold_act_name)) + geom_col() + labs(x = "Distinct surface forms", y = NULL) }Add a target in
_targets.R, prefixedfig_/tbl_and codebook-scoped:tar_target(fig_c0_variance_per_act, plot_variance_per_act(variance_per_act))
Stage 3: Consume (both documents)
The notebook and index.qmd read the same target. The caption and label live in the consuming chunk, since the paper usually re-words captions:
#| label: fig-variance-per-act
#| fig-cap: "**Surface-form variance**. Distinct C1 forms per gold act."
tar_read(fig_c0_variance_per_act)
Notes:
- Tables: store tidy data, never a
ttobject. Build the table in the chunk. This avoids coupling the target to a single output format and sidesteps the tinytable caption-needs-label quirk. - Theme applies at print time in the consuming session. With visual identity deferred, each document's
set_theme()governs. When a shared ggplot theme is later standardized, sourcing it in both setup chunks restyles every graduated figure with no target rebuild. - This is consistent with the project's data-generation policy: plotting logic lives in
R/, the object is produced by atar_target, and functions stay pure (no side effects).
Article Layout
These features render in both HTML and built-in Typst, so they are safe for notebooks. They are notebook presentation only; the academic-typst paper is single-column and ignores them.
Use only the portable subset below. Do not use directional column variants (.column-*-left, .column-*-right) or sidebar-width; those are HTML-only.
| Purpose | Chunk form | Div / inline form |
|---|---|---|
| Margin note | — | [text]{.aside} or ::: {.aside} … ::: or ::: {.column-margin} … ::: |
| Margin figure/table | #| column: margin |
::: {.column-margin} … ::: |
| Caption in margin | #| fig-cap-location: margin / #| tbl-cap-location: margin |
— |
| Page-width | #| column: page |
::: {.column-page} … ::: |
| Slightly wider | #| column: body-outset |
::: {.column-body-outset} … ::: |
| Full-bleed | #| column: screen-inset |
::: {.column-screen-inset} … ::: |
Margin notes (both forms):
The exogenous share rose sharply.[^margin-fn] An inline aside also works.[noisy due to small N]{.aside}
[^margin-fn]: Footnotes render in the margin as sidenotes (`reference-location: margin`).
::: {.column-margin}
A `.column-margin` block holds caveats, a definition, or a small figure without breaking body flow.
:::
Margin figure:
#| label: fig-margin-demo
#| fig-cap: "**Side-note figure**. A compact plot in the margin."
#| column: margin
ggplot(df, aes(x, y)) + geom_point()
Page-width table:
#| label: tbl-wide-results
#| tbl-cap: "**Full results**. All folds by metric."
#| column: page
results |> tt() |> tt_theme_report()
Footnotes already render in the margin project-wide (reference-location: margin). To put citations in the margin too, add citation-location: margin at the document level.
Equations
Label display equations by placing the label after the closing $$, and reference with @eq-name:
$$
d_{\text{euc}} = \sqrt{2 \, d_{\text{cos}}}
$$ {#eq-euc}
As shown in @eq-euc, the Euclidean and cosine distances are monotonically related.
Typst caveat: labeled equations with heavy LaTeX can fail to convert to Typst math (Quarto issue #7744), and notebooks have no mitex import. Therefore:
- Keep paper-bound equations to standard, Typst-translatable LaTeX.
- Add a
{#eq-}label only to equations you actually cross-reference. - Render the notebook to Typst to confirm an equation compiles before relying on it for the paper.
Markdown Formatting
Blank lines before block elements (CRITICAL)
A blank line is required before bullet lists, numbered lists, block quotes, and code blocks. This applies in both markdown body text and cat() output in R chunks.
<!-- WRONG -->
**Some text:**
- Bullet 1
- Bullet 2
<!-- CORRECT -->
**Some text:**
- Bullet 1
- Bullet 2
In R code chunks with results='asis':
# WRONG
cat("Some text:\n")
cat("- Bullet 1\n")
# CORRECT
cat("Some text:\n\n")
cat("- Bullet 1\n")
Horizontal rules
NEVER end a section or document with a --- divider. Horizontal rules should only appear between two sections of content, never trailing after the last paragraph of a section. When in doubt, omit the rule entirely. Prefer using headings (##, ###) to separate content rather than horizontal rules.
Callout boxes
Use Quarto callout boxes for important notes, warnings, or tips:
::: {.callout-note}
Important context the reader should know.
:::
::: {.callout-warning}
A caveat or limitation.
:::
Cross-references
Use Quarto cross-reference syntax for figures, tables, equations, and sections when the document is long enough to benefit from it.
- Figures:
label: fig-{ref}+fig-cap:in chunk options, reference with@fig-{ref} - Tables:
label: tbl-{ref}+tbl-cap:in chunk options, reference with@tbl-{ref} - Equations:
{#eq-{ref}}after the closing$$, reference with@eq-{ref} - Sections:
{#sec-label}on heading, reference with@sec-label
Cross-references work regardless of column placement; @fig-/@tbl- resolve normally even for floats set to column: margin or column: page.
Citations
- Use
@keysyntax for in-text citations:@romer2010,@halterman2025 - Use
[@key]for parenthetical citations:[@romer2010] - All references go in
references.bibat the project root - Citation style and bibliography are configured in
_quarto.yml; do not override in individual files - Footnotes already render in the margin (
reference-location: margin); addcitation-location: marginif you want citations in the margin too - End documents that use citations with:
## References {.unnumbered}
::: {#refs}
:::
Writing Style
Tone
- Active voice preferred over passive
- Concise and direct. Say what happened and what it means
- Write for an informed reader (World Bank economist) who is not a machine learning specialist
- Explain technical LLM concepts; assume fiscal policy knowledge
Emphasis
- Bold for key terms, findings, and important numbers on first use
- Italics for emphasis within sentences and for terms being defined
- Do not overuse either; if everything is bold, nothing stands out
Dashes
Minimize use of em dashes. Prefer shorter sentences, colons, or parentheses instead.
<!-- Avoid -->
The model achieved 92% accuracy --- exceeding the target --- on the test set.
<!-- Prefer -->
The model achieved 92% accuracy, exceeding the target, on the test set.
The model achieved 92% accuracy (exceeding the target) on the test set.
Numbers and metrics
- Report percentages with one decimal:
92.3%, not92.307% - Always include the target alongside the result: "92.3% accuracy (target: >85%)"
- Use
scales::comma()for large numbers in R output - Dollar amounts in billions:
$8.4Bin prose,scales::dollar_format(suffix = "B")in code
Structure
- No mandatory section template. Structure follows the document's purpose
- When a summary/overview section is included, place it first
- End with References section (if citations used) and optionally a Technical Appendix
Code Chunks
Execute options
Default execute options are set in _metadata.yml per directory (read it first). Override in individual chunks only when needed:
#| echo: true # Show this specific chunk in a report
#| cache: true # Cache expensive computation
#| fig-width: 8 # Control figure size
#| fig-height: 5
#| column: page # Article-layout placement (page, margin, body-outset, screen-inset)
#| fig-cap-location: margin # Caption in the margin (fig-cap-location / tbl-cap-location)
Dynamic markdown
When generating markdown from R, use results: 'asis' and cat():
#| results: 'asis'
cat(sprintf("**Result:** %.1f%% accuracy\n\n", value * 100))
# Remember blank line before bullets
cat("**Key findings:**\n\n")
cat("- Finding 1\n")
cat("- Finding 2\n")
Chunk labels
Use Quarto's fig- and tbl- prefixes so cross-references work automatically:
#| label: fig-magnitude-distribution
#| fig-cap: "**Magnitude distribution**. Fiscal shock sizes across the corpus."
#| label: tbl-codebook-performance
#| tbl-cap: "**Codebook performance**. Metrics across all stages."
Labels must be descriptive kebab-case with the appropriate prefix (fig- or tbl-). Chunks that produce neither a figure nor a table use plain kebab-case labels (e.g., label: setup, label: load-data).