2 Commits

Author SHA1 Message Date
Claude b6140c75ea Give gallery tiles real gutters
The grid drew tiles as cells in a 1px hairline lattice, which worked
while every tile was text. With a pictogram banner across the top of
each one, a card's footer sat directly against the next card's banner
and the rows read as a single continuous column.

Tiles now carry their own border with a real gap between them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULE5RRxdQE1ebwefEd2eCM
2026-08-06 10:57:29 +00:00
Claude 53e9803a61 Give every sin a pictogram for navigation
The gallery and the cross-links were all text, so nothing on either told
you what the chart problem actually looked like.

Shrinking the bad chart doesn't work here: in nearly every sin the tell
lives in the axis, the scale or the legend, which is exactly what a chart
loses at thumbnail size. Eight miniatures would have been eight
indistinguishable squiggles with the evidence stripped out.

So each sin gets a hand-drawn glyph of the tell instead — a cut baseline,
a down-pointing axis arrow, class breaks that moved between two ramps.
Banner across the top of the gallery tile, small glyph beside each
"Often found together" link. Drawn against currentColor plus three theme
tokens, so unlike the baked chart SVGs these follow light and dark.

Missing thumbnail throws at build, same as an unknown citation key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULE5RRxdQE1ebwefEd2eCM
2026-08-06 10:31:13 +00:00
14 changed files with 207 additions and 5 deletions
+28 -1
View File
@@ -96,7 +96,34 @@ Each sin page follows one shape: **poke → proof → why → the fix → receip
a renamed slug fails the build rather than shipping a dead link. a renamed slug fails the build rather than shipping a dead link.
- `compareNote` — overrides "Same numbers, both charts," which is wrong on a - `compareNote` — overrides "Same numbers, both charts," which is wrong on a
sin whose whole point is an omission. sin whose whole point is an omission.
4. `npm run dev` and check. The OG card for the sin is generated automatically. 4. Draw the thumbnail: `src/thumbs/<slug>.svg` (see "Thumbnails" below). The
build throws if it's missing.
5. `npm run dev` and check. The OG card for the sin is generated automatically.
## Thumbnails
Every sin has a hand-drawn pictogram at `src/thumbs/<slug>.svg`, used as a
banner across the top of its gallery tile and as a small glyph in the
"Often found together" cross-links. Lookup is `src/lib/thumbs.ts`, rendered by
`SinThumb.astro`; a missing file fails the build.
- **A symbol of the sin, not a shrunken chart.** In nearly every sin the tell
lives in the axis, the scale or the legend — exactly what a chart loses when
you shrink it to 120px. A miniature of the bad chart throws away the evidence
and leaves a gallery of indistinguishable squiggles. Draw the *tell*: a cut
baseline, a down-pointing axis arrow, class breaks that moved.
- **The glyph shows the sin, not the fix.** The tile is a rogues' gallery you
scan for the one you recognise; the correction lives on the page.
- `viewBox="0 0 72 48"`, `fill="none"`, stroke-width 2 for structure. Neutral
linework is `currentColor`; the offending element takes `var(--thumb-accent)`.
`--thumb-alt` and `--thumb-third` exist for glyphs that need to show two or
three distinct series (dual axis, mismatched category colours).
- Those three vars resolve to site tokens in `global.css`, so **unlike the
charts, thumbnails follow the light/dark theme** — they don't need the fixed
`--chart-canvas`, and they must stay legible on both `--layer-02` surfaces.
- Check the result at both sizes: the banner is ~72px tall, the cross-link
glyph ~36px. A glyph that needs more than about six strokes won't survive the
smaller one.
## Design & rendering ## Design & rendering
+2
View File
@@ -2,6 +2,7 @@
// "While you're here" — the other sins a chart like this usually commits. // "While you're here" — the other sins a chart like this usually commits.
// Charts rarely break one rule at a time, and the person you sent this to may // Charts rarely break one rule at a time, and the person you sent this to may
// need the neighbouring page more than this one. Renders nothing when empty. // need the neighbouring page more than this one. Renders nothing when empty.
import SinThumb from './SinThumb.astro';
import { href } from '../lib/url'; import { href } from '../lib/url';
import { severityUnits } from '../lib/charts'; import { severityUnits } from '../lib/charts';
@@ -19,6 +20,7 @@ const { sins } = Astro.props;
<ul class="related__list"> <ul class="related__list">
{sins.map((s) => ( {sins.map((s) => (
<li class="related__item"> <li class="related__item">
<SinThumb slug={s.id} variant="glyph" />
<a class="related__link" href={href(`sins/${s.id}/`)}> <a class="related__link" href={href(`sins/${s.id}/`)}>
<span class="related__name">{s.data.title}</span> <span class="related__name">{s.data.title}</span>
<span class="related__summary">{s.data.summary}</span> <span class="related__summary">{s.data.summary}</span>
+19
View File
@@ -0,0 +1,19 @@
---
// The pictogram that stands in for a sin in navigation — a banner across the
// top of a gallery tile, or a small glyph beside a cross-link.
//
// Always decorative: the sin's title sits right next to it and carries the
// meaning, so announcing the glyph too would just be a duplicate.
import { getThumb } from '../lib/thumbs';
interface Props {
slug: string;
/** "banner" for the gallery tile, "glyph" for the compact cross-link row. */
variant?: 'banner' | 'glyph';
}
const { slug, variant = 'banner' } = Astro.props;
const svg = getThumb(slug);
---
<span class={`thumb thumb--${variant}`} aria-hidden="true" set:html={svg} />
+37
View File
@@ -0,0 +1,37 @@
// Sin thumbnails.
//
// Each sin gets one hand-drawn pictogram at src/thumbs/<slug>.svg — a *symbol*
// of the sin, not a shrunken chart. The distinction matters: in nearly every
// sin the tell lives in the axis, the scale or the legend, which is exactly
// what a chart loses when you shrink it to thumbnail size. A miniature of the
// bad chart would throw away the evidence and leave eight indistinguishable
// squiggles.
//
// The glyphs are drawn against `currentColor` plus three theme tokens
// (--thumb-accent / --thumb-alt / --thumb-third, set in global.css), so unlike
// the build-time-baked Vega charts they follow the light/dark theme instead of
// needing the fixed --chart-canvas surface.
const modules = import.meta.glob<string>('../thumbs/*.svg', {
eager: true,
query: '?raw',
import: 'default',
});
const bySlug = new Map<string, string>();
for (const [path, svg] of Object.entries(modules)) {
const slug = path.split('/').pop()!.replace(/\.svg$/, '');
bySlug.set(slug, svg);
}
/** Inline SVG markup for a sin's thumbnail; throws at build time if missing. */
export function getThumb(slug: string): string {
const svg = bySlug.get(slug);
if (!svg) {
throw new Error(
`No thumbnail for sin "${slug}". Add src/thumbs/${slug}.svg. ` +
`Available: ${[...bySlug.keys()].join(', ')}`,
);
}
return svg;
}
+2
View File
@@ -1,6 +1,7 @@
--- ---
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
import BaseLayout from '../layouts/BaseLayout.astro'; import BaseLayout from '../layouts/BaseLayout.astro';
import SinThumb from '../components/SinThumb.astro';
import { severityUnits, tagModifier } from '../lib/charts'; import { severityUnits, tagModifier } from '../lib/charts';
import { href } from '../lib/url'; import { href } from '../lib/url';
@@ -27,6 +28,7 @@ const sins = (await getCollection('sins', ({ data }) => !data.draft)).sort((a, b
{ {
sins.map((sin) => ( sins.map((sin) => (
<a class="tile" href={href(`sins/${sin.id}/`)}> <a class="tile" href={href(`sins/${sin.id}/`)}>
<SinThumb slug={sin.id} />
<span class={`tag tag--${tagModifier(sin.data.category)}`}>{sin.data.category}</span> <span class={`tag tag--${tagModifier(sin.data.category)}`}>{sin.data.category}</span>
<h2 class="tile__title">{sin.data.title}</h2> <h2 class="tile__title">{sin.data.title}</h2>
<p class="tile__summary">{sin.data.summary}</p> <p class="tile__summary">{sin.data.summary}</p>
+47 -4
View File
@@ -334,11 +334,53 @@ code {
.gallery { .gallery {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
gap: 1px; /* tiles sit in a 1px grid of border color */ /* Real gutters, not hairlines: each tile is banded with a pictogram at the
background: var(--border-subtle); top, so a 1px rule left one card's footer touching the next card's banner
border: 1px solid var(--border-subtle); and the rows read as one continuous column. */
gap: var(--spacing-06);
margin-block: var(--spacing-09) var(--spacing-10); margin-block: var(--spacing-09) var(--spacing-10);
} }
/* ------------------------------------------------------------------ *\
Sin thumbnails — a pictogram of the sin, not a shrunken chart.
Drawn against currentColor plus three accents, so unlike the baked
chart SVGs these follow the theme instead of a fixed canvas.
\* ------------------------------------------------------------------ */
.thumb {
--thumb-accent: var(--support-error);
--thumb-alt: var(--interactive);
--thumb-third: var(--support-success);
display: flex;
align-items: center;
justify-content: center;
flex: none;
color: var(--text-secondary);
transition: color var(--dur-fast) var(--ease-productive);
}
.thumb svg {
display: block;
height: 100%;
width: auto;
}
/* banner: bleeds to the tile edges, above everything else in the tile */
.thumb--banner {
height: 7rem;
padding: var(--spacing-05) var(--spacing-06);
background: var(--layer-02);
border-bottom: 1px solid var(--border-subtle);
margin: calc(-1 * var(--spacing-06)) calc(-1 * var(--spacing-06)) var(--spacing-02);
transition:
color var(--dur-fast) var(--ease-productive),
background var(--dur-fast) var(--ease-productive);
}
.tile:hover .thumb {
color: var(--text-primary);
}
/* glyph: the compact form for cross-link rows */
.thumb--glyph {
height: 2.25rem;
width: 3.375rem;
}
.tile { .tile {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -348,7 +390,7 @@ code {
background: var(--layer-01); background: var(--layer-01);
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
border: 0; border: 1px solid var(--border-subtle);
position: relative; position: relative;
transition: background var(--dur-fast) var(--ease-productive); transition: background var(--dur-fast) var(--ease-productive);
} }
@@ -708,6 +750,7 @@ a.receipt__work {
text-decoration: none; text-decoration: none;
display: grid; display: grid;
gap: var(--spacing-02); gap: var(--spacing-02);
flex: 1;
} }
.related__name { .related__name {
font-weight: 600; font-weight: 600;
+7
View File
@@ -0,0 +1,7 @@
<!-- One series, two frames: a cliff on the left, a shrug on the right. -->
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="6" width="20" height="36" stroke="currentColor" stroke-width="2" />
<path d="M9 38l6-11 6-5 6-10" stroke="var(--thumb-accent)" stroke-width="2" />
<rect x="38" y="19" width="28" height="14" stroke="currentColor" stroke-width="2" />
<path d="M39 31l9-4 9-2 8-3" stroke="var(--thumb-accent)" stroke-width="2" />
</svg>

After

Width:  |  Height:  |  Size: 489 B

+14
View File
@@ -0,0 +1,14 @@
<!-- Bars in one order of colours, their labels in another. -->
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke-width="10">
<path d="M18 32V12" stroke="var(--thumb-alt)" />
<path d="M36 32V18" stroke="var(--thumb-third)" />
<path d="M54 32V8" stroke="var(--thumb-accent)" />
</g>
<path d="M8 33h56" stroke="currentColor" stroke-width="2" />
<g>
<rect x="14" y="38" width="8" height="8" fill="var(--thumb-third)" />
<rect x="32" y="38" width="8" height="8" fill="var(--thumb-accent)" />
<rect x="50" y="38" width="8" height="8" fill="var(--thumb-alt)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 635 B

+7
View File
@@ -0,0 +1,7 @@
<!-- Two scales, tuned until the two series trace one path. -->
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 8v32M6 14h4M6 24h4M6 34h4" stroke="var(--thumb-accent)" stroke-width="2" />
<path d="M62 8v32M62 12h4M62 22h4M62 32h4" stroke="var(--thumb-alt)" stroke-width="2" />
<path d="M15 31l12-7 12 3 18-13" stroke="var(--thumb-accent)" stroke-width="2" />
<path d="M15 37l12-7 12 3 18-13" stroke="var(--thumb-alt)" stroke-width="2" />
</svg>

After

Width:  |  Height:  |  Size: 491 B

+7
View File
@@ -0,0 +1,7 @@
<!-- Three quarters of a result, with the fourth simply absent. -->
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M36 24h17A17 17 0 1 1 36 7z" fill="currentColor" opacity=".14" />
<path d="M53 24A17 17 0 1 1 36 7" stroke="currentColor" stroke-width="2" />
<path d="M36 24v17M36 24H19" stroke="currentColor" stroke-width="2" />
<path d="M36 24h17M36 24V7" stroke="var(--thumb-accent)" stroke-width="2" />
</svg>

After

Width:  |  Height:  |  Size: 455 B

+6
View File
@@ -0,0 +1,6 @@
<!-- The axis arrow points down, so a rising series draws as a fall. -->
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 8v30" stroke="var(--thumb-accent)" stroke-width="2" />
<path d="M7 32l5 7 5-7" stroke="var(--thumb-accent)" stroke-width="2" />
<path d="M22 11l12 8 12 5 14 12" stroke="currentColor" stroke-width="2" />
</svg>

After

Width:  |  Height:  |  Size: 375 B

+16
View File
@@ -0,0 +1,16 @@
<!-- The same ramp twice, with the class breaks moved in between. -->
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor">
<rect x="10" y="8" width="13" height="11" opacity=".15" />
<rect x="23" y="8" width="13" height="11" opacity=".4" />
<rect x="36" y="8" width="13" height="11" opacity=".65" />
<rect x="49" y="8" width="13" height="11" opacity=".9" />
<rect x="10" y="29" width="9" height="11" opacity=".15" />
<rect x="19" y="29" width="17" height="11" opacity=".4" />
<rect x="36" y="29" width="17" height="11" opacity=".65" />
<rect x="53" y="29" width="9" height="11" opacity=".9" />
</g>
<path d="M10 8h52v11H10zM10 29h52v11H10z" stroke="currentColor" stroke-width="1.5" />
<path d="M23 8v11M36 8v11M49 8v11" stroke="currentColor" stroke-width="1.5" />
<path d="M19 26v17M36 26v17M53 26v17" stroke="var(--thumb-accent)" stroke-width="2" />
</svg>

After

Width:  |  Height:  |  Size: 943 B

+10
View File
@@ -0,0 +1,10 @@
<!-- A whole cut into more wedges than anyone can rank by eye. -->
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="36" cy="24" r="17" stroke="currentColor" stroke-width="2" />
<path
d="M36 24 36 7M36 24 44.5 9.3M36 24 50.7 15.5M36 24 53 24M36 24 50.7 32.5M36 24 44.5 38.7M36 24 36 41M36 24 27.5 38.7M36 24 21.3 32.5M36 24 19 24M36 24 21.3 15.5M36 24 27.5 9.3"
stroke="currentColor"
stroke-width="1.25"
opacity=".85"
/>
</svg>

After

Width:  |  Height:  |  Size: 485 B

+5
View File
@@ -0,0 +1,5 @@
<!-- Bars floating on a baseline that has been cut away. -->
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 36V14M36 36V8M54 36V20" stroke="currentColor" stroke-width="8" />
<path d="M6 39h13l3-5 4 10 3-5h37" stroke="var(--thumb-accent)" stroke-width="2" />
</svg>

After

Width:  |  Height:  |  Size: 308 B