feat: add Quarto themes and templates (revealjs, HTML, PDF, Typst)
R-CMD-check / R CMD check (push) Successful in 3m52s

Integrate Civilytics Reveal.js slide extension, HTML/PDF/Typst document
themes, and _brand.yml into the package under inst/quarto/. Three new
helper functions (use_civilytics_revealjs, use_civilytics_theme,
use_civilytics_brand) copy assets into a user's Quarto project. Logos
are stored once in inst/img/ and distributed at install time.
This commit is contained in:
2026-05-20 09:39:27 -06:00
parent 41fe2b6170
commit 136c53643c
25 changed files with 3641 additions and 2 deletions
+66
View File
@@ -0,0 +1,66 @@
# Quarto 1.5+ brand file
# Optional but recommended — Quarto auto-applies these to HTML, PDF, and Revealjs.
# https://quarto.org/docs/authoring/brand.html
brand:
meta:
name: Civilytics Consulting
description: Turning public data into clear, actionable analysis for public good.
logo:
small: assets/logo/civilytics-mark.svg
medium: assets/logo/civilytics-wordmark.svg
large: assets/logo/civilytics-wordmark.svg
color:
palette:
paper: "#FAF7F2"
paper-2: "#F2EDE4"
ink: "#0E1A2B"
ink-2: "#2B3A52"
ink-3: "#5A6A82"
navy: "#22406A"
ember: "#C25311"
ember-2: "#923D00"
teal: "#1F6F70"
plum: "#6B3A5E"
moss: "#4A6B2F"
brass: "#B8751C"
background: paper
foreground: ink
primary: navy
secondary: ember
success: moss
info: navy
warning: "#9A5F18"
danger: "#A6271D"
light: paper-2
dark: ink
link: navy
typography:
fonts:
- family: "Source Serif 4"
source: google
- family: "Inter"
source: google
- family: "JetBrains Mono"
source: google
- family: "Libre Franklin"
source: google
base:
family: Inter
size: 1rem
line-height: 1.65
headings:
family: "Libre Franklin"
weight: 800
style: normal
line-height: 1.08
color: ink
monospace:
family: "JetBrains Mono"
size: 0.92em
link:
color: navy
decoration: underline
+80
View File
@@ -0,0 +1,80 @@
---
title: "Who pays when rent outpaces wages?"
subtitle: "A 12-county analysis of cost-burdened renter households, 2019–2024."
author:
- name: "Civilytics Research"
affiliation: "Civilytics Consulting"
date: "2026-04-15"
abstract: |
We combine ACS 5-year microdata with HUD Fair Market Rent estimates
to track changes in rent burden across 12 metropolitan counties.
Between 2019 and 2024, the share of renter households spending more
than 30% of income on housing rose by 6.4 percentage points; the
increase was concentrated in counties where median wages stagnated.
categories: [Housing, Affordability, ACS]
format:
html:
theme: ../theme/civilytics.scss
css: ../theme/extras.css
toc: true
toc-location: right
typst:
template: ../typst/civilytics-typst.typ
pdf:
include-in-header: ../latex/civilytics.tex
include-before-body: ../latex/civilytics-title.tex
execute:
echo: false
warning: false
---
## Findings at a glance
::: {.stat-callout}
::: {}
[6.4 pp]{.stat-num}
[Increase in rent-burdened share, 2019→2024]{.stat-label .accent}
:::
::: {}
[1 in 2]{.stat-num}
[Renters in the studied counties now cost-burdened]{.stat-label}
:::
::: {}
[$412]{.stat-num}
[Median monthly rent gap, lowest-wage quintile]{.stat-label}
:::
:::
[Methodology note]{.eyebrow}
## Background
Rent has outpaced wages in every county we studied. In some, the gap is
modest and recent; in others, it has been widening for a decade. This
report disaggregates the trend along three lines: county, income
quintile, and household composition.
```{r}
#| label: fig-trend
#| fig-cap: "Cost-burdened renter share, 12 metro counties, 2019–2024."
#| fig-width: 7
#| fig-height: 4.2
library(ggplot2); library(civilytics)
ggplot(economics, aes(date, unemploy / pop)) +
geom_line(color = "#C25311", linewidth = 1) +
labs(title = "Cost-burdened renter share, 2019–2024",
subtitle = "Share spending more than 30% of household income on rent",
x = NULL, y = NULL,
caption = "Source: ACS 5-yr microdata; Civilytics analysis.") +
scale_y_continuous(labels = scales::percent_format()) +
theme_civilytics()
```
> Across all 12 counties, the steepest increases came in places where
> median wages were already in the bottom quartile of the metro region.
## Recommendations
1. Targeted rental assistance keyed to local wage trajectories.
2. Quarterly publication of rent-burden indicators by county.
3. Coordinated reporting between HUD and county human-services offices.
+65
View File
@@ -0,0 +1,65 @@
---
title: "Quarterly Analysis"
subtitle: "A brief look at the numbers."
author: "Civilytics Consulting"
date: today
format: civilytics-reveal-revealjs
---
## Findings at a glance
::: {.columns}
::: {.column width="50%"}
[Key metric]{.stat-number}
- Point one
- Point two
- Point three
:::
::: {.column width="50%"}
```{r}
#| fig-width: 6
#| fig-height: 4
library(ggplot2)
library(civilytics)
ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point(size = 2.5) +
scale_color_civilytics() +
theme_civilytics_slide() +
labs(title = "Engine Size vs Fuel Economy",
x = "Displacement (L)", y = "Highway MPG")
```
:::
:::
## Section divider {.section}
Big idea goes here.
## Data table
| County | Burden (%) | Change |
|:-------------|:----------:|-------:|
| Dane | 38.2 | +4.1 |
| Milwaukee | 52.7 | +7.3 |
| Hennepin | 41.5 | +3.9 |
: Cost-burdened renter share, selected counties.
## A blockquote slide {.pullquote}
> Rent has outpaced wages in every county we studied.
Civilytics Research, 2026
## Thank you {.thank-you}
Questions?
- jared@civilytics.com
- civilytics.consulting
@@ -0,0 +1,56 @@
title: Civilytics Reveal
author: Civilytics Consulting
version: 1.0.0
quarto-required: ">=1.4.0"
contributes:
formats:
revealjs:
# ---- Theme ----
theme: [default, civilytics.scss]
# ---- Slide geometry ----
width: 1280
height: 720
margin: 0
min-scale: 0.2
max-scale: 2.0
center: false
# ---- Navigation / chrome ----
controls: true
controls-layout: edges
progress: true
slide-number: "c/t"
hash-type: number
history: true
navigation-mode: linear
transition: fade
transition-speed: fast
background-transition: fade
# ---- Code ----
highlight-style: github
code-line-numbers: true
code-copy: true
code-overflow: wrap
# ---- Math ----
html-math-method: mathjax
# ---- Tables ----
tbl-cap-location: top
# ---- Links / fragments ----
link-external-newwindow: true
incremental: false
# ---- Brand chrome (injected by civilytics.js) ----
include-in-header:
- file: civilytics-head.html
include-after-body:
- file: civilytics-after.html
# ---- Footer / logo ----
# Override in the document front-matter if desired:
# footer: "Civilytics Consulting · 2026"
# logo: _extensions/civilytics-reveal/assets/civilytics-mark.svg
@@ -0,0 +1,80 @@
<!-- Civilytics Reveal — brand chrome injector -->
<script>
(function () {
// The Civilytics wordmark is the bold serif "Civilytics" lockup with the pulse
// waveform riding inline at the end. It ships as a baked SVG in two variants:
// ink-on-paper (default) and paper-on-ink (reverse) for dark backgrounds.
// We render it as an <img> so the official artwork is preserved exactly —
// the file picks up its color from the SVG itself, not from CSS currentColor.
var ASSET_BASE = (function () {
// Find the URL this script's <link rel="stylesheet" href="..civilytics.css..">
// was loaded from, so we can resolve sibling assets the same way.
var links = document.querySelectorAll('link[rel="stylesheet"]');
for (var i = 0; i < links.length; i++) {
var href = links[i].getAttribute('href') || '';
var m = href.match(/^(.*\/)civilytics\.css(\?|$)/);
if (m) return m[1] + 'assets/';
}
// Fallback: assume Quarto-style sibling layout
return '_extensions/civilytics-reveal/assets/';
})();
function makeWordmarkImg(reverse) {
var img = document.createElement('img');
img.className = 'civilytics-wordmark';
img.alt = 'Civilytics';
img.src = ASSET_BASE + (reverse ? 'civilytics-wordmark-reverse.svg' : 'civilytics-wordmark.svg');
return img;
}
function injectWordmark(slide, reverse) {
if (!slide) return;
if (slide.querySelector(':scope > .civilytics-wordmark')) return;
slide.insertBefore(makeWordmarkImg(reverse), slide.firstChild);
}
function init() {
if (!window.Reveal) { setTimeout(init, 50); return; }
var root = document.querySelector('.reveal');
if (!root) return;
// Title slide — paper background, ink wordmark
document.querySelectorAll(
'.slides #title-slide, .slides section.title-slide'
).forEach(function (s) { injectWordmark(s, false); });
// Thank-you slide — ink background, reversed wordmark
document.querySelectorAll('.slides section.thank-you').forEach(function (s) {
injectWordmark(s, true);
});
// Brand footer (pulse glyph + "Civilytics") bottom-left on body slides.
// Uses inline SVG so it inherits the slide's text color via currentColor.
if (!root.querySelector('.civilytics-footer')) {
var footer = document.createElement('div');
footer.className = 'civilytics-footer';
footer.setAttribute('aria-hidden', 'true');
footer.innerHTML =
'<svg viewBox="0 0 30 14" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter">' +
'<path d="M0 8 L5 8 L8 3 L12 13 L16 1 L20 11 L23 8 L30 8"/>' +
'</svg>' +
'<span>Civilytics</span>';
root.appendChild(footer);
}
function updateChrome() {
var current = Reveal.getCurrentSlide();
if (!current) return;
var hide = current.matches('.title-slide, #title-slide, .section-divider, .section, .thank-you, .full-bleed, .no-chrome');
document.body.classList.toggle('civilytics-hide-chrome', !!hide);
}
Reveal.on('ready', updateChrome);
Reveal.on('slidechanged', updateChrome);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
@@ -0,0 +1,3 @@
<!-- Civilytics Reveal — fonts preconnect (theme CSS also @imports, this just warms the connection) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
% Civilytics — title-page partial.
% Replaces Quarto's default \maketitle. Uses values from YAML
% (\thetitle, \theauthor, \thedate) plus an \ifabstract block.
\begin{titlepage}
\pagecolor{paper}
\color{ink}
\vspace*{0.5in}
{\sffamily\bfseries\scriptsize\color{ember}\MakeUppercase{— Civilytics Research}\par}
\vspace{12pt}
{\displayfont\fontsize{32pt}{34pt}\selectfont\bfseries\color{ink}\thetitle\par}
\vspace{8pt}
\ifx\thesubtitle\@empty\else
{\rmfamily\itshape\fontsize{14pt}{18pt}\selectfont\color{ink2}\thesubtitle\par}
\fi
\vspace{18pt}
\noindent\rule{\linewidth}{2pt}\par
\vspace{1pt}
\noindent{\color{rule}\rule{\linewidth}{0.4pt}}\par
\vspace{8pt}
\noindent
\begin{minipage}[t]{0.5\linewidth}
{\sffamily\scriptsize\color{ink3}\MakeUppercase{Authors}\par}
\vspace{2pt}
{\sffamily\small\color{ink}\theauthor\par}
\end{minipage}\hfill
\begin{minipage}[t]{0.4\linewidth}\raggedleft
{\sffamily\scriptsize\color{ink3}\MakeUppercase{Published}\par}
\vspace{2pt}
{\sffamily\small\color{ink}\thedate\par}
\end{minipage}\par
\vspace{1.5cm}
% Pulse mark, in ember
\begin{center}
\begin{tikzpicture}[overlay, remember picture]
\end{tikzpicture}
{\color{ember}\rule{40pt}{2pt}}
\end{center}
\end{titlepage}
\clearpage
+112
View File
@@ -0,0 +1,112 @@
% =============================================================
% Civilytics — LaTeX preamble for Quarto PDF
% Usage in YAML:
% format:
% pdf:
% include-in-header: quarto/latex/civilytics.tex
% include-before-body: quarto/latex/civilytics-title.tex
% Requires: xelatex or lualatex (for system fonts).
% =============================================================
\usepackage{xcolor}
\usepackage{fontspec}
\usepackage{titlesec}
\usepackage{titling}
\usepackage{fancyhdr}
\usepackage{enumitem}
\usepackage{booktabs}
\usepackage{caption}
\usepackage[hidelinks]{hyperref}
\usepackage{microtype}
\usepackage{tcolorbox}
\tcbuselibrary{skins,breakable}
% --- Civilytics palette ---
\definecolor{paper}{HTML}{FAF7F2}
\definecolor{paper2}{HTML}{F2EDE4}
\definecolor{ink}{HTML}{0E1A2B}
\definecolor{ink2}{HTML}{2B3A52}
\definecolor{ink3}{HTML}{5A6A82}
\definecolor{rule}{HTML}{D6CEBD}
\definecolor{ruleStrong}{HTML}{B8AE97}
\definecolor{navy}{HTML}{22406A}
\definecolor{ember}{HTML}{C25311}
\definecolor{emberDark}{HTML}{923D00}
\definecolor{teal}{HTML}{1F6F70}
\definecolor{plum}{HTML}{6B3A5E}
% --- Page color ---
\pagecolor{paper}
\color{ink}
% --- Fonts (require local install or fontspec lookup) ---
\setmainfont{Source Serif 4}[
UprightFont = *,
ItalicFont = * Italic,
BoldFont = * SemiBold,
BoldItalicFont = * SemiBold Italic,
Ligatures = TeX,
]
\setsansfont{Inter}[Ligatures = TeX]
\setmonofont{JetBrains Mono}[Scale = 0.92]
\newfontfamily\displayfont{Libre Franklin}[Ligatures = TeX]
% --- Hyperlinks ---
\hypersetup{
colorlinks = true,
linkcolor = navy,
urlcolor = navy,
citecolor = navy,
filecolor = navy,
}
% --- Section headings ---
\titleformat{\section}[block]
{\bfseries\Large\displayfont\color{ink}}
{}{0pt}
{\titlerule[0.4pt]\vspace{4pt}}[\vspace{-6pt}]
\titleformat{\subsection}[block]
{\bfseries\large\displayfont\color{ink}}{}{0pt}{}
\titleformat{\subsubsection}[block]
{\bfseries\normalsize\displayfont\color{ink}}{}{0pt}{}
% --- Page header / footer ---
\pagestyle{fancy}
\fancyhf{}
\renewcommand{\headrulewidth}{0pt}
\renewcommand{\footrulewidth}{0pt}
\fancyhead[L]{\sffamily\scriptsize\color{ink3}\MakeUppercase{Civilytics Consulting}}
\fancyhead[R]{\sffamily\scriptsize\color{ink3}\thetitle}
\fancyfoot[L]{\sffamily\scriptsize\color{ink3}civilytics.consulting}
\fancyfoot[C]{\sffamily\scriptsize\color{ink3}\thepage}
\fancyfoot[R]{\sffamily\scriptsize\color{ink3}\textcopyright\ 2026}
% --- Captions ---
\captionsetup{
font={sf,small,color=ink3},
labelfont={sf,bf,color=ink3},
labelsep=period,
justification=raggedright,
singlelinecheck=false,
}
% --- Pullquote ---
\newtcolorbox{pullquote}{
enhanced, breakable, frame hidden, colback=paper,
borderline west = {2pt}{0pt}{ember},
left=14pt, right=4pt, top=4pt, bottom=4pt,
fontupper={\itshape\rmfamily\large\color{ink}},
}
% --- Code blocks (Pandoc + listings or minted both inherit colors below) ---
\definecolor{codebg}{HTML}{0E1A2B}
\definecolor{codefg}{HTML}{FAF7F2}
% --- Lists tighter ---
\setlist{itemsep=2pt, topsep=4pt}
% --- Tabular numerals ---
\newcommand{\tnum}[1]{{\addfontfeature{Numbers={Tabular,Lining}}#1}}
% --- Ember accent rule above section starts ---
\newcommand{\emberrule}{{\color{ember}\rule{60pt}{3pt}}\par\vspace{4pt}}
+73
View File
@@ -0,0 +1,73 @@
/*-- scss:defaults --*/
// =============================================================
// Civilytics tokens — single source of truth for colors + type.
// Mirrored from colors_and_type.css so figures, themes, and
// preview pages stay in sync.
// =============================================================
// Neutrals — warm paper → civic ink
$paper: #FAF7F2 !default;
$paper-2: #F2EDE4 !default;
$paper-3: #E6DFD1 !default;
$rule: #D6CEBD !default;
$rule-strong: #B8AE97 !default;
$ink: #0E1A2B !default;
$ink-2: #2B3A52 !default;
$ink-3: #5A6A82 !default;
$ink-4: #8C97AB !default;
// Brand — Civic Navy
$navy-900: #0E1A2B; $navy-800: #132339; $navy-700: #1A2E4A;
$navy-600: #22406A; $navy-500: #2E5590; $navy-400: #4A74B0;
$navy-300: #7A9BCA; $navy-200: #B3C6E0; $navy-100: #DDE6F2; $navy-50: #EEF3FA;
// Accent — Ember
$accent-900: #451A00; $accent-800: #6B2B00; $accent-700: #923D00;
$accent-600: #C25311; $accent-500: #DB6C25; $accent-400: #EA8A49;
$accent-300: #F2A976; $accent-200: #F8C8A3; $accent-100: #FBE0C6; $accent-50: #FDF1E4;
// Supporting (data-viz)
$teal-600: #1F6F70; $teal-300: #7CB3B3; $teal-100: #D3E6E6;
$plum-600: #6B3A5E; $plum-300: #B392A7; $plum-100: #E7DAE1;
$moss-600: #4A6B2F; $moss-300: #9CB47D; $moss-100: #DEE8CF;
$brass-600: #B8751C; $brass-100: #F9E6C8;
// Semantic
$success: $moss-600; $warning: #9A5F18; $danger: #A6271D; $info: $navy-500;
// Bootstrap-friendly aliases (used by Quarto's HTML theme)
$body-bg: $paper !default;
$body-color: $ink !default;
$primary: $navy-600 !default;
$secondary: $accent-600 !default;
$success-bs: $success !default;
$info-bs: $info !default;
$warning-bs: $warning !default;
$danger-bs: $danger !default;
$light: $paper-2 !default;
$dark: $ink !default;
$link-color: $navy-600 !default;
$link-hover-color: $navy-700 !default;
// Type stacks
$font-display: 'Libre Franklin', 'Franklin Gothic', 'Inter', system-ui, sans-serif !default;
$font-serif: 'Source Serif 4', 'Source Serif Pro', Georgia, 'Times New Roman', serif !default;
$font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif !default;
$font-mono: 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace !default;
$font-family-sans-serif: $font-sans !default;
$font-family-monospace: $font-mono !default;
$font-family-base: $font-sans !default;
// Sizing
$font-size-base: 1rem !default;
$line-height-base: 1.65 !default;
// Borders + radii
$border-color: $rule !default;
$border-radius: 4px !default;
$border-radius-sm: 2px !default;
$border-radius-lg: 8px !default;
$border-radius-xl: 14px !default;
+331
View File
@@ -0,0 +1,331 @@
/*-- scss:defaults --*/
// =============================================================
// Civilytics — Quarto HTML report + website theme
// Built on Bootstrap defaults via Quarto's SCSS pipeline.
// Inherits all tokens from _tokens.scss.
// =============================================================
@import "_tokens.scss";
// Quarto-specific defaults
$navbar-bg: $paper;
$navbar-fg: $ink;
$navbar-hl: $accent-600;
$footer-bg: $ink;
$footer-fg: $paper;
$code-bg: $paper-2;
$code-color: $accent-700;
$pre-bg: $ink;
$pre-color: $paper;
// Tighter table look
$table-border-color: $rule;
$table-color: $ink;
// Headings inherit display family + tight tracking
$h1-font-size: 2.875rem;
$h2-font-size: 2.125rem;
$h3-font-size: 1.625rem;
$h4-font-size: 1.25rem;
$headings-font-family: $font-display;
$headings-font-weight: 800;
$headings-line-height: 1.08;
$headings-color: $ink;
$callout-color-note: $navy-600;
$callout-color-tip: $teal-600;
$callout-color-caution: $accent-700;
$callout-color-warning: $warning;
$callout-color-important: $danger;
/*-- scss:rules --*/
// =============================================================
// Editorial layer — overrides Bootstrap + Quarto defaults so
// rendered .qmd files look like Civilytics publications, not
// generic blog posts.
// =============================================================
@import url('https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@500;600;700;800;900&family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600;8..60,700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
body {
font-family: $font-sans;
font-size: 1rem;
line-height: 1.7;
color: $ink;
background: $paper;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
// Headings — display sans for headlines, tight tracking
h1, h2, h3, h4, h5, h6 {
font-family: $font-display;
letter-spacing: -0.025em;
text-wrap: balance;
color: $ink;
}
h1 { font-weight: 900; letter-spacing: -0.035em; line-height: 1.02; }
h2 { font-weight: 800; letter-spacing: -0.03em; margin-top: 2.5rem; padding-top: 1rem; border-top: 1px solid $rule; }
h3 { font-weight: 700; margin-top: 1.75rem; }
h4 { font-weight: 700; }
h5, h6 { font-family: $font-sans; font-weight: 600; text-transform: none; }
// Article-body prose — long-form reading uses Source Serif 4
.quarto-title-block { margin-bottom: 2.5rem; }
.quarto-title .title {
font-family: $font-display;
font-weight: 900;
font-size: 3.25rem;
line-height: 1.0;
letter-spacing: -0.04em;
margin-bottom: 0.75rem;
}
.quarto-title .subtitle {
font-family: $font-serif;
font-style: italic;
font-weight: 400;
font-size: 1.375rem;
color: $ink-2;
line-height: 1.4;
max-width: 60ch;
}
.quarto-title-meta {
font-family: $font-sans;
font-size: 0.8125rem;
color: $ink-3;
text-transform: uppercase;
letter-spacing: 0.08em;
border-top: 3px double $ink;
border-bottom: 1px solid $rule;
padding: 0.75rem 0;
margin-top: 1.5rem;
}
.quarto-title-meta .quarto-title-meta-heading { color: $ink-4; font-size: 0.6875rem; }
.quarto-title-meta .quarto-title-meta-contents { color: $ink; font-size: 0.875rem; text-transform: none; letter-spacing: 0; font-weight: 500; }
// Lead / abstract — italic serif lockup
.abstract, p.abstract {
font-family: $font-serif;
font-style: italic;
font-size: 1.25rem;
line-height: 1.5;
color: $ink;
border-left: 2px solid $accent-600;
padding-left: 1.25rem;
margin: 2rem 0 2.5rem;
max-width: 62ch;
}
// Body paragraphs in article
main p, main li {
font-family: $font-serif;
font-size: 1.0625rem;
line-height: 1.65;
color: $ink;
text-wrap: pretty;
}
main ul, main ol { padding-left: 1.5em; }
main li { margin-bottom: 0.4em; }
// Links — civic navy, thickening underline
a { color: $navy-600; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; transition: color 120ms ease; }
a:hover { color: $navy-700; text-decoration-thickness: 2px; }
a:visited { color: $plum-600; }
// Pullquote / blockquote
blockquote {
font-family: $font-serif;
font-style: italic;
font-size: 1.625rem;
line-height: 1.4;
color: $ink;
border-left: 3px solid $accent-600;
padding: 0.5rem 0 0.5rem 1.5rem;
margin: 2rem 0;
}
blockquote p { font-family: inherit !important; font-size: inherit !important; line-height: inherit !important; }
// Code — light by default
code, kbd, samp {
font-family: $font-mono;
font-size: 0.92em;
font-feature-settings: "tnum" 1, "zero" 1;
}
code:not(pre code) {
background: $paper-2;
color: $accent-700;
padding: 0.1em 0.4em;
border-radius: 2px;
border: 1px solid $rule;
}
// Code blocks — light variant (default), with a dark-emphasis option below
div.sourceCode, pre.sourceCode {
background: $paper-2;
border: 1px solid $rule;
border-radius: 8px;
}
div.sourceCode pre {
background: transparent;
color: $ink;
padding: 1rem 1.25rem;
font-size: 0.875rem;
line-height: 1.55;
margin: 0;
}
// Highlight tokens (Quarto/Pandoc default classes)
pre.sourceCode {
.kw { color: $navy-700; font-weight: 600; } // keyword
.dt { color: $teal-600; } // data type
.st { color: $accent-700; } // string
.co { color: $ink-3; font-style: italic; } // comment
.fu { color: $navy-600; } // function
.op, .sc { color: $ink-2; } // operator
.dv, .fl { color: $plum-600; } // numbers
.va { color: $brass-600; } // variable
}
// Dark variant — opt in by adding {.dark} to the code chunk
.cell.dark div.sourceCode,
div.sourceCode.dark,
pre.dark {
background: $ink;
border-color: $navy-800;
pre, code { color: $paper; }
.kw { color: lighten($navy-300, 5%); }
.dt { color: $teal-300; }
.st { color: $accent-300; }
.co { color: $ink-4; }
.fu { color: $navy-200; }
.op, .sc { color: $paper-3; }
.dv, .fl { color: $plum-300; }
}
// Tables — editorial grid
.table, table {
font-family: $font-sans;
font-size: 0.9375rem;
border-collapse: collapse;
width: 100%;
font-variant-numeric: tabular-nums;
margin: 1.5rem 0;
}
.table thead th, table thead th {
font-family: $font-sans;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: $ink-3;
border-top: 2px solid $ink;
border-bottom: 1px solid $rule-strong;
padding: 0.6rem 0.75rem;
text-align: left;
}
.table tbody td, table tbody td {
border-bottom: 1px solid $rule;
padding: 0.55rem 0.75rem;
color: $ink;
}
.table tbody tr:last-child td, table tbody tr:last-child td { border-bottom: 2px solid $ink; }
.table.numeric td, table .numeric { font-family: $font-mono; }
// Figures + captions
figure, .quarto-figure { margin: 2rem 0; }
figcaption, .figure-caption, .quarto-figcaption {
font-family: $font-sans;
font-size: 0.8125rem;
color: $ink-3;
line-height: 1.45;
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid $rule;
text-align: left;
}
// Callouts — keep Quarto markup, restyle as editorial sidebars
.callout {
border-radius: 8px;
border: 1px solid $rule;
background: $paper-2;
margin: 1.5rem 0;
border-left: 3px solid $navy-600;
}
.callout-note { border-left-color: $navy-600; }
.callout-tip { border-left-color: $teal-600; }
.callout-caution { border-left-color: $accent-700; }
.callout-warning { border-left-color: $warning; }
.callout-important { border-left-color: $danger; }
.callout-title { font-family: $font-sans; font-weight: 600; font-size: 0.9375rem; color: $ink; text-transform: none; letter-spacing: 0; }
.callout-body p { font-family: $font-sans !important; font-size: 0.9375rem !important; line-height: 1.55 !important; }
// Eyebrow / kicker (Quarto category renders as `.quarto-categories`)
.quarto-categories {
display: inline-flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.quarto-categories .quarto-category {
font-family: $font-sans;
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: $accent-600;
background: transparent;
border: none;
padding: 0;
}
.quarto-categories .quarto-category::before { content: "— "; color: $accent-600; }
// Navbar (used by website projects)
.navbar {
background: rgba(250, 247, 242, 0.92) !important;
backdrop-filter: blur(12px);
border-bottom: 1px solid $rule;
padding: 0.875rem 0;
}
.navbar-brand { font-family: $font-display; font-weight: 800; font-size: 1.125rem; letter-spacing: -0.02em; color: $ink !important; }
.navbar-nav .nav-link { font-family: $font-sans; font-weight: 500; font-size: 0.9375rem; color: $ink-2 !important; padding: 0.4rem 0.85rem !important; }
.navbar-nav .nav-link:hover, .navbar-nav .nav-link.active { color: $accent-600 !important; }
// Footer
.nav-footer, footer.footer {
background: $ink;
color: $paper;
font-family: $font-sans;
font-size: 0.875rem;
padding: 2rem 0;
border-top: 4px solid $accent-600;
}
.nav-footer a, footer.footer a { color: $paper; text-decoration: underline; text-decoration-thickness: 1px; }
// TOC — right-side sidebar
#TOC, .sidebar.toc-active, nav#TOC {
font-family: $font-sans;
font-size: 0.8125rem;
}
#TOC ul, nav#TOC ul { list-style: none; padding-left: 0.75rem; border-left: 1px solid $rule; }
#TOC li, nav#TOC li { margin: 0.25rem 0; }
#TOC a, nav#TOC a { color: $ink-3; text-decoration: none; }
#TOC a:hover, #TOC a.active, nav#TOC a:hover, nav#TOC a.active { color: $accent-600; border-left: 2px solid $accent-600; padding-left: 0.5rem; margin-left: -0.75rem; }
// HR — editorial divider
hr { border: none; border-top: 1px solid $rule; margin: 2.5rem 0; }
hr.section-break { border: none; height: 4px; background: linear-gradient(to right, $accent-600 60px, $rule 60px); margin: 3rem 0; }
// Listing pages (website blog/index)
.quarto-listing .listing-item {
background: white;
border: 1px solid $rule;
border-top: 3px solid $accent-600;
border-radius: 8px;
padding: 1.5rem;
transition: border-color 120ms ease;
}
.quarto-listing .listing-item:hover { border-color: $rule-strong; }
.quarto-listing .listing-title a { color: $ink; text-decoration: none; font-family: $font-display; font-weight: 800; }
.quarto-listing .listing-date { font-family: $font-sans; font-size: 0.75rem; color: $ink-3; text-transform: uppercase; letter-spacing: 0.08em; }
// Selection
::selection { background: $accent-200; color: $ink; }
+63
View File
@@ -0,0 +1,63 @@
/* Civilytics — extras layered on top of the SCSS theme.
Use for utilities that don't need to participate in the
Bootstrap variable cascade. */
/* Stat callout — drop into reports for hero numbers */
.stat-callout {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 2rem;
margin: 2rem 0 2.5rem;
padding: 1.5rem 0;
border-top: 3px double #0E1A2B;
border-bottom: 1px solid #D6CEBD;
}
.stat-callout .stat-num {
font-family: 'Source Serif 4', Georgia, serif;
font-weight: 700;
font-size: 3rem;
line-height: 1;
letter-spacing: -0.02em;
color: #0E1A2B;
font-variant-numeric: tabular-nums;
display: block;
}
.stat-callout .stat-label {
font-family: 'Inter', system-ui, sans-serif;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #5A6A82;
margin-top: 0.4rem;
}
.stat-callout .stat-label.accent { color: #C25311; }
/* Pulse glyph utility — matches design system */
.civilytics-pulse {
display: inline-block;
vertical-align: middle;
width: 30px;
height: 14px;
}
/* Section eyebrow — use above an h1/h2 */
.eyebrow {
font-family: 'Inter', system-ui, sans-serif;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #C25311;
margin: 0 0 0.5rem;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.eyebrow::before {
content: "";
display: inline-block;
width: 24px;
height: 2px;
background: #C25311;
}
+244
View File
@@ -0,0 +1,244 @@
// =============================================================
// Civilytics — Typst template for Quarto PDF
// Usage in YAML:
// format:
// typst:
// template: quarto/typst/civilytics-typst.typ
// =============================================================
#let paper-bg = rgb("#FAF7F2")
#let ink = rgb("#0E1A2B")
#let ink-2 = rgb("#2B3A52")
#let ink-3 = rgb("#5A6A82")
#let rule = rgb("#D6CEBD")
#let rule-strong = rgb("#B8AE97")
#let navy = rgb("#22406A")
#let ember = rgb("#C25311")
#let ember-dark = rgb("#923D00")
#let plum = rgb("#6B3A5E")
#let teal = rgb("#1F6F70")
#let serif-stack = ("Source Serif 4", "Source Serif Pro", "Georgia", "Times New Roman")
#let sans-stack = ("Inter", "Helvetica Neue", "Arial")
#let display-stack = ("Libre Franklin", "Inter", "Helvetica Neue")
#let mono-stack = ("JetBrains Mono", "Menlo", "Consolas")
#let civilytics(
title: none,
subtitle: none,
authors: (),
date: none,
abstract: none,
toc: true,
doc,
) = {
// Page setup
set page(
paper: "us-letter",
margin: (top: 1in, bottom: 1in, left: 1.1in, right: 1.1in),
fill: paper-bg,
header: context {
if counter(page).get().first() > 1 {
set text(font: sans-stack, size: 8pt, fill: ink-3, tracking: 0.06em)
upper[Civilytics Consulting]
h(1fr)
upper(if title != none { title } else { "" })
v(2pt)
line(length: 100%, stroke: 0.4pt + rule)
}
},
footer: context {
set text(font: sans-stack, size: 8pt, fill: ink-3)
line(length: 100%, stroke: 0.4pt + rule)
v(4pt)
grid(
columns: (1fr, auto, 1fr),
align: (left, center, right),
[civilytics.consulting],
counter(page).display("1 / 1", both: true),
[© 2026]
)
},
)
// Body text
set text(font: serif-stack, size: 10.5pt, fill: ink, lang: "en")
set par(justify: false, leading: 0.6em, first-line-indent: 0pt)
// Headings
show heading.where(level: 1): it => {
v(1.4em)
block[
#set text(font: display-stack, weight: 900, size: 22pt, fill: ink, tracking: -0.025em)
#it.body
]
v(0.2em)
line(length: 60pt, stroke: 3pt + ember)
v(0.6em)
}
show heading.where(level: 2): it => {
v(1.1em)
line(length: 100%, stroke: 0.5pt + rule)
v(0.5em)
block[
#set text(font: display-stack, weight: 800, size: 16pt, fill: ink, tracking: -0.02em)
#it.body
]
v(0.3em)
}
show heading.where(level: 3): it => {
v(0.8em)
block[
#set text(font: display-stack, weight: 700, size: 13pt, fill: ink, tracking: -0.015em)
#it.body
]
v(0.2em)
}
show heading.where(level: 4): it => block[
#set text(font: sans-stack, weight: 600, size: 11pt, fill: ink-2)
#it.body
]
// Links
show link: it => text(fill: navy, underline(it))
// Inline code
show raw.where(block: false): it => box(
fill: rgb("#F2EDE4"),
inset: (x: 3pt, y: 1pt),
outset: (y: 2pt),
radius: 1pt,
text(font: mono-stack, size: 0.92em, fill: ember-dark)[#it]
)
// Code blocks
show raw.where(block: true): it => block(
fill: ink,
width: 100%,
inset: 12pt,
radius: 4pt,
)[
#set text(font: mono-stack, size: 8.5pt, fill: paper-bg)
#it
]
// Block quote
show quote.where(block: true): it => block(
stroke: (left: 2pt + ember),
inset: (left: 14pt, top: 4pt, bottom: 4pt),
spacing: 1.2em,
)[
#set text(font: serif-stack, size: 13pt, style: "italic", fill: ink)
#it.body
]
// Figures
show figure: it => block(spacing: 1.4em)[
#it.body
#v(4pt)
#line(length: 100%, stroke: 0.4pt + rule)
#v(2pt)
#set text(font: sans-stack, size: 8.5pt, fill: ink-3)
#it.caption
]
// Tables
set table(
stroke: (x, y) => (
top: if y == 0 { 1.5pt + ink } else if y == 1 { 0.6pt + rule-strong } else { 0pt },
bottom: if y == 0 { 0pt } else { 0.4pt + rule },
),
inset: (x: 8pt, y: 6pt),
)
show table.cell.where(y: 0): set text(
font: sans-stack, size: 8pt, weight: 600, fill: ink-3, tracking: 0.06em
)
// ----- Title block -----
if title != none {
block[
#set text(font: sans-stack, size: 8pt, weight: 600, fill: ember, tracking: 0.1em)
#upper[— Civilytics Research]
]
v(8pt)
block[
#set text(font: display-stack, weight: 900, size: 32pt, fill: ink, tracking: -0.035em)
#set par(leading: 0.4em)
#title
]
if subtitle != none {
v(8pt)
block[
#set text(font: serif-stack, style: "italic", size: 14pt, fill: ink-2)
#set par(leading: 0.55em)
#subtitle
]
}
v(18pt)
line(length: 100%, stroke: 3pt + ink)
v(2pt)
line(length: 100%, stroke: 0.4pt + rule)
v(8pt)
// Authors + date row
grid(
columns: (1fr, 1fr),
align: (left, right),
[
#set text(font: sans-stack, size: 7.5pt, fill: ink-3, tracking: 0.08em)
#upper[Authors] \
#set text(font: sans-stack, size: 10pt, fill: ink, weight: 500, tracking: 0em)
#if authors.len() > 0 {
authors.map(a => if type(a) == str { a } else { a.name }).join(", ")
}
],
[
#set text(font: sans-stack, size: 7.5pt, fill: ink-3, tracking: 0.08em)
#upper[Published] \
#set text(font: sans-stack, size: 10pt, fill: ink, weight: 500, tracking: 0em)
#if date != none { date }
]
)
v(24pt)
if abstract != none {
block(
stroke: (left: 2pt + ember),
inset: (left: 14pt),
)[
#set text(font: serif-stack, style: "italic", size: 12pt, fill: ink)
#set par(leading: 0.55em)
#abstract
]
v(20pt)
}
if toc {
block[
#set text(font: sans-stack, size: 7.5pt, fill: ink-3, tracking: 0.08em)
#upper[Contents]
]
v(4pt)
line(length: 100%, stroke: 0.4pt + rule)
v(8pt)
outline(title: none, indent: auto, depth: 3)
v(20pt)
pagebreak()
}
}
doc
}
// Quarto entry point
#show: doc => civilytics(
title: $title$,
$if(subtitle)$subtitle: $subtitle$,$endif$
$if(by-author)$authors: ($for(by-author)$"$it.name.literal$",$endfor$),$endif$
$if(date)$date: $date$,$endif$
$if(abstract)$abstract: [$abstract$],$endif$
toc: $if(toc)$true$else$false$endif$,
doc
)
$body$