Reframe as a linkable, cited callout ("nohello for charts")

Make each sin a self-contained page you send instead of re-explaining:

- Per-sin "poke" one-liner (the shareable callout)
- Citations system: a canonical bibliography (src/lib/references.ts) of the
  field's giants — Munzner, Knaflic, Tufte, Cairo, Few, Cleveland & McGill,
  Wilke, Healy, Calling Bullshit, data-to-viz, Junk Charts, Spurious
  Correlations, FT Visual Vocabulary — cited per sin with a relevance note
- "Don't take our word for it" receipts block on each sin
- /canon page: the full annotated bibliography
- Share control: copy-link "send them the receipt" (progressive enhancement)
- Restructured sin page: poke -> proof -> why -> receipts -> share
- Header nav (Sins / Canon); homepage reframed to the "send the link" mission

Also fix the truncated-axis bad chart: clamp the [90,100] domain so the bar
baseline doesn't overflow thousands of pixels off-canvas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPEvmMbac2fCvKXpQcovj8
This commit is contained in:
Claude
2026-08-05 13:21:07 +00:00
parent a3ab9f8a79
commit e0d3f9f773
13 changed files with 578 additions and 11 deletions
+1 -1
View File
@@ -23,7 +23,7 @@
"y": { "y": {
"field": "revenue", "field": "revenue",
"type": "quantitative", "type": "quantitative",
"scale": { "domain": [90, 100] }, "scale": { "domain": [90, 100], "zero": false, "clamp": true },
"title": "Revenue ($M)" "title": "Revenue ($M)"
} }
} }
+36
View File
@@ -0,0 +1,36 @@
---
import { getReference, citeLabel } from '../lib/references';
interface Props {
citations: { key: string; note?: string }[];
}
const { citations } = Astro.props;
const resolved = citations.map((c) => ({ ...c, ref: getReference(c.key) }));
---
{
resolved.length > 0 && (
<section class="receipts" aria-labelledby="receipts-h">
<h2 id="receipts-h" class="receipts__title">Dont take our word for it</h2>
<p class="receipts__lede">The giants who say the same thing, so you dont have to argue:</p>
<ol class="receipts__list">
{resolved.map(({ ref, note }) => (
<li class="receipt">
<div class="receipt__cite">
{ref.url ? (
<a href={ref.url} rel="noopener" class="receipt__work">
{citeLabel(ref)}
</a>
) : (
<span class="receipt__work">{citeLabel(ref)}</span>
)}
<span class={`receipt__kind receipt__kind--${ref.kind}`}>{ref.kind}</span>
{ref.free && <span class="receipt__free">free online</span>}
</div>
{note && <p class="receipt__note">{note}</p>}
</li>
))}
</ol>
</section>
)
}
+43
View File
@@ -0,0 +1,43 @@
---
// The "send this to them" mechanic — the whole point of the site.
// Progressive enhancement: the URL is always visible and selectable; the
// button just makes copying a one-click affair when JS is available.
interface Props {
url: string;
}
const { url } = Astro.props;
---
<aside class="share" aria-label="Share this sin">
<p class="share__prompt">Caught someone mid-sin? Send them the receipt:</p>
<div class="share__row">
<input class="share__url" type="text" readonly value={url} aria-label="Link to this page" />
<button class="share__btn" type="button" data-url={url}>Copy link</button>
</div>
</aside>
<script>
for (const btn of document.querySelectorAll<HTMLButtonElement>('.share__btn')) {
btn.addEventListener('click', async () => {
const url = btn.dataset.url ?? '';
const done = () => {
const label = btn.textContent;
btn.textContent = 'Copied ✓';
btn.classList.add('is-copied');
setTimeout(() => {
btn.textContent = label;
btn.classList.remove('is-copied');
}, 1600);
};
try {
await navigator.clipboard.writeText(url);
done();
} catch {
const input = btn.previousElementSibling as HTMLInputElement | null;
input?.select();
document.execCommand('copy');
done();
}
});
}
</script>
+7
View File
@@ -11,6 +11,13 @@ const sins = defineCollection({
title: z.string(), title: z.string(),
// Short one-line summary shown on the index gallery. // Short one-line summary shown on the index gallery.
summary: z.string(), summary: z.string(),
// The cheeky one-line callout — the thing you send the sinner.
poke: z.string(),
// Citations into the canon (src/lib/references.ts) with a per-sin note on
// why that source is relevant. This is the "don't take our word for it".
citations: z
.array(z.object({ key: z.string(), note: z.string().optional() }))
.default([]),
// Grouping, e.g. "Misleading Scales", "Chart-Type Abuse". // Grouping, e.g. "Misleading Scales", "Chart-Type Abuse".
category: z.string(), category: z.string(),
// 1 (venial) … 5 (mortal). Drives the flame rating in the UI. // 1 (venial) … 5 (mortal). Drives the flame rating in the UI.
+10
View File
@@ -1,12 +1,22 @@
--- ---
title: "The Dual-Axis Deception" title: "The Dual-Axis Deception"
summary: "Two unrelated series on two independent axes, engineered to look correlated." summary: "Two unrelated series on two independent axes, engineered to look correlated."
poke: "Two y-axes, tuned until the lines kiss. Congratulations — you can now make anything 'cause' anything."
category: "False Relationships" category: "False Relationships"
severity: 5 severity: 5
tags: ["dual axis", "correlation", "line chart"] tags: ["dual axis", "correlation", "line chart"]
badChart: "dual-axis-bad" badChart: "dual-axis-bad"
fixedChart: "dual-axis-fixed" fixedChart: "dual-axis-fixed"
date: 2026-08-05 date: 2026-08-05
citations:
- key: datatoviz
note: "Lists “using two Y axes” as a top caveat — a way to manipulate the story."
- key: spurious
note: "Thousands of nonsense correlations: proof that “they move together” proves nothing."
- key: howchartslie
note: "Cairo on how aligned axes manufacture relationships that arent there."
- key: swd
note: "Knaflics standing advice: avoid the second axis; separate or index instead."
--- ---
## The sin ## The sin
+10
View File
@@ -1,12 +1,22 @@
--- ---
title: "Pie Chart Overload" title: "Pie Chart Overload"
summary: "Eight near-identical slices no human eye can rank. A bar chart was right there." summary: "Eight near-identical slices no human eye can rank. A bar chart was right there."
poke: "Eight slices, three of them the same size. Nobody can read this. A bar chart could."
category: "Chart-Type Abuse" category: "Chart-Type Abuse"
severity: 3 severity: 3
tags: ["pie chart", "part-to-whole", "comparison"] tags: ["pie chart", "part-to-whole", "comparison"]
badChart: "pie-overload-bad" badChart: "pie-overload-bad"
fixedChart: "pie-overload-fixed" fixedChart: "pie-overload-fixed"
date: 2026-08-05 date: 2026-08-05
citations:
- key: fewpies
note: "“Save the Pies for Dessert” — the definitive, free takedown of the overloaded pie."
- key: clevelandmcgill
note: "Their ranking shows angle and area are judged far worse than position along a scale."
- key: junkcharts
note: "Kaiser Fungs running #onelesspie campaign exists for precisely this chart."
- key: datatoviz
note: "Pie charts are the first entry in its catalogue of caveats."
--- ---
## The sin ## The sin
+10
View File
@@ -1,12 +1,22 @@
--- ---
title: "The Truncated Y-Axis" title: "The Truncated Y-Axis"
summary: "Chopping the baseline off a bar chart to make small differences look enormous." summary: "Chopping the baseline off a bar chart to make small differences look enormous."
poke: "You started the y-axis at 90. That's not a bar chart — it's a magic trick."
category: "Misleading Scales" category: "Misleading Scales"
severity: 4 severity: 4
tags: ["bar chart", "axis", "exaggeration"] tags: ["bar chart", "axis", "exaggeration"]
badChart: "truncated-axis-bad" badChart: "truncated-axis-bad"
fixedChart: "truncated-axis-fixed" fixedChart: "truncated-axis-fixed"
date: 2026-08-05 date: 2026-08-05
citations:
- key: callingbullshit
note: "Their “Misleading Axes” module is the canonical explainer for exactly this move."
- key: clevelandmcgill
note: "The perception research: readers compare bar length, so length must map to value from zero."
- key: tufte
note: "The “Lie Factor” puts an actual number on how much the graphic exaggerates."
- key: howchartslie
note: "Cairo devotes a chapter to scale manipulation like this one."
--- ---
## The sin ## The sin
+4 -1
View File
@@ -32,7 +32,10 @@ const { title, description = 'A catalogue of data-visualization sins, styled on
<a class="cds-header__name" href={href()}> <a class="cds-header__name" href={href()}>
Chart&nbsp;Sins <span class="mono">/ honest data-viz</span> Chart&nbsp;Sins <span class="mono">/ honest data-viz</span>
</a> </a>
<span class="cds-header__tag">built on Carbon</span> <nav class="cds-header__nav" aria-label="Primary">
<a href={href()}>Sins</a>
<a href={href('canon/')}>Canon</a>
</nav>
</div> </div>
</header> </header>
+183
View File
@@ -0,0 +1,183 @@
// The Canon — the shoulders of giants Chart Sins stands on.
//
// Each sin cites into this list by key, so a page can say "don't take our word
// for it" and point at the authority that actually condemns the practice.
// Add a reference once here; cite it from any number of sins.
export type RefKind = 'book' | 'site' | 'paper';
export interface Reference {
authors: string;
title: string;
/** Publisher / journal / site, for books and papers. */
work?: string;
year?: number;
url?: string;
kind: RefKind;
/** One line on why it belongs in the canon. */
blurb: string;
/** True for the free-to-read-online works, surfaced as a hint. */
free?: boolean;
}
export const REFERENCES = {
// --- books ---
munzner: {
authors: 'Tamara Munzner',
title: 'Visualization Analysis & Design',
work: 'CRC Press',
year: 2014,
url: 'https://www.cs.ubc.ca/~tmm/vadbook/',
kind: 'book',
blurb: 'The rigorous framework: what channels the eye reads accurately, and why position beats angle, length, and area.',
},
swd: {
authors: 'Cole Nussbaumer Knaflic',
title: 'Storytelling with Data',
work: 'Wiley',
year: 2015,
url: 'https://www.storytellingwithdata.com/books',
kind: 'book',
blurb: 'The practitioners bible on decluttering charts and directing attention honestly.',
},
tufte: {
authors: 'Edward R. Tufte',
title: 'The Visual Display of Quantitative Information',
work: 'Graphics Press (2nd ed.)',
year: 2001,
url: 'https://www.edwardtufte.com/book/the-visual-display-of-quantitative-information/',
kind: 'book',
blurb: 'Origin of the “Lie Factor,” chartjunk, and the data-ink ratio.',
},
howchartslie: {
authors: 'Alberto Cairo',
title: 'How Charts Lie',
work: 'W. W. Norton',
year: 2019,
url: 'http://www.thefunctionalart.com/',
kind: 'book',
blurb: 'A whole book on exactly this: the tricks charts use to mislead, and how to read past them.',
},
fewpies: {
authors: 'Stephen Few',
title: 'Save the Pies for Dessert',
work: 'Perceptual Edge',
year: 2007,
url: 'https://www.perceptualedge.com/articles/visual_business_intelligence/save_the_pies_for_dessert.pdf',
kind: 'paper',
blurb: 'The definitive takedown of the pie chart, from the fields most stubborn advocate for clarity.',
free: true,
},
wilke: {
authors: 'Claus O. Wilke',
title: 'Fundamentals of Data Visualization',
work: 'OReilly',
year: 2019,
url: 'https://clauswilke.com/dataviz/',
kind: 'book',
blurb: 'Free, thorough, and organized by problem — a per-topic reference for doing it right.',
free: true,
},
healy: {
authors: 'Kieran Healy',
title: 'Data Visualization: A Practical Introduction',
work: 'Princeton University Press',
year: 2018,
url: 'https://socviz.co/',
kind: 'book',
blurb: 'Free online; strong on why perception, not taste, decides what works.',
free: true,
},
callingbullshit: {
authors: 'Carl T. Bergstrom & Jevin D. West',
title: 'Calling Bullshit',
work: 'Random House',
year: 2020,
url: 'https://www.callingbullshit.org/tools/tools_misleading_axes.html',
kind: 'book',
blurb: 'Book and free course; the “Misleading Axes” module is required reading for axis crimes.',
free: true,
},
huff: {
authors: 'Darrell Huff',
title: 'How to Lie with Statistics',
work: 'W. W. Norton',
year: 1954,
kind: 'book',
blurb: 'The 1954 classic that named the game; the truncated axis is right there in it.',
},
clevelandmcgill: {
authors: 'William S. Cleveland & Robert McGill',
title: 'Graphical Perception: Theory, Experimentation, and Application',
work: 'Journal of the American Statistical Association',
year: 1984,
url: 'https://www.jstor.org/stable/2288400',
kind: 'paper',
blurb: 'The experiments proving people judge position accurately and angle/area poorly — the root of most of these sins.',
},
schwabish: {
authors: 'Jonathan Schwabish',
title: 'Better Data Visualizations',
work: 'Columbia University Press',
year: 2021,
url: 'https://policyviz.com/books/',
kind: 'book',
blurb: 'A modern, chart-by-chart guide to which form fits which job.',
},
// --- sites ---
datatoviz: {
authors: 'Yan Holtz & Conor Healy',
title: 'From Data to Viz — Caveats',
work: 'data-to-viz.com',
url: 'https://www.data-to-viz.com/caveats.html',
kind: 'site',
blurb: 'A browsable catalogue of common dataviz mistakes, each with a fix.',
free: true,
},
junkcharts: {
authors: 'Kaiser Fung',
title: 'Junk Charts',
work: 'junkcharts.com',
url: 'https://junkcharts.typepad.com/',
kind: 'site',
blurb: 'Fifteen years of chart critique-and-redesign, plus the #onelesspie campaign.',
free: true,
},
spurious: {
authors: 'Tyler Vigen',
title: 'Spurious Correlations',
work: 'tylervigen.com',
url: 'https://www.tylervigen.com/spurious-correlations',
kind: 'site',
blurb: 'Thousands of nonsense correlations — the canonical proof that “they move together” proves nothing.',
free: true,
},
ftvisvocab: {
authors: 'Financial Times Visual Journalism',
title: 'Visual Vocabulary',
work: 'FT',
url: 'https://github.com/Financial-Times/chart-doctor/tree/main/visual-vocabulary',
kind: 'site',
blurb: 'A cheat sheet mapping what you want to say to the chart that says it.',
free: true,
},
} as const satisfies Record<string, Reference>;
export type ReferenceKey = keyof typeof REFERENCES;
/** Resolve a citation key; throws at build time if it isn't in the canon. */
export function getReference(key: string): Reference {
const ref = (REFERENCES as Record<string, Reference>)[key];
if (!ref) {
throw new Error(
`Reference "${key}" is not in the canon (src/lib/references.ts). Known: ${Object.keys(REFERENCES).join(', ')}`,
);
}
return ref;
}
/** A short "Author, Title (Year)" label. */
export function citeLabel(ref: Reference): string {
return `${ref.authors}, ${ref.title}${ref.year ? ` (${ref.year})` : ''}`;
}
+51
View File
@@ -0,0 +1,51 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { REFERENCES, citeLabel, type RefKind } from '../lib/references';
const groups: { kind: RefKind; heading: string; blurb: string }[] = [
{ kind: 'book', heading: 'The books', blurb: 'The shelf every chart argument can lean on.' },
{ kind: 'paper', heading: 'The papers & essays', blurb: 'The primary sources — where the evidence actually lives.' },
{ kind: 'site', heading: 'The sites', blurb: 'Living references you can link straight into a thread.' },
];
const byKind = (kind: RefKind) => Object.values(REFERENCES).filter((r) => r.kind === kind);
---
<BaseLayout title="The Canon — Chart Sins" description="The books, papers, and sites Chart Sins stands on.">
<section class="wrap hero">
<p class="hero__eyebrow">// the shoulders we stand on</p>
<h1 class="expressive-display">The Canon</h1>
<p class="hero__lede">
Every sin here cites its sources. This is the whole shelf in one place —
the giants of data visualization who already made the case, so you can
link the receipt instead of relitigating it.
</p>
</section>
<div class="wrap canon">
{
groups.map((g) => (
<section class="canon__group">
<h2 class="canon__heading productive-heading-03">{g.heading}</h2>
<p class="helper">{g.blurb}</p>
<ul class="canon__list">
{byKind(g.kind).map((ref) => (
<li class="canon__item">
<div class="canon__cite">
{ref.url ? (
<a href={ref.url} rel="noopener" class="canon__work">{citeLabel(ref)}</a>
) : (
<span class="canon__work">{citeLabel(ref)}</span>
)}
{ref.work && <span class="canon__pub">{ref.work}</span>}
{ref.free && <span class="receipt__free">free online</span>}
</div>
<p class="canon__blurb">{ref.blurb}</p>
</li>
))}
</ul>
</section>
))
}
</div>
</BaseLayout>
+6 -4
View File
@@ -13,11 +13,13 @@ const sins = (await getCollection('sins', ({ data }) => !data.draft)).sort((a, b
<BaseLayout title="Chart Sins"> <BaseLayout title="Chart Sins">
<section class="wrap hero"> <section class="wrap hero">
<p class="hero__eyebrow">// a catalogue of data-visualization sins</p> <p class="hero__eyebrow">// stop re-explaining the same chart mistake</p>
<h1 class="expressive-display">Charts that lie,<br />and the honest fix.</h1> <h1 class="expressive-display">Someone made a<br />misleading chart again.</h1>
<p class="hero__lede"> <p class="hero__lede">
Each entry is a data-visualization mistake in the wild: the chart that Dont type the whole explanation for the hundredth time — send the link.
misleads, why it fools the eye, and the honest version that repents. Each sin shows the deceptive chart beside its honest twin, names the tell,
and cites the giants who back you up. Its <a href="https://nohello.net" rel="noopener">nohello.net</a>,
for bad charts.
</p> </p>
</section> </section>
+16 -1
View File
@@ -2,6 +2,8 @@
import { getCollection, render } from 'astro:content'; import { getCollection, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import VegaChart from '../../components/VegaChart.astro'; import VegaChart from '../../components/VegaChart.astro';
import Citations from '../../components/Citations.astro';
import ShareLink from '../../components/ShareLink.astro';
import { getChartSpec, severityUnits, tagModifier } from '../../lib/charts'; import { getChartSpec, severityUnits, tagModifier } from '../../lib/charts';
import { href } from '../../lib/url'; import { href } from '../../lib/url';
@@ -18,9 +20,12 @@ const { Content } = await render(sin);
const badSpec = getChartSpec(sin.data.badChart); const badSpec = getChartSpec(sin.data.badChart);
const fixedSpec = getChartSpec(sin.data.fixedChart); const fixedSpec = getChartSpec(sin.data.fixedChart);
// Canonical, shareable URL for this sin — the thing you paste into a thread.
const pageUrl = new URL(href(`sins/${sin.id}/`), Astro.site).toString();
--- ---
<BaseLayout title={`${sin.data.title} — Chart Sins`} description={sin.data.summary}> <BaseLayout title={`${sin.data.title} — Chart Sins`} description={sin.data.poke}>
<article class="wrap sin"> <article class="wrap sin">
<nav class="breadcrumb"><a href={href()}>All sins</a> / {sin.data.title}</nav> <nav class="breadcrumb"><a href={href()}>All sins</a> / {sin.data.title}</nav>
@@ -37,6 +42,7 @@ const fixedSpec = getChartSpec(sin.data.fixedChart);
</span> </span>
</div> </div>
<h1 class="sin__title expressive-heading">{sin.data.title}</h1> <h1 class="sin__title expressive-heading">{sin.data.title}</h1>
<p class="sin__poke">{sin.data.poke}</p>
<div class="sin__tags"> <div class="sin__tags">
{sin.data.tags.map((t) => <span class="tag">{t}</span>)} {sin.data.tags.map((t) => <span class="tag">{t}</span>)}
</div> </div>
@@ -52,9 +58,18 @@ const fixedSpec = getChartSpec(sin.data.fixedChart);
<VegaChart spec={fixedSpec} title={`Honest chart: ${sin.data.title}`} /> <VegaChart spec={fixedSpec} title={`Honest chart: ${sin.data.title}`} />
</div> </div>
</div> </div>
<p class="compare__note">Same numbers, both charts. Only the design changed.</p>
<div class="prose"> <div class="prose">
<Content /> <Content />
</div> </div>
<Citations citations={sin.data.citations} />
<ShareLink url={pageUrl} />
<p class="more-canon">
Building a case? See the <a href={href('canon/')}>full canon →</a>
</p>
</article> </article>
</BaseLayout> </BaseLayout>
+201 -4
View File
@@ -235,10 +235,20 @@ code {
font-family: var(--font-mono); font-family: var(--font-mono);
letter-spacing: 0; letter-spacing: 0;
} }
@media (max-width: 40rem) { .cds-header__nav {
.cds-header__tag { margin-left: auto;
display: none; display: flex;
} gap: var(--spacing-06);
}
.cds-header__nav a {
color: var(--shell-text-secondary);
font-size: 0.875rem;
text-decoration: none;
border: 0;
padding-block: var(--spacing-02);
}
.cds-header__nav a:hover {
color: var(--shell-text);
} }
/* ------------------------------------------------------------------ *\ /* ------------------------------------------------------------------ *\
@@ -527,6 +537,193 @@ code {
color: var(--text-primary); color: var(--text-primary);
} }
/* the poke — the shareable one-liner under the title */
.sin__poke {
font-size: 1.375rem;
line-height: 1.35;
font-weight: 300;
color: var(--text-primary);
max-width: 42rem;
margin: var(--spacing-05) 0 0;
}
.compare__note {
margin: calc(-1 * var(--spacing-05)) 0 0;
font-family: var(--font-mono);
font-size: 0.8125rem;
color: var(--text-helper);
}
/* the receipts — citations block */
.receipts {
margin-top: var(--spacing-09);
padding-top: var(--spacing-07);
border-top: 1px solid var(--border-subtle);
}
.receipts__title {
font-size: 1.5rem;
font-weight: 400;
margin: 0;
letter-spacing: -0.01em;
}
.receipts__lede {
color: var(--text-secondary);
margin: var(--spacing-03) 0 var(--spacing-06);
}
.receipts__list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 1px;
background: var(--border-subtle);
border: 1px solid var(--border-subtle);
}
.receipt {
background: var(--layer-01);
padding: var(--spacing-05) var(--spacing-06);
}
.receipt__cite {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: var(--spacing-03);
}
.receipt__work {
font-weight: 600;
color: var(--text-primary);
}
a.receipt__work {
color: var(--link-primary);
}
.receipt__kind {
font-family: var(--font-mono);
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-helper);
border: 1px solid var(--border-subtle);
padding: 0 var(--spacing-02);
}
.receipt__free {
font-family: var(--font-mono);
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--support-success);
}
.receipt__note {
margin: var(--spacing-03) 0 0;
color: var(--text-secondary);
font-size: 0.9375rem;
line-height: 1.5;
}
/* the share box */
.share {
margin-top: var(--spacing-08);
padding: var(--spacing-06);
background: var(--layer-01);
border: 1px solid var(--border-subtle);
border-left: 3px solid var(--interactive);
}
.share__prompt {
margin: 0 0 var(--spacing-04);
font-weight: 600;
}
.share__row {
display: flex;
gap: 0;
max-width: 34rem;
}
.share__url {
flex: 1;
min-width: 0;
font-family: var(--font-mono);
font-size: 0.8125rem;
padding: var(--spacing-04) var(--spacing-05);
background: var(--background);
color: var(--text-secondary);
border: 1px solid var(--border-strong);
border-right: 0;
}
.share__url:focus-visible {
outline: 2px solid var(--focus);
outline-offset: -2px;
}
.share__btn {
flex: 0 0 auto;
font-family: var(--font-sans);
font-size: 0.875rem;
font-weight: 600;
color: var(--text-on-color);
background: var(--interactive);
border: 1px solid var(--interactive);
padding-inline: var(--spacing-05);
cursor: pointer;
transition: background var(--dur-fast) var(--ease-productive);
}
.share__btn:hover {
background: var(--link-hover);
}
.share__btn.is-copied {
background: var(--support-success);
border-color: var(--support-success);
}
.more-canon {
margin-top: var(--spacing-07);
font-family: var(--font-mono);
font-size: 0.875rem;
}
/* canon page */
.canon {
padding-block: var(--spacing-08) var(--spacing-10);
}
.canon__group {
margin-bottom: var(--spacing-09);
}
.canon__heading {
margin: 0;
}
.canon__list {
list-style: none;
margin: var(--spacing-05) 0 0;
padding: 0;
display: grid;
gap: 1px;
background: var(--border-subtle);
border: 1px solid var(--border-subtle);
}
.canon__item {
background: var(--layer-01);
padding: var(--spacing-05) var(--spacing-06);
}
.canon__cite {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: var(--spacing-03);
}
.canon__work {
font-weight: 600;
font-size: 1.0625rem;
color: var(--text-primary);
}
a.canon__work {
color: var(--link-primary);
}
.canon__pub {
font-size: 0.8125rem;
color: var(--text-helper);
}
.canon__blurb {
margin: var(--spacing-03) 0 0;
color: var(--text-secondary);
max-width: 44rem;
line-height: 1.5;
}
/* ------------------------------------------------------------------ *\ /* ------------------------------------------------------------------ *\
Footer + colophon Footer + colophon
\* ------------------------------------------------------------------ */ \* ------------------------------------------------------------------ */