Media
Media components handle images, figures, media lists, category pills, type badges, and book entries. These components extend the paper metaphor to photography and structured content listings.
For exact CSS values, see the implementation spec. For color foundations including the pill companion system, see color.
Images
Purpose
Images within prose content receive a warmth filter that extends the paper metaphor to photography. Everything on the page shares the same warm cast, unifying text and images into a single material.
Anatomy
| Part | Selector | Role |
|---|---|---|
| Image | .prose img | Standard content image with warmth filter |
| No-filter opt-out | img.no-filter, figure.no-filter img | Bypasses the warmth filter |
Specs
max-width: 100%
height: auto
border-radius: 10px
margin: var(--space-lg) 0
transition: filter 0.3s ease
Warmth filter by mode:
| Mode | Filter |
|---|---|
| Light | sepia(0.25) saturate(1.08) brightness(1.01) |
| Dark | sepia(0.04) saturate(1.02) brightness(0.95) |
Light mode applies a noticeable sepia warmth that harmonizes with the cream paper background. Dark mode pulls it back to near-neutral, with a slight brightness reduction that prevents images from blowing out against the dark surface.
The .no-filter class removes the filter entirely (filter: none !important). Use it for images where accurate color reproduction matters: design work, color palettes, screenshots with UI chrome.
States
| State | Filter |
|---|---|
| Default (light) | sepia(0.25) saturate(1.08) brightness(1.01) |
| Default (dark) | sepia(0.04) saturate(1.02) brightness(0.95) |
filter: none |
Responsive behavior
No responsive changes. Images scale via max-width: 100% within the content column.
Accessibility
Images should always include meaningful alt text. The warmth filter is cosmetic and does not affect content comprehension.
Code example
<!-- Standard image with warmth filter -->
<img src="/images/posts/photo.jpg" alt="Description of the image" />
<!-- Opt out for accurate color -->
<img src="/images/posts/palette.png" alt="Color palette" class="no-filter" />
Figures
Purpose
Wrapper for image + caption. The figure provides the margin; the image inside has its own margin zeroed to prevent doubling.
Anatomy
| Part | Selector | Role |
|---|---|---|
| Container | .prose figure | Wrapper, provides vertical margin |
| Image | .prose figure img | Image with margin reset |
| Caption | .prose figcaption | Descriptive text below the image |
Specs
Figure:
margin: var(--space-lg) 0
Figure image:
margin: 0
Inherits all other .prose img styles (max-width, border-radius, warmth filter).
Figcaption:
font-size: 0.85rem
color: var(--text-2)
text-align: center
margin-top: var(--space-sm)
font-style: italic
The italic style and secondary color distinguish captions from body prose. Centered alignment below the image follows the book convention.
Usage guidance
Always use <figure> when an image needs a caption. For standalone images without captions, a bare <img> is sufficient. The .no-filter class can be applied to the figure element to bypass the warmth filter: <figure class="no-filter">.
Code example
<figure>
<img src="/images/posts/photo.jpg" alt="A street in the morning light" />
<figcaption>Morning light on Rue de Rivoli, Paris.</figcaption>
</figure>
<!-- No-filter variant -->
<figure class="no-filter">
<img src="/images/posts/ui-screenshot.png" alt="Application interface" />
<figcaption>The settings panel with default theme.</figcaption>
</figure>
Media lists
Purpose
Activity log and sub-page list layout on /media. Each row shows a title, year, optional type badge, rating, and date. The same typographic hover pattern as post lists applies: weight-shift on the title, no background highlight.
Anatomy
| Part | Selector | Role |
|---|---|---|
| Container | .media-list | List wrapper |
| Item | .media-list li | Flex row with bottom border |
| Link | .media-main (as <a>) | Clickable group: title + year + badge |
| Non-link | .media-main (as <span>) | Static group when no URL exists |
| Title | .media-title | Signifier, weight-shift hover |
| Year | .media-year | Montreuil, secondary color |
| Meta right | .media-meta-right | Right-aligned group: rating + date |
| Rating | .rating | Star characters in accent color |
| Date | .date | Montreuil, tertiary/secondary color |
Specs
Container:
list-style: none
padding: 0
margin: var(--space-lg) 0 0 0
List item:
display: flex
justify-content: space-between
align-items: baseline
gap: var(--space-md)
padding: var(--space-sm) 0
border-bottom: 1px solid var(--ui)
Last child removes border-bottom.
Link (.media-main):
display: flex
align-items: baseline
gap: var(--space-xs)
min-width: 0
text-decoration: none
background-image: none
color: inherit
Title (.media-title):
font-family: var(--font-serif)
font-size: 1rem
color: var(--text)
font-variation-settings: 'wght' 400
transition: color 0.3s ease, font-variation-settings 0.4s cubic-bezier(0.22, 1, 0.36, 1)
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
max-width: 20em
Year (.media-year):
font-family: var(--font-sans)
font-size: 0.85rem
color: var(--text-2)
white-space: nowrap
flex-shrink: 0
Meta right (.media-meta-right):
display: flex
align-items: baseline
gap: var(--space-sm)
flex-shrink: 0
Rating: color: var(--accent); font-size: 0.85rem.
Date: font-family: var(--font-sans); font-size: 0.8rem; color: var(--text-2); white-space: nowrap.
States
| State | Property | Value |
|---|---|---|
| Default | .media-title color | var(--text) |
| Default | .media-title font-variation-settings | 'wght' 400 |
a.media-main:hover | .media-title color | var(--accent) |
a.media-main:hover | .media-title font-variation-settings | 'wght' 500 |
No background change on hover. No box-shadow. The hover effect is entirely typographic.
Responsive behavior
At <=600px:
- List items stack vertically:
flex-direction: column; align-items: flex-start; gap: var(--space-xs) .media-main:flex-wrap: wrap.media-title:white-space: normal; overflow: visible(no truncation).media-meta-right:width: 100%; justify-content: space-between(rating and date span the full width)
Code example
<ul class="media-list">
<li>
<a href="/media/film-slug" class="media-main">
<span class="media-title">Film Title</span>
<span class="media-year">(2025)</span>
<span class="type-badge" data-type="film">Film</span>
</a>
<span class="media-meta-right">
<span class="rating">★★★★</span>
<span class="date">Feb 21, 2026</span>
</span>
</li>
<li>
<span class="media-main">
<span class="media-title">Item Without Link</span>
<span class="media-year">(2024)</span>
</span>
<span class="media-meta-right">
<span class="rating">★★★</span>
<span class="date">Jan 5, 2026</span>
</span>
</li>
</ul>
Pill color system
Category pills and type badges need color to differentiate content types, but they cannot use --accent (that would break its singular interactive meaning). Each accent maps to a curated triple of companion colors from the Flexoki palette, assigned to --pill-1, --pill-2, and --pill-3.
Two constraints govern every triple:
- No pill color matches the current accent
- No two pills are hue-wheel neighbors
| Accent | --pill-1 | --pill-2 | --pill-3 |
|---|---|---|---|
| Red | Yellow | Cyan | Purple |
| Orange | Green | Blue | Magenta |
| Yellow | Red | Cyan | Purple |
| Green | Orange | Blue | Magenta |
| Cyan | Red | Yellow | Purple |
| Blue | Orange | Green | Magenta |
| Purple | Red | Yellow | Cyan |
| Magenta | Orange | Green | Blue |
Pill variables update in the <head> inline script (preventing flash) and again when the user changes accent via the picker. The default accent (orange) maps to Green / Blue / Magenta.
For the full color rationale and dark mode values, see color foundations.
Type badges
Purpose
Inline category markers on media list items. Used for Film, TV, and Game labels in the activity log. These are UI elements using the functional voice (Montreuil).
Anatomy
| Part | Selector | Role |
|---|---|---|
| Badge | .type-badge | Pill-shaped label |
| Film variant | .type-badge[data-type="film"] | Uses --pill-1 |
| TV variant | .type-badge[data-type="tv"] | Uses --pill-2 |
| Game variant | .type-badge[data-type="game"] | Uses --pill-3 |
Specs
font-family: var(--font-sans)
font-size: 0.6rem
font-weight: 500
text-transform: uppercase
letter-spacing: 0.06em
white-space: nowrap
flex-shrink: 0
margin-left: 0.35em
padding: 1px 6px
border-radius: 3px
position: relative
top: -2px
The top: -2px optically centers the pill with adjacent baseline text. No border. No box-shadow. The pill shape comes from padding + border-radius + a subtle background tint.
Color per type:
| Type | Text color | Background |
|---|---|---|
| Film | var(--pill-1) | color-mix(in srgb, var(--pill-1) 8%, transparent) |
| TV | var(--pill-2) | color-mix(in srgb, var(--pill-2) 8%, transparent) |
| Game | var(--pill-3) | color-mix(in srgb, var(--pill-3) 8%, transparent) |
Colors are dynamic Flexoki palette values that shift when the user changes their accent color, ensuring category pills always contrast with --accent.
Code example
<span class="type-badge" data-type="film">Film</span>
<span class="type-badge" data-type="tv">TV</span>
<span class="type-badge" data-type="game">Game</span>
Category pills
Purpose
Inline category markers used in shortlist posts to identify the category of each item. Same visual treatment as type badges but positioned differently: hanging indent on desktop, stacked above text on mobile.
Anatomy
| Part | Selector | Role |
|---|---|---|
| Pill | .shortlist-cat | Category label with hanging indent positioning |
| Parent paragraph | .prose > p:has(.shortlist-cat) | Paragraph with left padding to create the hanging indent |
Specs
font-family: var(--font-sans)
font-size: 0.65rem
font-weight: 500
text-transform: uppercase
letter-spacing: 0.06em
padding: 1px 6px
border-radius: 3px
white-space: nowrap
position: absolute
right: calc(100% - 7em)
top: 0.3em
Parent paragraph: position: relative; padding-left: 5.4em.
Colors cycle through --pill-1, --pill-2, --pill-3, assigned sequentially per unique category via inline JavaScript at page load. Text color and background (color-mix(in srgb, <pill-color> 10%, transparent)) are applied as inline styles.
Responsive behavior
At <=600px, pills stack above text:
position: relative
right: auto
top: 0
display: block
width: fit-content
margin-top: var(--space-lg)
margin-bottom: var(--space-xs)
Parent paragraph loses padding-left.
Code example
<p>
<span class="shortlist-cat" style="color: var(--pill-1); background: color-mix(in srgb, var(--pill-1) 10%, transparent);">Category</span>
Description of the shortlist item with the pill hanging in the left margin.
</p>
Book entries
Purpose
Book listings on /books use two patterns: a cover grid for currently-reading items and a standard post list for the read/want-to-read lists. The cover grid adds a visual dimension that makes sense for books (where covers are recognizable), while the text list uses the same .post-list component documented in listing patterns.
Anatomy
| Part | Selector | Role |
|---|---|---|
| Section | .book-section | Top-level grouping with var(--space-xl) margin |
| Cover grid | .book-grid | CSS grid for currently-reading cards |
| Card | .book-card | Flex column: cover + info |
| Cover image | .book-cover | 2:3 aspect ratio, border, shadow |
| Title | .book-card .book-title | Signifier, 0.95rem |
| Author | .book-card .book-author | Montreuil, 0.8rem, --text-2 |
| Meta | .book-card .book-meta | Montreuil, 0.75rem, --text-2 |
| Read list | .post-list | Standard post list pattern for read books |
| Sort controls | .sort-controls | Date/Title/Rating sort buttons |
Specs
Cover grid:
display: grid
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr))
gap: var(--space-md)
margin-top: var(--space-md)
Book card:
display: flex
flex-direction: column
gap: var(--space-sm)
text-decoration: none
background-image: none
color: inherit
transition: translate 0.2s ease
Cover image:
width: 100%
aspect-ratio: 2 / 3
object-fit: cover
border-radius: 3px
background: var(--bg-2)
border: 1px solid var(--ui)
Cover shadow by mode:
| Mode | Value |
|---|---|
| Light | 0 1px 3px hsl(40deg 10% 50% / 0.1), 0 3px 8px hsl(40deg 10% 40% / 0.08) |
| Dark | 0 1px 3px hsl(30deg 5% 10% / 0.4), 0 3px 8px hsl(30deg 5% 5% / 0.3) |
States
| State | Property | Value |
|---|---|---|
| Default | translate | 0 |
| Hover | translate | 0 -2px |
| Hover | .book-title color | var(--accent) |
| Hover | box-shadow | Deepened shadow |
Responsive behavior
At <=600px, the book grid uses a fixed 3-column layout: grid-template-columns: repeat(3, 1fr).
The read and want-to-read lists use .post-list, which inherits its responsive behavior (enlarged touch targets at <=600px).
Code example
<!-- Currently reading grid -->
<div class="book-grid">
<a href="/books/book-slug" class="book-card">
<img src="https://covers.example.com/book.jpg" alt="Book Title" class="book-cover" loading="lazy" />
<div class="book-info">
<span class="book-title">Book Title</span>
<span class="book-author">Author Name</span>
<span class="book-meta">Started Jan 15, 2026</span>
</div>
</a>
</div>
<!-- Read list (uses standard post list) -->
<ul class="post-list">
<li>
<a href="/books/book-slug">
<span class="title">Book Title</span>
<span class="date">
<span class="rating">★★★★</span>
Jan 15, 2026
</span>
</a>
</li>
</ul>Specimens
Type Badges
Category Pills
Design A curated list of tools, books, and references that have shaped how I think about making things.
Music Albums and tracks that have been on heavy rotation.
Travel Places worth going and things worth doing once you get there.
Rating
Media List Row
- Severance (2022) TV
- Sinners (2025) Film
- Hades (2020) Game