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

PartSelectorRole
Image.prose imgStandard content image with warmth filter
No-filter opt-outimg.no-filter, figure.no-filter imgBypasses 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:

ModeFilter
Lightsepia(0.25) saturate(1.08) brightness(1.01)
Darksepia(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

StateFilter
Default (light)sepia(0.25) saturate(1.08) brightness(1.01)
Default (dark)sepia(0.04) saturate(1.02) brightness(0.95)
Printfilter: 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

PartSelectorRole
Container.prose figureWrapper, provides vertical margin
Image.prose figure imgImage with margin reset
Caption.prose figcaptionDescriptive 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

PartSelectorRole
Container.media-listList wrapper
Item.media-list liFlex 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-titleSignifier, weight-shift hover
Year.media-yearMontreuil, secondary color
Meta right.media-meta-rightRight-aligned group: rating + date
Rating.ratingStar characters in accent color
Date.dateMontreuil, 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

StatePropertyValue
Default.media-title colorvar(--text)
Default.media-title font-variation-settings'wght' 400
a.media-main:hover.media-title colorvar(--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:

  1. No pill color matches the current accent
  2. No two pills are hue-wheel neighbors
Accent--pill-1--pill-2--pill-3
RedYellowCyanPurple
OrangeGreenBlueMagenta
YellowRedCyanPurple
GreenOrangeBlueMagenta
CyanRedYellowPurple
BlueOrangeGreenMagenta
PurpleRedYellowCyan
MagentaOrangeGreenBlue

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

PartSelectorRole
Badge.type-badgePill-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:

TypeText colorBackground
Filmvar(--pill-1)color-mix(in srgb, var(--pill-1) 8%, transparent)
TVvar(--pill-2)color-mix(in srgb, var(--pill-2) 8%, transparent)
Gamevar(--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

PartSelectorRole
Pill.shortlist-catCategory 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

PartSelectorRole
Section.book-sectionTop-level grouping with var(--space-xl) margin
Cover grid.book-gridCSS grid for currently-reading cards
Card.book-cardFlex column: cover + info
Cover image.book-cover2:3 aspect ratio, border, shadow
Title.book-card .book-titleSignifier, 0.95rem
Author.book-card .book-authorMontreuil, 0.8rem, --text-2
Meta.book-card .book-metaMontreuil, 0.75rem, --text-2
Read list.post-listStandard post list pattern for read books
Sort controls.sort-controlsDate/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:

ModeValue
Light0 1px 3px hsl(40deg 10% 50% / 0.1), 0 3px 8px hsl(40deg 10% 40% / 0.08)
Dark0 1px 3px hsl(30deg 5% 10% / 0.4), 0 3px 8px hsl(30deg 5% 5% / 0.3)

States

StatePropertyValue
Defaulttranslate0
Hovertranslate0 -2px
Hover.book-title colorvar(--accent)
Hoverbox-shadowDeepened 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

Film TV Game

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 ★★★★★ Feb 21, 2026
  • Sinners (2025) Film ★★★★☆ Feb 14, 2026
  • Hades (2020) Game ★★★★★ Jan 30, 2026