feat: add Quarto themes and templates (revealjs, HTML, PDF, Typst)
R-CMD-check / R CMD check (push) Successful in 3m52s
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:
+4
-2
@@ -8,7 +8,8 @@ Authors@R:
|
||||
Description: Provides a complete ggplot2 brand theme system for Civilytics
|
||||
Consulting LLC, including editorial light, dark, and slide-optimized themes;
|
||||
10 curated color palettes for qualitative, sequential, and diverging data;
|
||||
logo composition utilities; and data-wrangling helpers for public-sector
|
||||
logo composition utilities; Quarto themes for HTML reports, PDF, Typst, and
|
||||
Reveal.js presentations; and data-wrangling helpers for public-sector
|
||||
analysis.
|
||||
License: LGPL (>= 3)
|
||||
URL: https://gitea.civilytics.org/Civilytics/civilyticsR
|
||||
@@ -28,7 +29,8 @@ Imports:
|
||||
Encoding: UTF-8
|
||||
Suggests:
|
||||
testthat (>= 3.0.0),
|
||||
tidycensus
|
||||
tidycensus,
|
||||
quarto
|
||||
Config/testthat/edition: 3
|
||||
Config/roxygen2/version: 8.0.0
|
||||
RoxygenNote: 7.3.3
|
||||
|
||||
@@ -49,6 +49,9 @@ export(theme_civilytics_map)
|
||||
export(theme_civilytics_slide)
|
||||
export(theme_civilytics_slide_map)
|
||||
export(trim_max)
|
||||
export(use_civilytics_brand)
|
||||
export(use_civilytics_revealjs)
|
||||
export(use_civilytics_theme)
|
||||
export(waldInterval)
|
||||
export(z_gap_test)
|
||||
export(z_univariate)
|
||||
|
||||
@@ -19,6 +19,13 @@
|
||||
#' \item [scale_color_civilytics()] / [scale_fill_civilytics()] -- ggplot2 scales
|
||||
#' }
|
||||
#'
|
||||
#' @section Quarto templates:
|
||||
#' \itemize{
|
||||
#' \item [use_civilytics_revealjs()] -- install Reveal.js slide extension
|
||||
#' \item [use_civilytics_theme()] -- install HTML/PDF/Typst document theme
|
||||
#' \item [use_civilytics_brand()] -- install brand.yml only (Quarto 1.5+)
|
||||
#' }
|
||||
#'
|
||||
#' @keywords internal
|
||||
#' @importFrom stats qbeta
|
||||
#' @importFrom stats qnorm
|
||||
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
#' Install Civilytics Quarto Themes and Templates
|
||||
#'
|
||||
#' Helper functions that copy Civilytics Quarto assets from the installed
|
||||
#' package into your Quarto project directory. Logos are stored in a single
|
||||
#' canonical location (`inst/img/`) and copied to the paths expected by each
|
||||
#' template at install time.
|
||||
#'
|
||||
#' @name quarto-helpers
|
||||
NULL
|
||||
|
||||
|
||||
# -- internal utilities -------------------------------------------------------
|
||||
|
||||
#' Copy a package file to a project directory
|
||||
#'
|
||||
#' @param src Relative path within the installed package (under `inst/`).
|
||||
#' @param dst Destination path relative to `path`.
|
||||
#' @param path Project root.
|
||||
#' @param force Overwrite existing files?
|
||||
#' @return Invisible logical indicating success.
|
||||
#' @keywords internal
|
||||
.copy_pkg_file <- function(src, dst, path, force) {
|
||||
from <- system.file(src, package = "civilytics", mustWork = TRUE)
|
||||
to <- file.path(path, dst)
|
||||
dir.create(dirname(to), recursive = TRUE, showWarnings = FALSE)
|
||||
|
||||
if (file.exists(to) && !force) {
|
||||
message(" skip: ", dst, " (already exists; use force = TRUE to overwrite)")
|
||||
return(invisible(FALSE))
|
||||
}
|
||||
|
||||
file.copy(from, to, overwrite = force)
|
||||
message(" copy: ", dst)
|
||||
invisible(TRUE)
|
||||
}
|
||||
|
||||
#' Copy logo SVGs from inst/img/ to a target directory
|
||||
#'
|
||||
#' @param dest_dir Destination directory relative to `path`.
|
||||
#' @param path Project root.
|
||||
#' @param force Overwrite existing files?
|
||||
#' @param files Character vector of logo filenames to copy.
|
||||
#' @keywords internal
|
||||
.copy_logos <- function(dest_dir, path, force,
|
||||
files = c("civilytics-mark.svg",
|
||||
"civilytics-mark-reverse.svg",
|
||||
"civilytics-wordmark.svg",
|
||||
"civilytics-wordmark-reverse.svg",
|
||||
"civilytics-pulse.svg")) {
|
||||
for (f in files) {
|
||||
.copy_pkg_file(file.path("img", f), file.path(dest_dir, f), path, force)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# -- public API ----------------------------------------------------------------
|
||||
|
||||
#' Install the Civilytics Reveal.js extension
|
||||
#'
|
||||
#' Copies the Civilytics Reveal.js Quarto extension into
|
||||
#' `_extensions/civilytics-reveal/` under `path`, including brand logos
|
||||
#' from the package. After running this function, add
|
||||
#' `format: civilytics-reveal-revealjs` to your `.qmd` YAML front-matter.
|
||||
#'
|
||||
#' @param path Character. Project directory to install into. Default `"."`.
|
||||
#' @param force Logical. Overwrite existing files? Default `FALSE`.
|
||||
#'
|
||||
#' @return Invisible `NULL`.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' use_civilytics_revealjs()
|
||||
#' }
|
||||
use_civilytics_revealjs <- function(path = ".", force = FALSE) {
|
||||
path <- normalizePath(path, mustWork = TRUE)
|
||||
message("Installing Civilytics Reveal.js extension into: ", path)
|
||||
|
||||
ext_src <- "quarto/extensions/civilytics-reveal"
|
||||
ext_dst <- "_extensions/civilytics-reveal"
|
||||
|
||||
ext_files <- c(
|
||||
"_extension.yml",
|
||||
"civilytics.scss",
|
||||
"civilytics.css",
|
||||
"civilytics-head.html",
|
||||
"civilytics-after.html"
|
||||
)
|
||||
for (f in ext_files) {
|
||||
.copy_pkg_file(file.path(ext_src, f), file.path(ext_dst, f), path, force)
|
||||
}
|
||||
|
||||
# Logos — single source from inst/img/
|
||||
.copy_logos(file.path(ext_dst, "assets"), path, force)
|
||||
|
||||
message("\nDone! Add this to your .qmd front-matter:\n")
|
||||
message("---")
|
||||
message("format: civilytics-reveal-revealjs")
|
||||
message("---")
|
||||
invisible(NULL)
|
||||
}
|
||||
|
||||
|
||||
#' Install the Civilytics document theme
|
||||
#'
|
||||
#' Copies Civilytics HTML, PDF (LaTeX), and Typst theme files into
|
||||
#' your Quarto project. Also installs `_brand.yml` and the logo
|
||||
#' assets it references.
|
||||
#'
|
||||
#' @inheritParams use_civilytics_revealjs
|
||||
#'
|
||||
#' @return Invisible `NULL`.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' use_civilytics_theme()
|
||||
#' }
|
||||
use_civilytics_theme <- function(path = ".", force = FALSE) {
|
||||
path <- normalizePath(path, mustWork = TRUE)
|
||||
message("Installing Civilytics document theme into: ", path)
|
||||
|
||||
# Brand file
|
||||
.copy_pkg_file("quarto/_brand.yml", "_brand.yml", path, force)
|
||||
|
||||
# HTML theme
|
||||
theme_files <- c("civilytics.scss", "_tokens.scss", "extras.css")
|
||||
for (f in theme_files) {
|
||||
.copy_pkg_file(file.path("quarto/theme", f), file.path("theme", f), path, force)
|
||||
}
|
||||
|
||||
# LaTeX
|
||||
latex_files <- c("civilytics.tex", "civilytics-title.tex")
|
||||
for (f in latex_files) {
|
||||
.copy_pkg_file(file.path("quarto/latex", f), file.path("latex", f), path, force)
|
||||
}
|
||||
|
||||
# Typst
|
||||
.copy_pkg_file(
|
||||
"quarto/typst/civilytics-typst.typ",
|
||||
"typst/civilytics-typst.typ",
|
||||
path, force
|
||||
)
|
||||
|
||||
# Logos — for _brand.yml (expects assets/logo/)
|
||||
.copy_logos("assets/logo", path, force)
|
||||
|
||||
# Example report
|
||||
.copy_pkg_file("quarto/examples/report.qmd", "examples/report.qmd", path, force)
|
||||
|
||||
message("\nDone! Example YAML for a report:\n")
|
||||
message("---")
|
||||
message("format:")
|
||||
message(" html:")
|
||||
message(" theme: theme/civilytics.scss")
|
||||
message(" css: theme/extras.css")
|
||||
message(" pdf:")
|
||||
message(" include-in-header: latex/civilytics.tex")
|
||||
message(" include-before-body: latex/civilytics-title.tex")
|
||||
message(" typst:")
|
||||
message(" template: typst/civilytics-typst.typ")
|
||||
message("---")
|
||||
message("\nSee examples/report.qmd for a complete example.")
|
||||
invisible(NULL)
|
||||
}
|
||||
|
||||
|
||||
#' Install the Civilytics brand file only
|
||||
#'
|
||||
#' Copies `_brand.yml` and the logo assets it references into your
|
||||
#' Quarto project. This gives you Quarto 1.5+ automatic brand
|
||||
#' styling (colors, fonts, logos) without the full theme SCSS.
|
||||
#'
|
||||
#' @inheritParams use_civilytics_revealjs
|
||||
#'
|
||||
#' @return Invisible `NULL`.
|
||||
#' @export
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' use_civilytics_brand()
|
||||
#' }
|
||||
use_civilytics_brand <- function(path = ".", force = FALSE) {
|
||||
path <- normalizePath(path, mustWork = TRUE)
|
||||
message("Installing Civilytics brand file into: ", path)
|
||||
|
||||
.copy_pkg_file("quarto/_brand.yml", "_brand.yml", path, force)
|
||||
.copy_logos("assets/logo", path, force)
|
||||
|
||||
message("\nDone! Quarto 1.5+ will auto-apply brand colors, fonts, and logos.")
|
||||
message("See: https://quarto.org/docs/authoring/brand.html")
|
||||
invisible(NULL)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
@@ -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}}
|
||||
@@ -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;
|
||||
@@ -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; }
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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$
|
||||
@@ -30,6 +30,15 @@ public-sector analysis.
|
||||
}
|
||||
}
|
||||
|
||||
\section{Quarto templates}{
|
||||
|
||||
\itemize{
|
||||
\item [use_civilytics_revealjs()] -- install Reveal.js slide extension
|
||||
\item [use_civilytics_theme()] -- install HTML/PDF/Typst document theme
|
||||
\item [use_civilytics_brand()] -- install brand.yml only (Quarto 1.5+)
|
||||
}
|
||||
}
|
||||
|
||||
\seealso{
|
||||
Useful links:
|
||||
\itemize{
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/quarto.R
|
||||
\name{.copy_logos}
|
||||
\alias{.copy_logos}
|
||||
\title{Copy logo SVGs from inst/img/ to a target directory}
|
||||
\usage{
|
||||
.copy_logos(
|
||||
dest_dir,
|
||||
path,
|
||||
force,
|
||||
files = c("civilytics-mark.svg", "civilytics-mark-reverse.svg",
|
||||
"civilytics-wordmark.svg", "civilytics-wordmark-reverse.svg", "civilytics-pulse.svg")
|
||||
)
|
||||
}
|
||||
\arguments{
|
||||
\item{dest_dir}{Destination directory relative to `path`.}
|
||||
|
||||
\item{path}{Project root.}
|
||||
|
||||
\item{force}{Overwrite existing files?}
|
||||
|
||||
\item{files}{Character vector of logo filenames to copy.}
|
||||
}
|
||||
\description{
|
||||
Copy logo SVGs from inst/img/ to a target directory
|
||||
}
|
||||
\keyword{internal}
|
||||
@@ -0,0 +1,24 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/quarto.R
|
||||
\name{.copy_pkg_file}
|
||||
\alias{.copy_pkg_file}
|
||||
\title{Copy a package file to a project directory}
|
||||
\usage{
|
||||
.copy_pkg_file(src, dst, path, force)
|
||||
}
|
||||
\arguments{
|
||||
\item{src}{Relative path within the installed package (under `inst/`).}
|
||||
|
||||
\item{dst}{Destination path relative to `path`.}
|
||||
|
||||
\item{path}{Project root.}
|
||||
|
||||
\item{force}{Overwrite existing files?}
|
||||
}
|
||||
\value{
|
||||
Invisible logical indicating success.
|
||||
}
|
||||
\description{
|
||||
Copy a package file to a project directory
|
||||
}
|
||||
\keyword{internal}
|
||||
@@ -0,0 +1,11 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/quarto.R
|
||||
\name{quarto-helpers}
|
||||
\alias{quarto-helpers}
|
||||
\title{Install Civilytics Quarto Themes and Templates}
|
||||
\description{
|
||||
Helper functions that copy Civilytics Quarto assets from the installed
|
||||
package into your Quarto project directory. Logos are stored in a single
|
||||
canonical location (`inst/img/`) and copied to the paths expected by each
|
||||
template at install time.
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/quarto.R
|
||||
\name{use_civilytics_brand}
|
||||
\alias{use_civilytics_brand}
|
||||
\title{Install the Civilytics brand file only}
|
||||
\usage{
|
||||
use_civilytics_brand(path = ".", force = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{path}{Character. Project directory to install into. Default `"."`.}
|
||||
|
||||
\item{force}{Logical. Overwrite existing files? Default `FALSE`.}
|
||||
}
|
||||
\value{
|
||||
Invisible `NULL`.
|
||||
}
|
||||
\description{
|
||||
Copies `_brand.yml` and the logo assets it references into your
|
||||
Quarto project. This gives you Quarto 1.5+ automatic brand
|
||||
styling (colors, fonts, logos) without the full theme SCSS.
|
||||
}
|
||||
\examples{
|
||||
\dontrun{
|
||||
use_civilytics_brand()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/quarto.R
|
||||
\name{use_civilytics_revealjs}
|
||||
\alias{use_civilytics_revealjs}
|
||||
\title{Install the Civilytics Reveal.js extension}
|
||||
\usage{
|
||||
use_civilytics_revealjs(path = ".", force = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{path}{Character. Project directory to install into. Default `"."`.}
|
||||
|
||||
\item{force}{Logical. Overwrite existing files? Default `FALSE`.}
|
||||
}
|
||||
\value{
|
||||
Invisible `NULL`.
|
||||
}
|
||||
\description{
|
||||
Copies the Civilytics Reveal.js Quarto extension into
|
||||
`_extensions/civilytics-reveal/` under `path`, including brand logos
|
||||
from the package. After running this function, add
|
||||
`format: civilytics-reveal-revealjs` to your `.qmd` YAML front-matter.
|
||||
}
|
||||
\examples{
|
||||
\dontrun{
|
||||
use_civilytics_revealjs()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/quarto.R
|
||||
\name{use_civilytics_theme}
|
||||
\alias{use_civilytics_theme}
|
||||
\title{Install the Civilytics document theme}
|
||||
\usage{
|
||||
use_civilytics_theme(path = ".", force = FALSE)
|
||||
}
|
||||
\arguments{
|
||||
\item{path}{Character. Project directory to install into. Default `"."`.}
|
||||
|
||||
\item{force}{Logical. Overwrite existing files? Default `FALSE`.}
|
||||
}
|
||||
\value{
|
||||
Invisible `NULL`.
|
||||
}
|
||||
\description{
|
||||
Copies Civilytics HTML, PDF (LaTeX), and Typst theme files into
|
||||
your Quarto project. Also installs `_brand.yml` and the logo
|
||||
assets it references.
|
||||
}
|
||||
\examples{
|
||||
\dontrun{
|
||||
use_civilytics_theme()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user