From 136c53643c846d5d30bacca190837755c1dbb4c7 Mon Sep 17 00:00:00 2001 From: Jared Knowles Date: Wed, 20 May 2026 09:39:27 -0600 Subject: [PATCH] feat: add Quarto themes and templates (revealjs, HTML, PDF, Typst) 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. --- DESCRIPTION | 6 +- NAMESPACE | 3 + R/civilytics-package.R | 7 + R/quarto.R | 193 +++ inst/quarto/_brand.yml | 66 ++ inst/quarto/examples/report.qmd | 80 ++ inst/quarto/examples/slides.qmd | 65 + .../civilytics-reveal/_extension.yml | 56 + .../civilytics-reveal/civilytics-after.html | 80 ++ .../civilytics-reveal/civilytics-head.html | 3 + .../civilytics-reveal/civilytics.css | 1014 ++++++++++++++++ .../civilytics-reveal/civilytics.scss | 1056 +++++++++++++++++ inst/quarto/latex/civilytics-title.tex | 41 + inst/quarto/latex/civilytics.tex | 112 ++ inst/quarto/theme/_tokens.scss | 73 ++ inst/quarto/theme/civilytics.scss | 331 ++++++ inst/quarto/theme/extras.css | 63 + inst/quarto/typst/civilytics-typst.typ | 244 ++++ man/civilytics-package.Rd | 9 + man/dot-copy_logos.Rd | 27 + man/dot-copy_pkg_file.Rd | 24 + man/quarto-helpers.Rd | 11 + man/use_civilytics_brand.Rd | 26 + man/use_civilytics_revealjs.Rd | 27 + man/use_civilytics_theme.Rd | 26 + 25 files changed, 3641 insertions(+), 2 deletions(-) create mode 100644 R/quarto.R create mode 100644 inst/quarto/_brand.yml create mode 100644 inst/quarto/examples/report.qmd create mode 100644 inst/quarto/examples/slides.qmd create mode 100644 inst/quarto/extensions/civilytics-reveal/_extension.yml create mode 100644 inst/quarto/extensions/civilytics-reveal/civilytics-after.html create mode 100644 inst/quarto/extensions/civilytics-reveal/civilytics-head.html create mode 100644 inst/quarto/extensions/civilytics-reveal/civilytics.css create mode 100644 inst/quarto/extensions/civilytics-reveal/civilytics.scss create mode 100644 inst/quarto/latex/civilytics-title.tex create mode 100644 inst/quarto/latex/civilytics.tex create mode 100644 inst/quarto/theme/_tokens.scss create mode 100644 inst/quarto/theme/civilytics.scss create mode 100644 inst/quarto/theme/extras.css create mode 100644 inst/quarto/typst/civilytics-typst.typ create mode 100644 man/dot-copy_logos.Rd create mode 100644 man/dot-copy_pkg_file.Rd create mode 100644 man/quarto-helpers.Rd create mode 100644 man/use_civilytics_brand.Rd create mode 100644 man/use_civilytics_revealjs.Rd create mode 100644 man/use_civilytics_theme.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 1d167fd..235325a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -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 diff --git a/NAMESPACE b/NAMESPACE index 79ca718..e4ef366 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -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) diff --git a/R/civilytics-package.R b/R/civilytics-package.R index 866eae7..67e4fcc 100644 --- a/R/civilytics-package.R +++ b/R/civilytics-package.R @@ -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 diff --git a/R/quarto.R b/R/quarto.R new file mode 100644 index 0000000..ed3cc06 --- /dev/null +++ b/R/quarto.R @@ -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) +} diff --git a/inst/quarto/_brand.yml b/inst/quarto/_brand.yml new file mode 100644 index 0000000..73ad02a --- /dev/null +++ b/inst/quarto/_brand.yml @@ -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 diff --git a/inst/quarto/examples/report.qmd b/inst/quarto/examples/report.qmd new file mode 100644 index 0000000..76aecf0 --- /dev/null +++ b/inst/quarto/examples/report.qmd @@ -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. diff --git a/inst/quarto/examples/slides.qmd b/inst/quarto/examples/slides.qmd new file mode 100644 index 0000000..c70a6c4 --- /dev/null +++ b/inst/quarto/examples/slides.qmd @@ -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 diff --git a/inst/quarto/extensions/civilytics-reveal/_extension.yml b/inst/quarto/extensions/civilytics-reveal/_extension.yml new file mode 100644 index 0000000..52005e7 --- /dev/null +++ b/inst/quarto/extensions/civilytics-reveal/_extension.yml @@ -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 diff --git a/inst/quarto/extensions/civilytics-reveal/civilytics-after.html b/inst/quarto/extensions/civilytics-reveal/civilytics-after.html new file mode 100644 index 0000000..ad995c4 --- /dev/null +++ b/inst/quarto/extensions/civilytics-reveal/civilytics-after.html @@ -0,0 +1,80 @@ + + diff --git a/inst/quarto/extensions/civilytics-reveal/civilytics-head.html b/inst/quarto/extensions/civilytics-reveal/civilytics-head.html new file mode 100644 index 0000000..acf404a --- /dev/null +++ b/inst/quarto/extensions/civilytics-reveal/civilytics-head.html @@ -0,0 +1,3 @@ + + + diff --git a/inst/quarto/extensions/civilytics-reveal/civilytics.css b/inst/quarto/extensions/civilytics-reveal/civilytics.css new file mode 100644 index 0000000..0fadd6b --- /dev/null +++ b/inst/quarto/extensions/civilytics-reveal/civilytics.css @@ -0,0 +1,1014 @@ +/* Civilytics Reveal.js theme — plain CSS build. */ + +/* ========================================================== + Civilytics Reveal.js theme — rules + Editorial, serif-led slides. Designed for Quarto + Pandoc. + Tokens mirror /colors_and_type.css from the Civilytics DS. + ========================================================== */ + +@import url('https://fonts.googleapis.com/css2?family=Libre+Franklin:ital,wght@0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&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'); + +:root { + /* Civilytics tokens — keep names in sync with the design system */ + --paper: #FAF7F2; + --paper-2: #F2EDE4; + --paper-3: #E6DFD1; + --rule: #D6CEBD; + --rule-strong: #B8AE97; + + --ink: #0E1A2B; + --ink-2: #2B3A52; + --ink-3: #5A6A82; + --ink-4: #8C97AB; + + --navy-900: #0E1A2B; + --navy-700: #1A2E4A; + --navy-600: #22406A; + --navy-500: #2E5590; + --navy-100: #DDE6F2; + + --accent-900: #451A00; + --accent-700: #923D00; + --accent-600: #C25311; + --accent-500: #DB6C25; + --accent-300: #F2A976; + --accent-200: #F8C8A3; + --accent-100: #FBE0C6; + + --teal-600: #1F6F70; + --plum-600: #6B3A5E; + --moss-600: #4A6B2F; + --brass-600: #B8751C; + + --font-display: "Libre Franklin", "Franklin Gothic", "Inter", system-ui, sans-serif; + --font-serif: "Source Serif 4", "Source Serif Pro", Georgia, serif; + --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + --font-mono: "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace; + + --tracking-caps: 0.08em; +} + +/* ----------------------------------------------------------- + Reveal root & slide chrome + ----------------------------------------------------------- */ +.reveal-viewport { + background: var(--paper); +} + +.reveal { + font-family: var(--font-sans); + font-size: 32px; + font-weight: 400; + color: var(--ink); + letter-spacing: 0; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +.reveal ::selection { background: var(--accent-200); color: var(--ink); } +.reveal ::-moz-selection { background: var(--accent-200); color: var(--ink); } + +.reveal .slides { + text-align: left; +} + +.reveal .slides > section, +.reveal .slides > section > section { + padding: 48px 72px; + line-height: 1.45; + font-weight: inherit; + box-sizing: border-box; + height: 100%; +} + +/* ----------------------------------------------------------- + Headings + ----------------------------------------------------------- */ +.reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { + font-family: var(--font-display); + color: var(--ink); + font-weight: 800; + line-height: 1.08; + letter-spacing: -0.025em; + text-transform: none; + text-wrap: balance; + margin: 0 0 0.5em; +} + +.reveal h1 { font-size: 2.4em; font-weight: 900; letter-spacing: -0.035em; line-height: 1.0; } +.reveal h2 { font-size: 1.7em; font-weight: 800; letter-spacing: -0.03em; } +.reveal h3 { font-size: 1.3em; font-weight: 700; letter-spacing: -0.022em; } +.reveal h4 { font-size: 1.1em; font-weight: 700; letter-spacing: -0.018em; } + +/* Eyebrow kicker — used above headlines on many layouts */ +.reveal .eyebrow, +.reveal [data-eyebrow]::before { + display: inline-flex; + align-items: center; + gap: 10px; + font-family: var(--font-sans); + font-size: 0.5em; + font-weight: 600; + color: var(--accent-600); + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + margin: 0 0 18px; +} +.reveal .eyebrow::before { + content: ""; + display: inline-block; + width: 28px; + height: 2px; + background: var(--accent-600); +} +.reveal [data-eyebrow]::before { + content: attr(data-eyebrow); +} + +/* ----------------------------------------------------------- + Body copy, lists, links + ----------------------------------------------------------- */ +.reveal p { + font-family: var(--font-sans); + line-height: 1.55; + margin: 0 0 0.75em; + text-wrap: pretty; +} + +.reveal .lead, +.reveal p.lead { + font-family: var(--font-serif); + font-size: 1.15em; + font-weight: 400; + line-height: 1.5; + color: var(--ink); + max-width: 26em; + padding-left: 0.75em; + border-left: 2px solid var(--accent-600); + margin: 0.5em 0 1em; +} + +.reveal ul, .reveal ol { + display: block; + margin: 0.25em 0 0.75em; + padding-left: 1.3em; +} +.reveal li { + margin: 0.35em 0; + line-height: 1.5; +} +.reveal ul { list-style: none; } +.reveal ul > li { position: relative; } +.reveal ul > li::before { + content: ""; + position: absolute; + left: -1.1em; + top: 0.7em; + width: 10px; + height: 2px; + background: var(--accent-600); +} +.reveal ol { list-style: decimal; } +.reveal ol > li::marker { + color: var(--accent-600); + font-family: var(--font-mono); + font-weight: 600; +} + +.reveal a { + color: var(--navy-600); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 3px; + transition: color 120ms ease-out, text-decoration-thickness 120ms ease-out; +} +.reveal a:hover { + color: var(--navy-700); + text-decoration-thickness: 2px; +} + +.reveal hr { + border: none; + border-top: 1px solid var(--rule); + margin: 0.75em 0; +} + +.reveal strong, .reveal b { font-weight: 700; color: var(--ink); } +.reveal em, .reveal i { font-style: italic; } + +/* Small caps helper — for metadata rows */ +.reveal .smallcaps, +.reveal .meta { + font-family: var(--font-sans); + font-size: 0.55em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--ink-3); +} + +/* ----------------------------------------------------------- + Block quote / pullquote + ----------------------------------------------------------- */ +.reveal blockquote { + font-family: var(--font-serif); + font-size: 1.1em; + font-style: italic; + line-height: 1.4; + color: var(--ink); + border-left: 3px solid var(--accent-600); + padding: 0.25em 0 0.25em 1em; + margin: 0.75em 0; + background: transparent; + box-shadow: none; + width: auto; +} + +/* Big pullquote layout — .pullquote on a section */ +.reveal section.pullquote { + display: flex; + flex-direction: column; + justify-content: center; +} +.reveal section.pullquote blockquote { + font-size: 1.8em; + font-style: italic; + line-height: 1.2; + border-left: none; + padding: 0; + max-width: 22em; + letter-spacing: -0.01em; +} +.reveal section.pullquote blockquote::before { + content: "\201C"; + display: block; + font-family: var(--font-serif); + font-size: 2.2em; + line-height: 0.9; + color: var(--accent-600); + margin-bottom: 0.1em; + font-style: normal; +} +.reveal section.pullquote .attribution, +.reveal section.pullquote footer { + margin-top: 1em; + font-family: var(--font-sans); + font-size: 0.55em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--ink-3); + font-style: normal; +} +.reveal section.pullquote .attribution::before { + content: "— "; + color: var(--accent-600); +} + +/* ----------------------------------------------------------- + Code + ----------------------------------------------------------- */ +.reveal code { + font-family: var(--font-mono); + font-size: 0.85em; + color: var(--accent-700); + background: var(--paper-2); + padding: 0.08em 0.35em; + border-radius: 2px; + font-feature-settings: "tnum" 1, "zero" 1; +} + +.reveal pre { + font-family: var(--font-mono); + font-size: 0.55em; + line-height: 1.5; + background: var(--ink); + color: var(--paper); + padding: 1em 1.25em; + border-radius: 8px; + box-shadow: none; + width: 100%; + max-width: 100%; + overflow: auto; + margin: 0.5em 0; +} +.reveal pre code { + background: transparent; + color: inherit; + padding: 0; + font-size: 1em; + display: block; + overflow: visible; + white-space: pre; + max-height: none; +} + +/* Quarto code-line-number + highlight markers */ +.reveal pre code .hljs-ln-numbers, +.reveal pre code .code-line-number { + color: var(--ink-4); + padding-right: 1em; + user-select: none; +} +.reveal div.sourceCode { + background: transparent; + border: none; + margin: 0.5em 0; +} +.reveal code span.co { color: #8C97AB; font-style: italic; } /* comments */ +.reveal code span.st { color: #F2A976; } /* strings */ +.reveal code span.dv, +.reveal code span.fl { color: #F8C8A3; } /* numbers */ +.reveal code span.kw { color: #EA8A49; font-weight: 600; } /* keywords */ +.reveal code span.cf { color: #EA8A49; font-weight: 600; } /* control flow */ +.reveal code span.fu { color: #B3C6E0; } /* function */ +.reveal code span.op { color: #D6CEBD; } /* operator */ +.reveal code span.cn, +.reveal code span.va { color: #7CB3B3; } /* variables */ +.reveal code span.at { color: #7CB3B3; } + +/* Quarto line-highlighting */ +.reveal pre code.sourceCode > span.has-line-highlight { + background: rgba(248, 200, 163, 0.14); +} + +/* ----------------------------------------------------------- + Tables + ----------------------------------------------------------- */ +.reveal table { + font-family: var(--font-sans); + font-size: 0.7em; + border-collapse: collapse; + margin: 0.5em 0; + width: 100%; + font-variant-numeric: tabular-nums; +} +.reveal table th, +.reveal table td { + padding: 0.55em 0.9em; + text-align: left; + border: none; + border-bottom: 1px solid var(--rule); + vertical-align: baseline; +} +.reveal table th { + font-family: var(--font-sans); + font-size: 0.82em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--ink-3); + border-bottom: 2px solid var(--ink); + padding-bottom: 0.4em; + background: transparent; +} +.reveal table tbody tr:nth-child(even) td { + background: var(--paper-2); +} +.reveal table tbody tr:last-child td { + border-bottom: 2px solid var(--ink); +} +/* Numeric columns — add .numeric to a / */ +.reveal table td.numeric, +.reveal table th.numeric, +.reveal table td[align="right"], +.reveal table th[align="right"] { + text-align: right; + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; +} +.reveal table caption { + caption-side: top; + text-align: left; + font-family: var(--font-sans); + font-size: 0.55em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--accent-600); + margin-bottom: 0.5em; +} + +/* ----------------------------------------------------------- + Images & figures + ----------------------------------------------------------- */ +.reveal img, +.reveal section img { + background: transparent; + border: none; + box-shadow: none; + margin: 0.25em 0; + max-width: 100%; + height: auto; +} +.reveal figure { + margin: 0.5em 0; +} +.reveal figure img { + display: block; + border: 1px solid var(--rule); +} +.reveal figure figcaption, +.reveal .caption { + font-family: var(--font-sans); + font-size: 0.55em; + line-height: 1.4; + color: var(--ink-3); + margin-top: 0.5em; + max-width: 42em; +} +.reveal figure figcaption strong, +.reveal .caption strong { + color: var(--accent-600); + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + font-size: 0.9em; + display: inline-block; + margin-right: 0.5em; +} + +/* ----------------------------------------------------------- + TITLE SLIDE + Quarto renders the title slide with #title-slide +

+ ----------------------------------------------------------- */ +.reveal #title-slide, +.reveal section.title-slide { + padding: 72px 80px; + display: flex !important; + flex-direction: column; + justify-content: space-between; + text-align: left; + background: + linear-gradient(180deg, var(--paper) 0%, var(--paper) 58%, var(--paper-2) 100%); +} + +.reveal #title-slide .civilytics-mark, +.reveal section.title-slide .civilytics-mark { + display: block; + width: 72px; + height: 72px; + margin-bottom: 32px; + object-fit: contain; +} + +/* Civilytics wordmark lockup — bold serif "Civilytics" + inline pulse accent. + Rendered as an tag pointing at the official wordmark SVG asset + (assets/civilytics-wordmark.svg for ink-on-paper slides, + assets/civilytics-wordmark-reverse.svg for paper-on-ink slides). + Source viewBox is 558×160 (aspect ratio ≈ 3.4875). */ +img.civilytics-wordmark, +.reveal img.civilytics-wordmark, +.reveal .civilytics-wordmark { + display: block; + height: 48px; + width: 167.4px; /* 558/160 aspect * 48px height */ + flex: 0 0 auto; + align-self: flex-start; + object-fit: contain; + margin: 0 0 36px; +} + +.reveal #title-slide .civilytics-wordmark, +.reveal section.title-slide .civilytics-wordmark { + height: 48px; + width: 167.4px; + margin: 0 0 36px; +} + +.reveal #title-slide .title, +.reveal section.title-slide h1.title, +.reveal section.title-slide h1:first-of-type { + font-family: var(--font-display); + font-size: 2.6em; + font-weight: 900; + letter-spacing: -0.035em; + line-height: 0.98; + color: var(--ink); + margin: 0 0 0.3em; + max-width: 18em; + text-wrap: balance; +} +.reveal #title-slide .subtitle, +.reveal section.title-slide p.subtitle, +.reveal section.title-slide .subtitle { + font-family: var(--font-serif); + font-size: 1.05em; + font-weight: 400; + font-style: italic; + line-height: 1.35; + color: var(--ink-2); + margin: 0 0 1em; + max-width: 28em; + padding-left: 0.75em; + border-left: 2px solid var(--accent-600); +} + +/* Author / date / affiliation block */ +.reveal #title-slide .quarto-title-authors, +.reveal #title-slide .quarto-title-author-name, +.reveal section.title-slide .author, +.reveal section.title-slide .date, +.reveal section.title-slide .affiliation { + font-family: var(--font-sans); + color: var(--ink-2); +} +.reveal #title-slide .author, +.reveal #title-slide .quarto-title-author-name, +.reveal section.title-slide .author { + font-size: 0.7em; + font-weight: 600; + color: var(--ink); + margin: 0.2em 0 0; + letter-spacing: 0; +} +.reveal #title-slide .affiliation, +.reveal #title-slide .quarto-title-affiliation, +.reveal section.title-slide .affiliation { + font-size: 0.55em; + font-weight: 500; + color: var(--ink-3); + letter-spacing: var(--tracking-caps); + text-transform: uppercase; + margin: 0.15em 0 0; +} +.reveal #title-slide .date, +.reveal section.title-slide .date { + font-size: 0.5em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--accent-600); + margin-top: 1em; + padding-top: 0.8em; + border-top: 3px double var(--ink); + display: inline-block; +} + +/* Editorial thick-top / thin-bottom rule block around title */ +.reveal section.title-slide .quarto-title-block, +.reveal section.title-slide .title-block { + border-top: 6px solid var(--ink); + border-bottom: 1px solid var(--rule); + padding: 0.5em 0 1em; +} + +/* ----------------------------------------------------------- + SECTION DIVIDER —
or + Quarto's {.section} header + ----------------------------------------------------------- */ +.reveal section.section-divider, +.reveal section.section { + background: var(--ink); + color: var(--paper); + display: flex !important; + flex-direction: column; + justify-content: center; + padding: 80px 96px; +} +.reveal section.section-divider::before, +.reveal section.section::before { + content: ""; + display: block; + width: 72px; + height: 2px; + background: var(--accent-600); + margin-bottom: 24px; +} +.reveal section.section-divider h1, +.reveal section.section-divider h2, +.reveal section.section-divider h3, +.reveal section.section h1, +.reveal section.section h2, +.reveal section.section h3 { + color: var(--paper); + font-size: 2.8em; + font-weight: 900; + letter-spacing: -0.035em; + line-height: 1.0; + max-width: 18em; +} +.reveal section.section-divider .eyebrow, +.reveal section.section .eyebrow { + color: var(--accent-300); +} +.reveal section.section-divider .eyebrow::before, +.reveal section.section .eyebrow::before { + background: var(--accent-300); +} +.reveal section.section-divider p, +.reveal section.section p { + color: var(--paper); + opacity: 0.82; + font-family: var(--font-serif); + font-size: 1.05em; + font-style: italic; + max-width: 28em; +} +.reveal section.section-divider .section-number, +.reveal section.section .section-number { + font-family: var(--font-mono); + font-size: 0.6em; + font-weight: 600; + color: var(--accent-300); + letter-spacing: var(--tracking-caps); + text-transform: uppercase; + margin-bottom: 0.5em; +} + +/* ----------------------------------------------------------- + FULL-BLEED IMAGE —
+ ----------------------------------------------------------- */ +.reveal section.full-bleed { + padding: 0 !important; +} +.reveal section.full-bleed .caption, +.reveal section.full-bleed figcaption { + position: absolute; + left: 48px; + right: 48px; + bottom: 48px; + background: var(--paper); + padding: 14px 18px; + border-left: 3px solid var(--accent-600); + font-family: var(--font-sans); + font-size: 0.5em; + line-height: 1.45; + color: var(--ink); + max-width: 36em; + box-shadow: 0 4px 12px rgba(14, 26, 43, 0.12); +} +.reveal section.full-bleed .caption strong, +.reveal section.full-bleed figcaption strong { + display: block; + font-size: 0.85em; + font-weight: 700; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--accent-600); + margin-bottom: 4px; +} + +/* ----------------------------------------------------------- + BIG STAT / DATA CALLOUT — .big-stat section or .stat block + ----------------------------------------------------------- */ +.reveal .stat-number { + font-family: var(--font-serif); + font-weight: 700; + font-size: 3.6em; + line-height: 1; + letter-spacing: -0.02em; + font-variant-numeric: tabular-nums; + color: var(--ink); + margin: 0 0 0.1em; + display: block; +} +.reveal .stat-label { + font-family: var(--font-sans); + font-size: 0.55em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--ink-3); +} +.reveal .stat-note { + font-family: var(--font-serif); + font-style: italic; + font-size: 0.9em; + color: var(--ink-2); + max-width: 22em; + margin-top: 0.6em; +} +.reveal section.big-stat { + display: flex !important; + flex-direction: column; + justify-content: center; +} +.reveal section.big-stat .stat-number { + font-size: 5em; +} + +/* Stat grid — multiple stats side-by-side */ +.reveal .stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 2em 3em; + margin: 1em 0; +} +.reveal .stat-grid .stat { + border-top: 3px solid var(--ink); + padding-top: 0.6em; +} +.reveal .stat-grid .stat .stat-number { font-size: 2.8em; } + +/* ----------------------------------------------------------- + TWO-COLUMN — Quarto renders {.columns}{.column width=50%} + ----------------------------------------------------------- */ +.reveal .columns { + display: flex; + gap: 3em; + align-items: flex-start; + margin: 0.25em 0; +} +.reveal .column { + flex: 1; + min-width: 0; +} +.reveal .column > :first-child { margin-top: 0; } + +/* Hand-rolled two-col for pandoc (non-Quarto) users */ +.reveal .two-col { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 3em; + align-items: start; +} +.reveal .two-col--wide-left { grid-template-columns: 2fr 1fr; } +.reveal .two-col--wide-right { grid-template-columns: 1fr 2fr; } + +/* ----------------------------------------------------------- + AGENDA / TOC slide —
+ Pair with an ordered list; we restyle it. + ----------------------------------------------------------- */ +.reveal section.agenda ol { + list-style: none; + counter-reset: agenda; + padding: 0; + margin: 0.5em 0; +} +.reveal section.agenda ol > li { + counter-increment: agenda; + display: grid; + grid-template-columns: 2.4em 1fr auto; + gap: 0.6em 1.2em; + align-items: baseline; + padding: 0.55em 0; + border-top: 1px solid var(--rule); + font-family: var(--font-serif); + font-size: 1.05em; +} +.reveal section.agenda ol > li:last-child { border-bottom: 1px solid var(--rule); } +.reveal section.agenda ol > li::before { + content: counter(agenda, decimal-leading-zero); + font-family: var(--font-mono); + font-weight: 600; + font-size: 0.7em; + color: var(--accent-600); + letter-spacing: var(--tracking-caps); +} +.reveal section.agenda ol > li .meta, +.reveal section.agenda ol > li em { + font-family: var(--font-sans); + font-style: normal; + font-size: 0.7em; + color: var(--ink-3); + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + justify-self: end; +} + +/* ----------------------------------------------------------- + THANK-YOU / END slide —
+ ----------------------------------------------------------- */ +.reveal section.thank-you { + background: var(--ink); + color: var(--paper); + display: flex !important; + flex-direction: column; + justify-content: center; + padding: 80px 96px; +} +.reveal section.thank-you .civilytics-mark { + display: block; + width: 56px; + height: 56px; + margin-bottom: 28px; + object-fit: contain; +} +.reveal section.thank-you .civilytics-wordmark { + height: 40px; + width: 139.5px; /* 558/160 aspect * 40 */ + flex: 0 0 auto; + align-self: flex-start; + margin: 0 0 32px; +} +.reveal section.thank-you h1, +.reveal section.thank-you h2 { + color: var(--paper); + font-size: 3em; + font-weight: 900; + letter-spacing: -0.035em; + margin-bottom: 0.2em; +} +.reveal section.thank-you p { + color: var(--paper); + opacity: 0.85; + font-family: var(--font-serif); + font-size: 1.1em; + font-style: italic; + max-width: 26em; +} +.reveal section.thank-you .contact, +.reveal section.thank-you ul.contact { + list-style: none; + padding: 0; + margin: 1.5em 0 0; + font-family: var(--font-sans); + font-size: 0.6em; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--accent-300); +} +.reveal section.thank-you .contact li, +.reveal section.thank-you ul.contact li { + margin: 0.3em 0; +} +.reveal section.thank-you .contact li::before, +.reveal section.thank-you ul.contact li::before { display: none; } +.reveal section.thank-you a { color: var(--paper); text-decoration: underline; text-decoration-color: var(--accent-600); } +.reveal section.thank-you a:hover { color: var(--accent-300); } + +/* ----------------------------------------------------------- + REFERENCES / SOURCES — small, tight + ----------------------------------------------------------- */ +.reveal .references, +.reveal section.references { + font-size: 0.72em; +} +.reveal section.references h2 { font-size: 1.4em; } +.reveal .references ol, +.reveal .references ul { + padding-left: 1.4em; +} +.reveal .references li { + font-family: var(--font-sans); + font-size: 0.95em; + line-height: 1.4; + color: var(--ink-2); + margin: 0.35em 0; +} +.reveal .references li::before { display: none; } + +/* Footnotes (Pandoc / Quarto) */ +.reveal .footnotes, +.reveal div.footnotes { + font-size: 0.6em; + color: var(--ink-3); + border-top: 1px solid var(--rule); + padding-top: 0.6em; + margin-top: 1em; +} +.reveal a.footnote-ref, +.reveal sup a { + color: var(--accent-600); + text-decoration: none; + font-family: var(--font-mono); + font-size: 0.7em; + font-weight: 600; + margin: 0 0.15em; +} + +/* ----------------------------------------------------------- + MATH — KaTeX / MathJax + ----------------------------------------------------------- */ +.reveal .katex, +.reveal .MathJax { + font-size: 1em !important; + color: var(--ink); +} +.reveal .katex-display, +.reveal mjx-container[display="true"] { + margin: 0.5em 0 !important; + padding: 0.5em 0; + border-left: 2px solid var(--accent-600); + padding-left: 1em; +} + +/* ----------------------------------------------------------- + CALLOUTS (Quarto) + ----------------------------------------------------------- */ +.reveal div.callout { + border: 1px solid var(--rule); + border-left: 3px solid var(--accent-600); + border-radius: 4px; + background: var(--paper-2); + margin: 0.75em 0; + font-size: 0.78em; + overflow: hidden; +} +.reveal div.callout > div.callout-header { + background: transparent; + padding: 0.5em 0.9em 0.2em; + font-family: var(--font-sans); + font-weight: 600; + font-size: 0.9em; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--accent-600); +} +.reveal div.callout > div.callout-body, +.reveal div.callout-body-container { + padding: 0.2em 0.9em 0.6em; +} +.reveal div.callout.callout-note { border-left-color: var(--navy-600); } +.reveal div.callout.callout-note > div.callout-header { color: var(--navy-600); } +.reveal div.callout.callout-warning { border-left-color: #9A5F18; } +.reveal div.callout.callout-warning > div.callout-header { color: #9A5F18; } +.reveal div.callout.callout-important, +.reveal div.callout.callout-caution { border-left-color: #A6271D; } +.reveal div.callout.callout-important > div.callout-header, +.reveal div.callout.callout-caution > div.callout-header { color: #A6271D; } +.reveal div.callout.callout-tip { border-left-color: var(--moss-600); } +.reveal div.callout.callout-tip > div.callout-header { color: var(--moss-600); } + +/* ----------------------------------------------------------- + SLIDE NUMBER + FOOTER BRAND CHROME + ----------------------------------------------------------- */ +.reveal .slide-number { + font-family: var(--font-mono); + font-size: 14px; + font-weight: 600; + color: var(--ink-3); + background: transparent; + right: 24px; + bottom: 18px; + letter-spacing: var(--tracking-caps); +} +.reveal .slide-number a { color: inherit; } + +/* Brand footer mark — inserted by extension JS */ +.reveal .civilytics-footer { + position: absolute; + left: 24px; + bottom: 18px; + display: flex; + align-items: center; + gap: 10px; + font-family: var(--font-sans); + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--ink-3); + pointer-events: none; + z-index: 30; +} +.reveal .civilytics-footer img, +.reveal .civilytics-footer svg { + width: 22px; + height: auto; + opacity: 0.85; + flex: 0 0 auto; +} + +/* Hide chrome on title / section / thank-you / full-bleed slides */ +.reveal .slides > section.title-slide ~ *, +.reveal #title-slide.present ~ .civilytics-footer { /* no-op, readable marker */ } +.reveal .slides > section.title-slide.present ~ .controls, +.reveal .slides > section.section-divider.present ~ .controls, +.reveal .slides > section.thank-you.present ~ .controls { } +body.civilytics-hide-chrome .civilytics-footer, +body.civilytics-hide-chrome .slide-number { + display: none !important; +} + +/* ----------------------------------------------------------- + PROGRESS BAR + ----------------------------------------------------------- */ +.reveal .progress { + color: var(--accent-600); + height: 3px; + background: rgba(14, 26, 43, 0.08); +} + +/* ----------------------------------------------------------- + CONTROLS + ----------------------------------------------------------- */ +.reveal .controls { + color: var(--ink-3); +} +.reveal .controls button { color: var(--ink-3) !important; } +.reveal .controls button:hover { color: var(--accent-600) !important; } + +/* ----------------------------------------------------------- + PRINT / PDF EXPORT + ----------------------------------------------------------- */ +@media print { + .reveal .civilytics-footer, + .reveal .slide-number { color: var(--ink-3); } + .reveal section { break-inside: avoid; } +} + +/* ----------------------------------------------------------- + SMALL-SCREEN GUARDRAILS + Reveal scales slides, but the narrowest keep-legible margins. + ----------------------------------------------------------- */ +@media (max-width: 640px) { + .reveal .slides > section, + .reveal .slides > section > section { + padding: 32px 36px; + } + .reveal section.section-divider, + .reveal section.thank-you { + padding: 48px 48px; + } +} diff --git a/inst/quarto/extensions/civilytics-reveal/civilytics.scss b/inst/quarto/extensions/civilytics-reveal/civilytics.scss new file mode 100644 index 0000000..c3921d9 --- /dev/null +++ b/inst/quarto/extensions/civilytics-reveal/civilytics.scss @@ -0,0 +1,1056 @@ +/*-- scss:defaults --*/ + +// Civilytics Reveal.js theme — defaults +// These map into Reveal/Quarto's theme variables so built-in +// components (lists, links, code, etc.) pick up the brand. + +// Fonts — loaded by the extension's @import block below +$font-family-sans-serif: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif !default; +$font-family-serif: "Source Serif 4", "Source Serif Pro", Georgia, "Times New Roman", serif !default; +$font-family-monospace: "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace !default; +$font-family-display: "Libre Franklin", "Franklin Gothic", "Inter", system-ui, sans-serif !default; + +$presentation-font-size-root: 32px !default; +$presentation-font-smaller: 0.7 !default; +$presentation-line-height: 1.45 !default; + +$body-bg: #FAF7F2 !default; +$body-color: #0E1A2B !default; + +$link-color: #22406A !default; +$link-color-hover: #1A2E4A !default; + +$selection-bg: #F8C8A3 !default; +$selection-color: #0E1A2B !default; + +$presentation-heading-font: $font-family-display !default; +$presentation-heading-color: #0E1A2B !default; +$presentation-heading-font-weight: 800 !default; +$presentation-heading-line-height: 1.08 !default; +$presentation-heading-letter-spacing: -0.025em !default; +$presentation-heading-text-transform: none !default; + +$presentation-h1-font-size: 2.4em !default; +$presentation-h2-font-size: 1.7em !default; +$presentation-h3-font-size: 1.3em !default; +$presentation-h4-font-size: 1.1em !default; + +$code-block-bg: #0E1A2B !default; +$code-block-color: #FAF7F2 !default; +$code-color: #923D00 !default; + +/*-- scss:rules --*/ + +/* ========================================================== + Civilytics Reveal.js theme — rules + Editorial, serif-led slides. Designed for Quarto + Pandoc. + Tokens mirror /colors_and_type.css from the Civilytics DS. + ========================================================== */ + +@import url('https://fonts.googleapis.com/css2?family=Libre+Franklin:ital,wght@0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&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'); + +:root { + /* Civilytics tokens — keep names in sync with the design system */ + --paper: #FAF7F2; + --paper-2: #F2EDE4; + --paper-3: #E6DFD1; + --rule: #D6CEBD; + --rule-strong: #B8AE97; + + --ink: #0E1A2B; + --ink-2: #2B3A52; + --ink-3: #5A6A82; + --ink-4: #8C97AB; + + --navy-900: #0E1A2B; + --navy-700: #1A2E4A; + --navy-600: #22406A; + --navy-500: #2E5590; + --navy-100: #DDE6F2; + + --accent-900: #451A00; + --accent-700: #923D00; + --accent-600: #C25311; + --accent-500: #DB6C25; + --accent-300: #F2A976; + --accent-200: #F8C8A3; + --accent-100: #FBE0C6; + + --teal-600: #1F6F70; + --plum-600: #6B3A5E; + --moss-600: #4A6B2F; + --brass-600: #B8751C; + + --font-display: "Libre Franklin", "Franklin Gothic", "Inter", system-ui, sans-serif; + --font-serif: "Source Serif 4", "Source Serif Pro", Georgia, serif; + --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + --font-mono: "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace; + + --tracking-caps: 0.08em; +} + +/* ----------------------------------------------------------- + Reveal root & slide chrome + ----------------------------------------------------------- */ +.reveal-viewport { + background: var(--paper); +} + +.reveal { + font-family: var(--font-sans); + font-size: 32px; + font-weight: 400; + color: var(--ink); + letter-spacing: 0; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +.reveal ::selection { background: var(--accent-200); color: var(--ink); } +.reveal ::-moz-selection { background: var(--accent-200); color: var(--ink); } + +.reveal .slides { + text-align: left; +} + +.reveal .slides > section, +.reveal .slides > section > section { + padding: 48px 72px; + line-height: 1.45; + font-weight: inherit; + box-sizing: border-box; + height: 100%; +} + +/* ----------------------------------------------------------- + Headings + ----------------------------------------------------------- */ +.reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { + font-family: var(--font-display); + color: var(--ink); + font-weight: 800; + line-height: 1.08; + letter-spacing: -0.025em; + text-transform: none; + text-wrap: balance; + margin: 0 0 0.5em; +} + +.reveal h1 { font-size: 2.4em; font-weight: 900; letter-spacing: -0.035em; line-height: 1.0; } +.reveal h2 { font-size: 1.7em; font-weight: 800; letter-spacing: -0.03em; } +.reveal h3 { font-size: 1.3em; font-weight: 700; letter-spacing: -0.022em; } +.reveal h4 { font-size: 1.1em; font-weight: 700; letter-spacing: -0.018em; } + +/* Eyebrow kicker — used above headlines on many layouts */ +.reveal .eyebrow, +.reveal [data-eyebrow]::before { + display: inline-flex; + align-items: center; + gap: 10px; + font-family: var(--font-sans); + font-size: 0.5em; + font-weight: 600; + color: var(--accent-600); + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + margin: 0 0 18px; +} +.reveal .eyebrow::before { + content: ""; + display: inline-block; + width: 28px; + height: 2px; + background: var(--accent-600); +} +.reveal [data-eyebrow]::before { + content: attr(data-eyebrow); +} + +/* ----------------------------------------------------------- + Body copy, lists, links + ----------------------------------------------------------- */ +.reveal p { + font-family: var(--font-sans); + line-height: 1.55; + margin: 0 0 0.75em; + text-wrap: pretty; +} + +.reveal .lead, +.reveal p.lead { + font-family: var(--font-serif); + font-size: 1.15em; + font-weight: 400; + line-height: 1.5; + color: var(--ink); + max-width: 26em; + padding-left: 0.75em; + border-left: 2px solid var(--accent-600); + margin: 0.5em 0 1em; +} + +.reveal ul, .reveal ol { + display: block; + margin: 0.25em 0 0.75em; + padding-left: 1.3em; +} +.reveal li { + margin: 0.35em 0; + line-height: 1.5; +} +.reveal ul { list-style: none; } +.reveal ul > li { position: relative; } +.reveal ul > li::before { + content: ""; + position: absolute; + left: -1.1em; + top: 0.7em; + width: 10px; + height: 2px; + background: var(--accent-600); +} +.reveal ol { list-style: decimal; } +.reveal ol > li::marker { + color: var(--accent-600); + font-family: var(--font-mono); + font-weight: 600; +} + +.reveal a { + color: var(--navy-600); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 3px; + transition: color 120ms ease-out, text-decoration-thickness 120ms ease-out; +} +.reveal a:hover { + color: var(--navy-700); + text-decoration-thickness: 2px; +} + +.reveal hr { + border: none; + border-top: 1px solid var(--rule); + margin: 0.75em 0; +} + +.reveal strong, .reveal b { font-weight: 700; color: var(--ink); } +.reveal em, .reveal i { font-style: italic; } + +/* Small caps helper — for metadata rows */ +.reveal .smallcaps, +.reveal .meta { + font-family: var(--font-sans); + font-size: 0.55em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--ink-3); +} + +/* ----------------------------------------------------------- + Block quote / pullquote + ----------------------------------------------------------- */ +.reveal blockquote { + font-family: var(--font-serif); + font-size: 1.1em; + font-style: italic; + line-height: 1.4; + color: var(--ink); + border-left: 3px solid var(--accent-600); + padding: 0.25em 0 0.25em 1em; + margin: 0.75em 0; + background: transparent; + box-shadow: none; + width: auto; +} + +/* Big pullquote layout — .pullquote on a section */ +.reveal section.pullquote { + display: flex; + flex-direction: column; + justify-content: center; +} +.reveal section.pullquote blockquote { + font-size: 1.8em; + font-style: italic; + line-height: 1.2; + border-left: none; + padding: 0; + max-width: 22em; + letter-spacing: -0.01em; +} +.reveal section.pullquote blockquote::before { + content: "\201C"; + display: block; + font-family: var(--font-serif); + font-size: 2.2em; + line-height: 0.9; + color: var(--accent-600); + margin-bottom: 0.1em; + font-style: normal; +} +.reveal section.pullquote .attribution, +.reveal section.pullquote footer { + margin-top: 1em; + font-family: var(--font-sans); + font-size: 0.55em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--ink-3); + font-style: normal; +} +.reveal section.pullquote .attribution::before { + content: "— "; + color: var(--accent-600); +} + +/* ----------------------------------------------------------- + Code + ----------------------------------------------------------- */ +.reveal code { + font-family: var(--font-mono); + font-size: 0.85em; + color: var(--accent-700); + background: var(--paper-2); + padding: 0.08em 0.35em; + border-radius: 2px; + font-feature-settings: "tnum" 1, "zero" 1; +} + +.reveal pre { + font-family: var(--font-mono); + font-size: 0.55em; + line-height: 1.5; + background: var(--ink); + color: var(--paper); + padding: 1em 1.25em; + border-radius: 8px; + box-shadow: none; + width: 100%; + max-width: 100%; + overflow: auto; + margin: 0.5em 0; +} +.reveal pre code { + background: transparent; + color: inherit; + padding: 0; + font-size: 1em; + display: block; + overflow: visible; + white-space: pre; + max-height: none; +} + +/* Quarto code-line-number + highlight markers */ +.reveal pre code .hljs-ln-numbers, +.reveal pre code .code-line-number { + color: var(--ink-4); + padding-right: 1em; + user-select: none; +} +.reveal div.sourceCode { + background: transparent; + border: none; + margin: 0.5em 0; +} +.reveal code span.co { color: #8C97AB; font-style: italic; } /* comments */ +.reveal code span.st { color: #F2A976; } /* strings */ +.reveal code span.dv, +.reveal code span.fl { color: #F8C8A3; } /* numbers */ +.reveal code span.kw { color: #EA8A49; font-weight: 600; } /* keywords */ +.reveal code span.cf { color: #EA8A49; font-weight: 600; } /* control flow */ +.reveal code span.fu { color: #B3C6E0; } /* function */ +.reveal code span.op { color: #D6CEBD; } /* operator */ +.reveal code span.cn, +.reveal code span.va { color: #7CB3B3; } /* variables */ +.reveal code span.at { color: #7CB3B3; } + +/* Quarto line-highlighting */ +.reveal pre code.sourceCode > span.has-line-highlight { + background: rgba(248, 200, 163, 0.14); +} + +/* ----------------------------------------------------------- + Tables + ----------------------------------------------------------- */ +.reveal table { + font-family: var(--font-sans); + font-size: 0.7em; + border-collapse: collapse; + margin: 0.5em 0; + width: 100%; + font-variant-numeric: tabular-nums; +} +.reveal table th, +.reveal table td { + padding: 0.55em 0.9em; + text-align: left; + border: none; + border-bottom: 1px solid var(--rule); + vertical-align: baseline; +} +.reveal table th { + font-family: var(--font-sans); + font-size: 0.82em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--ink-3); + border-bottom: 2px solid var(--ink); + padding-bottom: 0.4em; + background: transparent; +} +.reveal table tbody tr:nth-child(even) td { + background: var(--paper-2); +} +.reveal table tbody tr:last-child td { + border-bottom: 2px solid var(--ink); +} +/* Numeric columns — add .numeric to a / */ +.reveal table td.numeric, +.reveal table th.numeric, +.reveal table td[align="right"], +.reveal table th[align="right"] { + text-align: right; + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; +} +.reveal table caption { + caption-side: top; + text-align: left; + font-family: var(--font-sans); + font-size: 0.55em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--accent-600); + margin-bottom: 0.5em; +} + +/* ----------------------------------------------------------- + Images & figures + ----------------------------------------------------------- */ +.reveal img, +.reveal section img { + background: transparent; + border: none; + box-shadow: none; + margin: 0.25em 0; + max-width: 100%; + height: auto; +} +.reveal figure { + margin: 0.5em 0; +} +.reveal figure img { + display: block; + border: 1px solid var(--rule); +} +.reveal figure figcaption, +.reveal .caption { + font-family: var(--font-sans); + font-size: 0.55em; + line-height: 1.4; + color: var(--ink-3); + margin-top: 0.5em; + max-width: 42em; +} +.reveal figure figcaption strong, +.reveal .caption strong { + color: var(--accent-600); + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + font-size: 0.9em; + display: inline-block; + margin-right: 0.5em; +} + +/* ----------------------------------------------------------- + TITLE SLIDE + Quarto renders the title slide with #title-slide +

+ ----------------------------------------------------------- */ +.reveal #title-slide, +.reveal section.title-slide { + padding: 72px 80px; + display: flex !important; + flex-direction: column; + justify-content: space-between; + text-align: left; + background: + linear-gradient(180deg, var(--paper) 0%, var(--paper) 58%, var(--paper-2) 100%); +} + +.reveal #title-slide .civilytics-mark, +.reveal section.title-slide .civilytics-mark { + display: block; + width: 72px; + height: 72px; + margin-bottom: 32px; + object-fit: contain; +} + +/* Civilytics wordmark lockup — bold serif "Civilytics" + inline pulse accent. + Rendered as an tag pointing at the official wordmark SVG asset + (assets/civilytics-wordmark.svg for ink-on-paper slides, + assets/civilytics-wordmark-reverse.svg for paper-on-ink slides). + Source viewBox is 558×160 (aspect ratio ≈ 3.4875). */ +img.civilytics-wordmark, +.reveal img.civilytics-wordmark, +.reveal .civilytics-wordmark { + display: block; + height: 48px; + width: 167.4px; /* 558/160 aspect * 48px height */ + flex: 0 0 auto; + align-self: flex-start; + object-fit: contain; + margin: 0 0 36px; +} + +.reveal #title-slide .civilytics-wordmark, +.reveal section.title-slide .civilytics-wordmark { + height: 48px; + width: 167.4px; + margin: 0 0 36px; + color: var(--ink); +} + +.reveal #title-slide .title, +.reveal section.title-slide h1.title, +.reveal section.title-slide h1:first-of-type { + font-family: var(--font-display); + font-size: 2.6em; + font-weight: 900; + letter-spacing: -0.035em; + line-height: 0.98; + color: var(--ink); + margin: 0 0 0.3em; + max-width: 18em; + text-wrap: balance; +} +.reveal #title-slide .subtitle, +.reveal section.title-slide p.subtitle, +.reveal section.title-slide .subtitle { + font-family: var(--font-serif); + font-size: 1.05em; + font-weight: 400; + font-style: italic; + line-height: 1.35; + color: var(--ink-2); + margin: 0 0 1em; + max-width: 28em; + padding-left: 0.75em; + border-left: 2px solid var(--accent-600); +} + +/* Author / date / affiliation block */ +.reveal #title-slide .quarto-title-authors, +.reveal #title-slide .quarto-title-author-name, +.reveal section.title-slide .author, +.reveal section.title-slide .date, +.reveal section.title-slide .affiliation { + font-family: var(--font-sans); + color: var(--ink-2); +} +.reveal #title-slide .author, +.reveal #title-slide .quarto-title-author-name, +.reveal section.title-slide .author { + font-size: 0.7em; + font-weight: 600; + color: var(--ink); + margin: 0.2em 0 0; + letter-spacing: 0; +} +.reveal #title-slide .affiliation, +.reveal #title-slide .quarto-title-affiliation, +.reveal section.title-slide .affiliation { + font-size: 0.55em; + font-weight: 500; + color: var(--ink-3); + letter-spacing: var(--tracking-caps); + text-transform: uppercase; + margin: 0.15em 0 0; +} +.reveal #title-slide .date, +.reveal section.title-slide .date { + font-size: 0.5em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--accent-600); + margin-top: 1em; + padding-top: 0.8em; + border-top: 3px double var(--ink); + display: inline-block; +} + +/* Editorial thick-top / thin-bottom rule block around title */ +.reveal section.title-slide .quarto-title-block, +.reveal section.title-slide .title-block { + border-top: 6px solid var(--ink); + border-bottom: 1px solid var(--rule); + padding: 0.5em 0 1em; +} + +/* ----------------------------------------------------------- + SECTION DIVIDER —
or + Quarto's {.section} header + ----------------------------------------------------------- */ +.reveal section.section-divider, +.reveal section.section { + background: var(--ink); + color: var(--paper); + display: flex !important; + flex-direction: column; + justify-content: center; + padding: 80px 96px; +} +.reveal section.section-divider::before, +.reveal section.section::before { + content: ""; + display: block; + width: 72px; + height: 2px; + background: var(--accent-600); + margin-bottom: 24px; +} +.reveal section.section-divider h1, +.reveal section.section-divider h2, +.reveal section.section-divider h3, +.reveal section.section h1, +.reveal section.section h2, +.reveal section.section h3 { + color: var(--paper); + font-size: 2.8em; + font-weight: 900; + letter-spacing: -0.035em; + line-height: 1.0; + max-width: 18em; +} +.reveal section.section-divider .eyebrow, +.reveal section.section .eyebrow { + color: var(--accent-300); +} +.reveal section.section-divider .eyebrow::before, +.reveal section.section .eyebrow::before { + background: var(--accent-300); +} +.reveal section.section-divider p, +.reveal section.section p { + color: var(--paper); + opacity: 0.82; + font-family: var(--font-serif); + font-size: 1.05em; + font-style: italic; + max-width: 28em; +} +.reveal section.section-divider .section-number, +.reveal section.section .section-number { + font-family: var(--font-mono); + font-size: 0.6em; + font-weight: 600; + color: var(--accent-300); + letter-spacing: var(--tracking-caps); + text-transform: uppercase; + margin-bottom: 0.5em; +} + +/* ----------------------------------------------------------- + FULL-BLEED IMAGE —
+ ----------------------------------------------------------- */ +.reveal section.full-bleed { + padding: 0 !important; +} +.reveal section.full-bleed .caption, +.reveal section.full-bleed figcaption { + position: absolute; + left: 48px; + right: 48px; + bottom: 48px; + background: var(--paper); + padding: 14px 18px; + border-left: 3px solid var(--accent-600); + font-family: var(--font-sans); + font-size: 0.5em; + line-height: 1.45; + color: var(--ink); + max-width: 36em; + box-shadow: 0 4px 12px rgba(14, 26, 43, 0.12); +} +.reveal section.full-bleed .caption strong, +.reveal section.full-bleed figcaption strong { + display: block; + font-size: 0.85em; + font-weight: 700; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--accent-600); + margin-bottom: 4px; +} + +/* ----------------------------------------------------------- + BIG STAT / DATA CALLOUT — .big-stat section or .stat block + ----------------------------------------------------------- */ +.reveal .stat-number { + font-family: var(--font-serif); + font-weight: 700; + font-size: 3.6em; + line-height: 1; + letter-spacing: -0.02em; + font-variant-numeric: tabular-nums; + color: var(--ink); + margin: 0 0 0.1em; + display: block; +} +.reveal .stat-label { + font-family: var(--font-sans); + font-size: 0.55em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--ink-3); +} +.reveal .stat-note { + font-family: var(--font-serif); + font-style: italic; + font-size: 0.9em; + color: var(--ink-2); + max-width: 22em; + margin-top: 0.6em; +} +.reveal section.big-stat { + display: flex !important; + flex-direction: column; + justify-content: center; +} +.reveal section.big-stat .stat-number { + font-size: 5em; +} + +/* Stat grid — multiple stats side-by-side */ +.reveal .stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 2em 3em; + margin: 1em 0; +} +.reveal .stat-grid .stat { + border-top: 3px solid var(--ink); + padding-top: 0.6em; +} +.reveal .stat-grid .stat .stat-number { font-size: 2.8em; } + +/* ----------------------------------------------------------- + TWO-COLUMN — Quarto renders {.columns}{.column width=50%} + ----------------------------------------------------------- */ +.reveal .columns { + display: flex; + gap: 3em; + align-items: flex-start; + margin: 0.25em 0; +} +.reveal .column { + flex: 1; + min-width: 0; +} +.reveal .column > :first-child { margin-top: 0; } + +/* Hand-rolled two-col for pandoc (non-Quarto) users */ +.reveal .two-col { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 3em; + align-items: start; +} +.reveal .two-col--wide-left { grid-template-columns: 2fr 1fr; } +.reveal .two-col--wide-right { grid-template-columns: 1fr 2fr; } + +/* ----------------------------------------------------------- + AGENDA / TOC slide —
+ Pair with an ordered list; we restyle it. + ----------------------------------------------------------- */ +.reveal section.agenda ol { + list-style: none; + counter-reset: agenda; + padding: 0; + margin: 0.5em 0; +} +.reveal section.agenda ol > li { + counter-increment: agenda; + display: grid; + grid-template-columns: 2.4em 1fr auto; + gap: 0.6em 1.2em; + align-items: baseline; + padding: 0.55em 0; + border-top: 1px solid var(--rule); + font-family: var(--font-serif); + font-size: 1.05em; +} +.reveal section.agenda ol > li:last-child { border-bottom: 1px solid var(--rule); } +.reveal section.agenda ol > li::before { + content: counter(agenda, decimal-leading-zero); + font-family: var(--font-mono); + font-weight: 600; + font-size: 0.7em; + color: var(--accent-600); + letter-spacing: var(--tracking-caps); +} +.reveal section.agenda ol > li .meta, +.reveal section.agenda ol > li em { + font-family: var(--font-sans); + font-style: normal; + font-size: 0.7em; + color: var(--ink-3); + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + justify-self: end; +} + +/* ----------------------------------------------------------- + THANK-YOU / END slide —
+ ----------------------------------------------------------- */ +.reveal section.thank-you { + background: var(--ink); + color: var(--paper); + display: flex !important; + flex-direction: column; + justify-content: center; + padding: 80px 96px; +} +.reveal section.thank-you .civilytics-mark { + display: block; + width: 56px; + height: 56px; + margin-bottom: 28px; + object-fit: contain; +} +.reveal section.thank-you .civilytics-wordmark { + height: 40px; + width: 139.5px; /* 558/160 aspect * 40 */ + flex: 0 0 auto; + align-self: flex-start; + margin: 0 0 32px; +} +.reveal section.thank-you h1, +.reveal section.thank-you h2 { + color: var(--paper); + font-size: 3em; + font-weight: 900; + letter-spacing: -0.035em; + margin-bottom: 0.2em; +} +.reveal section.thank-you p { + color: var(--paper); + opacity: 0.85; + font-family: var(--font-serif); + font-size: 1.1em; + font-style: italic; + max-width: 26em; +} +.reveal section.thank-you .contact, +.reveal section.thank-you ul.contact { + list-style: none; + padding: 0; + margin: 1.5em 0 0; + font-family: var(--font-sans); + font-size: 0.6em; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--accent-300); +} +.reveal section.thank-you .contact li, +.reveal section.thank-you ul.contact li { + margin: 0.3em 0; +} +.reveal section.thank-you .contact li::before, +.reveal section.thank-you ul.contact li::before { display: none; } +.reveal section.thank-you a { color: var(--paper); text-decoration: underline; text-decoration-color: var(--accent-600); } +.reveal section.thank-you a:hover { color: var(--accent-300); } + +/* ----------------------------------------------------------- + REFERENCES / SOURCES — small, tight + ----------------------------------------------------------- */ +.reveal .references, +.reveal section.references { + font-size: 0.72em; +} +.reveal section.references h2 { font-size: 1.4em; } +.reveal .references ol, +.reveal .references ul { + padding-left: 1.4em; +} +.reveal .references li { + font-family: var(--font-sans); + font-size: 0.95em; + line-height: 1.4; + color: var(--ink-2); + margin: 0.35em 0; +} +.reveal .references li::before { display: none; } + +/* Footnotes (Pandoc / Quarto) */ +.reveal .footnotes, +.reveal div.footnotes { + font-size: 0.6em; + color: var(--ink-3); + border-top: 1px solid var(--rule); + padding-top: 0.6em; + margin-top: 1em; +} +.reveal a.footnote-ref, +.reveal sup a { + color: var(--accent-600); + text-decoration: none; + font-family: var(--font-mono); + font-size: 0.7em; + font-weight: 600; + margin: 0 0.15em; +} + +/* ----------------------------------------------------------- + MATH — KaTeX / MathJax + ----------------------------------------------------------- */ +.reveal .katex, +.reveal .MathJax { + font-size: 1em !important; + color: var(--ink); +} +.reveal .katex-display, +.reveal mjx-container[display="true"] { + margin: 0.5em 0 !important; + padding: 0.5em 0; + border-left: 2px solid var(--accent-600); + padding-left: 1em; +} + +/* ----------------------------------------------------------- + CALLOUTS (Quarto) + ----------------------------------------------------------- */ +.reveal div.callout { + border: 1px solid var(--rule); + border-left: 3px solid var(--accent-600); + border-radius: 4px; + background: var(--paper-2); + margin: 0.75em 0; + font-size: 0.78em; + overflow: hidden; +} +.reveal div.callout > div.callout-header { + background: transparent; + padding: 0.5em 0.9em 0.2em; + font-family: var(--font-sans); + font-weight: 600; + font-size: 0.9em; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--accent-600); +} +.reveal div.callout > div.callout-body, +.reveal div.callout-body-container { + padding: 0.2em 0.9em 0.6em; +} +.reveal div.callout.callout-note { border-left-color: var(--navy-600); } +.reveal div.callout.callout-note > div.callout-header { color: var(--navy-600); } +.reveal div.callout.callout-warning { border-left-color: #9A5F18; } +.reveal div.callout.callout-warning > div.callout-header { color: #9A5F18; } +.reveal div.callout.callout-important, +.reveal div.callout.callout-caution { border-left-color: #A6271D; } +.reveal div.callout.callout-important > div.callout-header, +.reveal div.callout.callout-caution > div.callout-header { color: #A6271D; } +.reveal div.callout.callout-tip { border-left-color: var(--moss-600); } +.reveal div.callout.callout-tip > div.callout-header { color: var(--moss-600); } + +/* ----------------------------------------------------------- + SLIDE NUMBER + FOOTER BRAND CHROME + ----------------------------------------------------------- */ +.reveal .slide-number { + font-family: var(--font-mono); + font-size: 14px; + font-weight: 600; + color: var(--ink-3); + background: transparent; + right: 24px; + bottom: 18px; + letter-spacing: var(--tracking-caps); +} +.reveal .slide-number a { color: inherit; } + +/* Brand footer mark — inserted by extension JS */ +.reveal .civilytics-footer { + position: absolute; + left: 24px; + bottom: 18px; + display: flex; + align-items: center; + gap: 10px; + font-family: var(--font-sans); + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + color: var(--ink-3); + pointer-events: none; + z-index: 30; +} +.reveal .civilytics-footer img, +.reveal .civilytics-footer svg { + width: 22px; + height: auto; + opacity: 0.85; + flex: 0 0 auto; +} + +/* Hide chrome on title / section / thank-you / full-bleed slides */ +.reveal .slides > section.title-slide ~ *, +.reveal #title-slide.present ~ .civilytics-footer { /* no-op, readable marker */ } +.reveal .slides > section.title-slide.present ~ .controls, +.reveal .slides > section.section-divider.present ~ .controls, +.reveal .slides > section.thank-you.present ~ .controls { } +body.civilytics-hide-chrome .civilytics-footer, +body.civilytics-hide-chrome .slide-number { + display: none !important; +} + +/* ----------------------------------------------------------- + PROGRESS BAR + ----------------------------------------------------------- */ +.reveal .progress { + color: var(--accent-600); + height: 3px; + background: rgba(14, 26, 43, 0.08); +} + +/* ----------------------------------------------------------- + CONTROLS + ----------------------------------------------------------- */ +.reveal .controls { + color: var(--ink-3); +} +.reveal .controls button { color: var(--ink-3) !important; } +.reveal .controls button:hover { color: var(--accent-600) !important; } + +/* ----------------------------------------------------------- + PRINT / PDF EXPORT + ----------------------------------------------------------- */ +@media print { + .reveal .civilytics-footer, + .reveal .slide-number { color: var(--ink-3); } + .reveal section { break-inside: avoid; } +} + +/* ----------------------------------------------------------- + SMALL-SCREEN GUARDRAILS + Reveal scales slides, but the narrowest keep-legible margins. + ----------------------------------------------------------- */ +@media (max-width: 640px) { + .reveal .slides > section, + .reveal .slides > section > section { + padding: 32px 36px; + } + .reveal section.section-divider, + .reveal section.thank-you { + padding: 48px 48px; + } +} diff --git a/inst/quarto/latex/civilytics-title.tex b/inst/quarto/latex/civilytics-title.tex new file mode 100644 index 0000000..2d31ca2 --- /dev/null +++ b/inst/quarto/latex/civilytics-title.tex @@ -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 diff --git a/inst/quarto/latex/civilytics.tex b/inst/quarto/latex/civilytics.tex new file mode 100644 index 0000000..5bdcc4e --- /dev/null +++ b/inst/quarto/latex/civilytics.tex @@ -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}} diff --git a/inst/quarto/theme/_tokens.scss b/inst/quarto/theme/_tokens.scss new file mode 100644 index 0000000..7dd370c --- /dev/null +++ b/inst/quarto/theme/_tokens.scss @@ -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; diff --git a/inst/quarto/theme/civilytics.scss b/inst/quarto/theme/civilytics.scss new file mode 100644 index 0000000..16b5727 --- /dev/null +++ b/inst/quarto/theme/civilytics.scss @@ -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; } diff --git a/inst/quarto/theme/extras.css b/inst/quarto/theme/extras.css new file mode 100644 index 0000000..ab51606 --- /dev/null +++ b/inst/quarto/theme/extras.css @@ -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; +} diff --git a/inst/quarto/typst/civilytics-typst.typ b/inst/quarto/typst/civilytics-typst.typ new file mode 100644 index 0000000..097f4d4 --- /dev/null +++ b/inst/quarto/typst/civilytics-typst.typ @@ -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$ diff --git a/man/civilytics-package.Rd b/man/civilytics-package.Rd index a132667..3eb4e0f 100644 --- a/man/civilytics-package.Rd +++ b/man/civilytics-package.Rd @@ -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{ diff --git a/man/dot-copy_logos.Rd b/man/dot-copy_logos.Rd new file mode 100644 index 0000000..1fdff42 --- /dev/null +++ b/man/dot-copy_logos.Rd @@ -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} diff --git a/man/dot-copy_pkg_file.Rd b/man/dot-copy_pkg_file.Rd new file mode 100644 index 0000000..3c65fd8 --- /dev/null +++ b/man/dot-copy_pkg_file.Rd @@ -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} diff --git a/man/quarto-helpers.Rd b/man/quarto-helpers.Rd new file mode 100644 index 0000000..cd6eac2 --- /dev/null +++ b/man/quarto-helpers.Rd @@ -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. +} diff --git a/man/use_civilytics_brand.Rd b/man/use_civilytics_brand.Rd new file mode 100644 index 0000000..85dcedf --- /dev/null +++ b/man/use_civilytics_brand.Rd @@ -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() +} +} diff --git a/man/use_civilytics_revealjs.Rd b/man/use_civilytics_revealjs.Rd new file mode 100644 index 0000000..2712acc --- /dev/null +++ b/man/use_civilytics_revealjs.Rd @@ -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() +} +} diff --git a/man/use_civilytics_theme.Rd b/man/use_civilytics_theme.Rd new file mode 100644 index 0000000..e806876 --- /dev/null +++ b/man/use_civilytics_theme.Rd @@ -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() +} +}