---
name: lexis
description: >
  Author and edit Quarto reveal.js slide decks that use the lexis theme
  (`format: lexis-revealjs`, the jhelvy/quarto-lexis extension). Use this whenever
  writing or editing a `.qmd` slide deck in the lexis paradigm — where `---` (not
  `##`) starts every slide, headings are just styled text, and slide modifiers
  (inverse / center / middle / backgrounds) are `{{< … >}}` shortcodes. Covers the
  slide model, shortcodes, inline styling classes, columns, image treatments,
  panels, the hand-authored title slide, footers, code line highlighting, and the
  knitr/fragment gotchas specific to this format. Includes drop-in scaffolds for
  common layout requests ("make a two-column slide"). Also porting
  xaringan/remark `.Rmd` decks (the original lexis theme) to lexis-revealjs.
metadata:
  author: John Paul Helveston
  repo: https://github.com/jhelvy/quarto-lexis
  version: "1.1.0"
---

# Authoring lexis slides

lexis is a Quarto reveal.js **format extension** (`format: lexis-revealjs`, from
`jhelvy/quarto-lexis`). It ports the xaringan/remark `lexis` theme, so it keeps
that theme's authoring model rather than stock Quarto's. **The paradigm is the
whole point — get it right and everything else follows.**

## The one rule that changes everything

**`---` starts every slide. `#`/`##`/`###` are just text sizes _within_ a slide,
never slide breaks.** The format sets `slide-level: 0` to get this.

This is the opposite of stock Quarto reveal.js, where every `##` begins a new
slide. When writing or editing a lexis deck, **never** reach for `##` to make a
new slide — use `---`. Use headings only to size text on the current slide.

```markdown
# Big text

## Smaller text

Regular body text.

---

# The next slide
```

A heading placed **inside** a fenced div (e.g. a column) is rendered as styled
text (`.h1`…`.h6`) so it can't accidentally break the slide.

## Slide modifiers (shortcodes)

A `---`-delimited slide has no heading to hang attributes on, so slide-level
modifiers are **shortcodes**. Put each on its own line, anywhere on the slide
(top is cleanest). They stack.

| Shortcode | Effect |
|---|---|
| `{{< inverse >}}` | Dark slide: `#121212` bg, white text, orange inline code |
| `{{< center >}}` | Center all content horizontally |
| `{{< middle >}}` | Center all content vertically |
| `{{< bg-color "#909099" >}}` | Full-slide background color |
| `{{< bg-image "images/x.jpg" >}}` | Full-slide background image (optional `size=` / `position=`) |
| `{{< no-slide-number >}}` | Hide the slide number on this slide |

A section-divider slide is just the stack:

```markdown
{{< inverse >}}
{{< center >}}
{{< middle >}}

# Section title
```

Mechanism (for debugging, not for authoring): each shortcode emits an invisible
marker span; the `lexis.lua` filter (running `post-quarto`) hoists it onto the
slide's `<section>`. If a modifier "doesn't take," check the shortcode is spelled
exactly as above and sits inside the intended `---`…`---` region.

## Inline text styling

xaringan's `.class[text]` becomes a Quarto span: `[text]{.class}`.

- `[text]{.fancy}` — Lobster Two display font
- **Colors:** `.red` `.orange` `.yellow` `.green` `.darkgreen` `.blue`
  `.darkblue` `.purple` `.black` `.white` `.gray`
- **Sizes:** `.small` `.large`, or `.font10` … `.font200` (5% steps) for an
  exact percentage. This is the one sizing system for *all* content: as a span
  for inline text, or as a fenced div (`::: {.font70}`) around paragraphs,
  lists, or code chunks — a wrapped chunk and its printed output scale
  together. (There is no separate `.codeNN` system; xaringan-era `.codeNN`
  classes must be converted to `.fontNN` when porting old decks.)
- Standard markdown works: `_italic_`, `**bold**`, `~~strike~~`, `` `code` ``

To align just a heading (not the whole slide), put the class on a span inside it:
`# [Title]{.center}` — also `.left`, `.right`. (Whole-slide centering is the
`{{< center >}}` shortcode instead.)

## Columns

Write **consecutive `::: {.col}` divs with no outer wrapper**. Any run of two or
more `.col` divs in a row is grouped automatically and splits the space evenly —
there is no separate class for 2 vs 3 vs 4 columns.

```markdown
::: {.col}
Left half.
:::

::: {.col}
Right half.
:::
```

For an uneven split, set `width` on a column. Columns without one share what's
left, so usually you set it on only one side:

```markdown
::: {.col width="65%"}
:::

::: {.col width="35%"}
:::
```

`width` takes any percentage (no fixed set of splits). You can also set
`gap="3em"` or `valign="middle"` on any `.col` in a row and it applies to the
whole row.

## Cards

A grid of icon-and-text boxes. Same ergonomics as columns: write **consecutive
`::: {.card}` divs with no outer wrapper** and any run is grouped into a grid.

```markdown
::: {.card .icon-left color="dodgerblue"}
{{< fa map >}}

### Plan

Make a plan before doing big work.

[read-only]{.tag}
:::

::: {.card .icon-left color="orange"}
{{< fa bolt >}}

### Auto

Approves everything.

[use with care]{.tag}
:::
```

Up to four cards go in one row; more wrap into a balanced grid (8 → 4×2). Set
`cols=` on any card in the run to choose, e.g. `cols="2"` for a 2×2 of four
cards.

**The icon is the card's first line**, and it can come from any icon library —
lexis only sees it after the shortcode has expanded, so it needs to know
nothing about them:

| Written as | Needs |
|---|---|
| `{{< fa map >}}` | `quarto-ext/fontawesome` (ships with the template) |
| `{{< bi map >}}` | `shafayetShafee/bsicons` |
| `{{< iconify mdi:map >}}` | `mcanouil/quarto-iconify` |
| `🏔️` / `![](images/icons/map.svg)` | nothing |

Icon *fonts* inherit the card's accent color automatically — don't set a color
on them. **Prefer `{{< fa … >}}` over shipping SVG files** unless the user
already has icon files or asks for one.

| Attribute | Effect |
|---|---|
| `icon=` | Shorthand for an image file or emoji, instead of a first line |
| `image=` | Cropped photo band across the top of the card |
| `color=` | Accent color: a palette name (`green`, `dodgerblue`) or any CSS color |
| `badge=` | Small accent flag in the top-right corner |
| `cols=` | Cards per row (set on any card in the run) |
| `gap=` | Gutter between cards (set on any card in the run) |

| Class | Effect |
|---|---|
| `.icon-left` | Icon beside the heading instead of above it |
| `.tint` | Paint the accent color through an SVG icon *file* |
| `.highlight` | Accent fill and halo — "this one" |
| `.center` | Center the card's contents (including the icon tile) |
| `.fragment` | Reveal this card on its own click |

Notes worth having:

- **One `color=` per card drives everything** — heading, icon tile, border,
  `.tag` chip, badge. Don't hand-set colors inside a card; set the accent.
- **`.tint` is for icon *files* only** — it paints the accent through the SVG
  instead of showing the file's own colors, so one plain line-art set serves a
  whole deck. Icon-font glyphs already take the accent; `.tint` on a card whose
  icon is a `{{< fa >}}` glyph does nothing.
- `[text]{.tag}` is a chip; usable anywhere, but inside a card it takes the
  accent and sits on the card's bottom edge, so chips line up across a row
  whatever the prose length.
- **Reveal cards one at a time with `::: {.card .fragment}`, not `. . .`** — a
  pause between cards splits the run into two separate grids.
- Everything is sized in `em`, so `::: {.font80}` around a grid scales the
  boxes, tiles and chips together.

## One div, many classes

Classes combine on a single fenced div — **never nest a div just to add a
class**. This applies everywhere, but shows up most with columns:

```markdown
<!-- verbose — don't do this -->      <!-- write this instead -->
::: {.col}                            ::: {.col .font60 .center}
::: {.font60}                         content
::: {.center}                         :::
content
:::
:::
:::
```

Same for spans: `[text]{.red .font80}`. Two rules that silently break this:
every class needs its own leading dot (`{.col font60}` drops `font60`), and
key–value attributes like `width="30%"` mix freely with classes on the same
fence (`::: {.col .font60 width="30%"}`).

## Scaffolds for common requests

When asked to "add/make a … slide," insert the matching scaffold as a **new
`---` slide** and leave the bodies blank (or drop in only content the user
supplied) — don't invent filler text. Scaffolds already follow the
one-div-many-classes rule; extra classes go on the existing fence.

**Two-column slide** ("two columns", "side by side"):

```markdown
---

# Title

::: {.col}

:::

::: {.col}

:::
```

For an uneven split, put `width` on one column: `::: {.col width="65%"}`. For
three or four columns, just add more `.col` divs — the row splits evenly.

**Section divider** ("section break", "divider slide"):

```markdown
---

{{< inverse >}}
{{< center >}}
{{< middle >}}

# Section title
```

**Image beside text** ("image on the right"):

```markdown
---

# Title

::: {.col}

:::

::: {.col}
![](images/image.png)
:::
```

**Code + output side by side**:

````markdown
---

# Title

::: {.col .font80}
```{r}
#| echo: true
#| eval: false

```
:::

::: {.col}
```{r}
#| echo: false

```
:::
````

**Card grid** ("cards", "grid of boxes", "feature grid"):

```markdown
---

# Title

::: {.card .icon-left color="dodgerblue"}
{{< fa map >}}

### 

:::

::: {.card .icon-left color="orange"}
{{< fa bolt >}}

### 

:::
```

Add more `.card` divs for more boxes; `cols="2"` on any of them for a 2×2. Use
a Font Awesome name that actually exists; only reference an `icon=` file that
exists, otherwise leave the attribute off.

**Full-bleed image slide**:

```markdown
---

{{< bg-image "images/image.jpg" >}}
{{< no-slide-number >}}
```

## Images

Wrap the image (or a code cell that renders a plot) in a treatment div:

```markdown
::: {.border}
![](images/photo.jpg)
:::
```

Treatments: `.border` `.borderthick` `.whiteborder` `.whiteborderthick`
`.polaroid` `.circle` `.thumbnail` `.noborder`. **The default is no border** —
only wrap when you want a treatment. Each works identically around a rendered
ggplot: wrap the ```` ```{r} ```` cell instead of the `![]()`.

## Panels / tabs

xaringan's `.panelset` → Quarto's native `::: {.panel-tabset}`, with a `###`
heading naming each tab.

```markdown
::: {.panel-tabset}

### R Code
```{r}
#| echo: true
#| fig-show: hide
plot(1:10)
```

### Plot
```{r}
#| echo: false
plot(1:10)
```

:::
```

## Incremental content

`. . .` on its own line is xaringan's `--`: everything after it on the slide
becomes one fragment, so the slide arrives in steps. Repeat it for more steps.

```markdown
# Findings

The headline number.

. . .

# But look closer

The caveat.
```

Unlike stock Quarto — where `. . .` can't fragment a heading, because a heading
*is* a slide break — headings fragment fine here, since in lexis they're just
text. (The extension's Lua filter does this split itself; Pandoc's own pause
handling doesn't reach inside a `slide-level: 0` slide.)

Two related tools, for when `. . .` is too blunt:

- `::: {.incremental}` around a **bare list** steps through its items.
- `::: {.fragment}` wraps one specific block, leaving the rest of the slide
  alone — use it when the steps aren't simply "the rest of the slide."

## Title slide

**There is no auto-generated title slide** (the extension ships an empty
`title-slide.html` partial that suppresses Quarto's). Author the title slide as
the deck's **first `---` slide**, pulling values from the YAML front matter with
Quarto's built-in `{{< meta >}}` shortcode so nothing is duplicated:

```markdown
{{< inverse >}}
{{< center >}}
{{< middle >}}
{{< no-slide-number >}}

# {{< meta title >}}

## [{{< meta subtitle >}}]{.fancy}

<br>

[{{< meta author >}}]{.large}

{{< meta date >}}
```

Drop any line you don't need; restyle the rest — it's just markdown.

## Footers

Set `footer: "..."` in the YAML header for the repeated black link bar. Use
`::: {.footer-large}` for the block footer on title/closing slides.

## Code line highlighting

xaringan's trailing `#<<` comment → the `code-line-numbers` cell option:

```` markdown
```{r}
#| code-line-numbers: "4,5"
```
````

## Minimal deck skeleton

```markdown
---
title: "My deck"
subtitle: "a subtitle"
author: "Your Name"
date: "2026-07-10"
format: lexis-revealjs
footer: "<https://example.com>"
footer-align: center
execute:
  echo: false
  warning: false
  message: false
---

{{< inverse >}}
{{< center >}}
{{< middle >}}
{{< no-slide-number >}}

# {{< meta title >}}

## [{{< meta subtitle >}}]{.fancy}

<br>

[{{< meta author >}}]{.large}

{{< meta date >}}

---

# First content slide

Body text.
```

The extension must be present at `_extensions/lexis/` (via
`quarto use template jhelvy/quarto-lexis` or `quarto add jhelvy/quarto-lexis`).
The canonical, feature-complete example is the extension's own `template.qmd`.

To audit an existing deck for errors, legacy syntax, and cleanup opportunities,
use the companion `lexis-clean` skill (`/lexis-clean`).

## Gotchas (these bite; don't rediscover them)

- **Showing a literal ```` ```{r} ```` chunk in a code block:** knitr runs
  *before* Pandoc and greedily parses any ```` ```{r} ```` — even nested inside a
  four-backtick ```` ````markdown ```` block — as a real chunk (→ "duplicate chunk
  label" render error). To *display* code without running it, use a plain
  ```` ```r ```` / ```` ```markdown ```` fence. To show a **literal** ```` ```{r} ````
  chunk, prefix the inner fence with an empty inline expr — `` `r ''` `` — right
  before the backticks (the classic knitr escape).
- **Fragments / incremental (xaringan's `--`):** use `. . .` — see *Incremental
  content* above. Don't reach for `::: {.fragment}` around every block; that's
  the fallback for when the step isn't "the rest of the slide."
- **Backgrounds are attributes, not CSS:** a per-slide background is a reveal DOM
  layer set from a `<section>` attribute — that's why it's a shortcode
  (`{{< bg-color >}}` / `{{< bg-image >}}` / `{{< inverse >}}`), not something you
  style in SCSS.
- **Sizing:** the canvas is 1600×900 with a 34px root font (tuned to match
  xaringan proportions). Prefer relative figure sizes (`out-width`, `fig-width`)
  over pixel-perfect tweaks.
- **Don't demote headings to make slides fit.** If you find yourself turning `#`
  into `##` to "make a new slide," stop — insert a `---` instead. Heading level is
  purely visual size here.
- **A card icon must be the whole first line of the card.** `{{< fa map >}}` on
  its own line becomes the icon tile; the same shortcode inside a sentence just
  renders where it sits. And the library's extension has to be installed in the
  deck (`_extensions/quarto-ext/fontawesome` for `{{< fa >}}`) — without it the
  shortcode renders as literal text, with no render error to warn you.

## Porting an xaringan/remark `.Rmd` to lexis

Mechanical mapping when converting an original-lexis `.Rmd` deck:

| xaringan | lexis (Quarto) |
|---|---|
| `---` slide break | `---` (same — keep them) |
| `class: center, middle, inverse` after `---` | `{{< center >}}` `{{< middle >}}` `{{< inverse >}}` lines |
| `background-image: url(x.jpg)` | `{{< bg-image "x.jpg" >}}` |
| `background-color: #hex` | `{{< bg-color "#hex" >}}` |
| `.red[text]`, `.fancy[text]`, etc. | `[text]{.red}`, `[text]{.fancy}` |
| `.leftcol[]` / `.rightcol[]` / `.cols3` / `.leftcol60[]` … | consecutive `::: {.col}` (add `width=` for splits) |
| `.border[<img>]`, `.circle[<img>]`, … | `::: {.border}` / `::: {.circle}` wrapping the image |
| `.panelset[.panel[.panel-name[…]]]` | `::: {.panel-tabset}` + `###` tab headings |
| `#<<` line-highlight comment | `#| code-line-numbers: "3,4"` |
| `layout: true` footer repeat | `footer:` in YAML |
| the auto title slide | hand-authored first `---` slide with `{{< meta … >}}` |
