Component reference · Editorial style guide

Component reference

A living reference of every visual pattern on this site. All examples below are rendered live — if something looks wrong here, it looks wrong everywhere.

Colors#

Design tokens defined as CSS custom properties on the body. The site uses a warm paper background (#fbf6ef) with dark text (#222) and blue accents.

Page background
#fbf6ef
Body text
#222
Grey darkest
--kh-color--grey--darkest
Grey
--kh-color--grey
Blue (links)
--kh-color--blue
Blue dark
--kh-color--blue--dark
Visited links
#734595
Orange (accent)
--kh-color--orange
Yellow dark
#ffb81c
Surface (boxes, tables)
#fafafa
Table header
#d0d0ce
Code/quote bg
#fff

Typography#

All type is set in Recursive, a variable font with axes for weight, monospace, casual, slant, and cursive. Base size is 16px with 1.7 line height.

Heading classes#


<h1 class="kh-text-heading--1">Heading 1 — 42px / 700 weight</h1>
<h2 class="kh-text-heading--2">Heading 2 — 30px / 500 weight</h2>
<h3 class="kh-text-heading--3">Heading 3 — 27px / 500 weight</h3>

Heading 1 — 42px / 700 weight

Heading 2 — 30px / 500 weight

Heading 3 — 27px / 500 weight

Content headings (unstyled h-tags inside .kh-content)#

Inside .kh-content, bare heading tags inherit proportional sizing:


<div class="kh-content">
  <h1>Content H1 — 42px</h1>
  <h2>Content H2 — 30px</h2>
  <h3>Content H3 — 27px</h3>
  <h4>Content H4 — 21px</h4>
  <h5>Content H5 — 19px</h5>
</div>

Content H1 — 42px

Content H2 — 30px

Content H3 — 27px

Content H4 — 21px

Content H5 — 19px

Body text classes#


<p class="kh-text-body--1">Body 1 — 32px, weight 500. Used for post teasers and lead text.</p>
<p class="kh-text-body--3">Body 3 — 16px, weight 500. Used for metadata and supporting text in dark grey (#373a36).</p>

Body 1 — 32px, weight 500. Used for post teasers and lead text.

Body 3 — 16px, weight 500. Used for metadata and supporting text in dark grey (#373a36).

Inline text styles#


<p>Regular paragraph text at 16px with 1.7 line height.</p>
<p><em>Italic text uses font-variation-settings for a slanted casual style.</em></p>
<p><strong>Bold text for emphasis.</strong></p>
<p><small>Small text — 13px, used for dates and fine print.</small></p>
<p>Text with <code>inline code</code> rendered in monospace Recursive.</p>
<p>An <abbr title="Abbreviation with tooltip">ABBR</abbr> gets a dotted underline and help cursor.</p>

Regular paragraph text at 16px with 1.7 line height.

Italic text uses font-variation-settings for a slanted casual style.

Bold text for emphasis.

Small text — 13px, used for dates and fine print.

Text with inline code rendered in monospace Recursive.

An ABBR gets a dotted underline and help cursor.

Buttons#

The button has a distinctive 3D shadow effect that animates on hover (shifts 4px) and active (shifts 8px, shadow collapses).


<a href="#buttons" class="kh-button">Default button</a>
<a href="#buttons" class="kh-button kh-button--sm">Small button</a>

The shadow uses --kh-button-shadow-background-color (defaults to blue-dark). Border color is --kh-button-border-color (defaults to blue). These can be overridden via CSS custom properties.

Badges and labels#


<span class="kh-badge">Badge</span>
<span class="kh-badge">UPPERCASE</span>
Badge UPPERCASE

Labels#

Pill-shaped categorization chips used for topics and tags:


<span class="kh-label">web development</span>
<span class="kh-label">design systems</span>
<span class="kh-label">Eleventy</span>
<span class="kh-label">performance</span>
web development design systems Eleventy performance

Note boxes#

Orange-bordered callout boxes for asides, updates, and contextual information. The text uses a casual variant of Recursive with a slight slant.


<aside class="kh-note-box">
  <span class="kh-note-box__label">Update</span>
  This pattern was revised in September 2025 to use the casual axis of Recursive for a more handwritten feel.
</aside>

<aside class="kh-note-box">
  <span class="kh-note-box__label">Context</span>
  The label uses a bold monospace casual variant. Keep labels short: "Update", "Sidebar", "Related", "Context".
</aside>

Content boxes#

General-purpose container with a bottom border accent. Themeable via --kh-box-theme-color--background and --kh-box-theme-color--foreground.


<div class="kh-box">
  <p>A content box with default styling — light background, 4px solid bottom border, and 1rem padding.</p>
  <p>Links inside <code>.kh-box__text</code> inherit the current color.</p>
</div>

A content box with default styling — light background, 4px solid bottom border, and 1rem padding.

Links inside .kh-box__text inherit the current color.

Blockquotes#

Standard blockquote#

Inside .kh-content, bare blockquotes get a white background, subtle shadow, rounded corners, and a left border accent:


<div class="kh-content">
  <blockquote>
    <p>The best way to predict the future is to invent it. Well-structured content is the foundation of every good digital experience.</p>
  </blockquote>
</div>

The best way to predict the future is to invent it. Well-structured content is the foundation of every good digital experience.

A styled blockquote with a yellow left border, opening quote mark, and a hand-drawn highlighter effect behind the text:


<blockquote class="kh-quote--featured">
  <p>The work you're most proud of is rarely the flashiest — it's the system that quietly keeps working long after you've moved on.</p>
</blockquote>

Code#

Inline code#

Inline code uses monospace Recursive with a white background and slight padding.

Code blocks#

Fenced code blocks inside .kh-content get a white background, rounded corners, and a subtle shadow:

.kh-button {
  appearance: none;
  background-color: #fafafa;
  border: 4px solid var(--kh-color--blue);
  border-radius: 4px;
  box-shadow: 8px 8px 0 var(--kh-color--blue--dark);
  transition: all linear 125ms;
}
<aside class="kh-note-box">
  <span class="kh-note-box__label">Note</span>
  Content goes here.
</aside>

Code blocks use pre-wrap for wrapping and overflow: auto for horizontal scroll when needed.

Figures and images#

Styled figure#


<figure class="kh-figure">
  <img class="kh-figure__image" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='300' fill='%23d0d0ce'%3E%3Crect width='600' height='300'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%23707372' font-family='system-ui' font-size='18'%3E600 × 300 placeholder%3C/text%3E%3C/svg%3E" alt="Placeholder image demonstrating the figure component">
  <figcaption class="kh-figure__caption">Caption text — used for attribution and context. Grey (#373a36), weight 500.</figcaption>
</figure>
Placeholder image demonstrating the figure component
Caption text — used for attribution and context. Grey (#373a36), weight 500.

Unstyled figure (inside .kh-content)#

Bare figure tags inside .kh-content use display: table with bottom-aligned captions:


<div class="kh-content">
  <figure>
    <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='200' fill='%23d0d0ce'%3E%3Crect width='400' height='200'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%23707372' font-family='system-ui' font-size='16'%3E400 × 200%3C/text%3E%3C/svg%3E" alt="Placeholder image">
    <figcaption>A plain figure caption in grey (#54585a).</figcaption>
  </figure>
</div>
Placeholder image
A plain figure caption in grey (#54585a).

Bleed-out image#

The .kh-bleed-out utility makes images break out of their container on larger screens (30% on medium, 50% on large):


<img class="kh-bleed-out" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='900' height='300' fill='%23d0d0ce'%3E%3Crect width='900' height='300'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%23707372' font-family='system-ui' font-size='18'%3EBleed-out image (wider on large screens)%3C/text%3E%3C/svg%3E" alt="Demonstration of bleed-out image effect">
Demonstration of bleed-out image effect

Tables#

Inside .kh-content, bare tables get alternating row striping with a grey header row:


<div class="kh-content">
  <table>
    <thead>
      <tr>
        <th scope="col">Property</th>
        <th scope="col">Value</th>
        <th scope="col">Notes</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Background</td>
        <td><code>#fafafa</code></td>
        <td>Light surface color</td>
      </tr>
      <tr>
        <td>Header bg</td>
        <td><code>#d0d0ce</code></td>
        <td>Warm grey</td>
      </tr>
      <tr>
        <td>Even rows</td>
        <td><code>#d8d8d8</code></td>
        <td>Alternating stripe</td>
      </tr>
      <tr>
        <td>Cell padding</td>
        <td><code>8px 16px</code></td>
        <td>Comfortable reading</td>
      </tr>
    </tbody>
  </table>
</div>
Property Value Notes
Background #fafafa Light surface color
Header bg #d0d0ce Warm grey
Even rows #d8d8d8 Alternating stripe
Cell padding 8px 16px Comfortable reading

Forms#

Form elements used for the site search. The search input has a custom SVG cancel button on WebKit browsers.


<div class="kh-form__item">
  <label class="kh-form__label" for="demo-search">Search</label>
  <input type="search" id="demo-search" placeholder="Search my site" class="kh-form__input">
</div>
<a href="#forms" class="kh-button">Search</a>
Search

Summary cards#

Used on the blog index and work pages to display post previews.

Standard summary#


<article class="kh-summary">
  <div class="kh-summary__date">7 Feb 2026</div>
  <h3 class="kh-summary__title">
    <a href="#summary-cards" class="kh-summary__link">Example post title with balanced text wrapping</a>
  </h3>
  <p class="kh-summary__text">A one-sentence teaser that summarizes the post content and entices the reader to click through.</p>
</article>

News summary#

A more compact variant with date on its own row and two-column layout on wider screens:


<article class="kh-summary kh-summary--news">
  <div class="kh-summary__date">7 Feb 2026</div>
  <h3 class="kh-summary__title">
    <a href="#summary-cards" class="kh-summary__link">News-style summary with tighter spacing</a>
  </h3>
  <p class="kh-summary__text">Used on the blog index. Grid layout stacks on mobile, goes side-by-side on screens wider than 600px.</p>
</article>

Summary with image#

Summaries can include a thumbnail image that snaps to the right column on medium+ screens:


<article class="kh-summary kh-summary--news">
  <div class="kh-summary__date">7 Feb 2026<span class="kh-summary__author">Filed in: design systems, performance</span></div>
  <h3 class="kh-summary__title">
    <a href="#summary-cards" class="kh-summary__link">Post with a thumbnail image</a>
  </h3>
  <a href="#summary-cards" class="kh-summary__image">
    <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='147' fill='%23d0d0ce'%3E%3Crect width='220' height='147'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%23707372' font-family='system-ui' font-size='14'%3E220×147%3C/text%3E%3C/svg%3E" alt="Example thumbnail">
  </a>
  <p class="kh-summary__text">The image uses a 3:2 aspect ratio with object-fit cover. Constrained to 220px wide.</p>
</article>
7 Feb 2026Filed in: design systems, performance

Post with a thumbnail image

Example thumbnail

The image uses a 3:2 aspect ratio with object-fit cover. Constrained to 220px wide.

Metrics#

Used on the CV page to display key impact numbers. Cards have a subtle hover lift effect.


<div class="kh-metrics-grid">
  <div class="kh-metric">
    <span class="kh-metric__number">20+</span>
    <span class="kh-metric__label">YEARS EXPERIENCE</span>
  </div>
  <div class="kh-metric">
    <span class="kh-metric__number">0.45s</span>
    <span class="kh-metric__label">PAGE LOAD TIME</span>
  </div>
  <div class="kh-metric">
    <span class="kh-metric__number">3 weeks</span>
    <span class="kh-metric__label">EDITORIAL TIME SAVED / MONTH</span>
  </div>
</div>
20+ YEARS EXPERIENCE
0.45s PAGE LOAD TIME
3 weeks EDITORIAL TIME SAVED / MONTH

Snapshot list#

A timeline-style list for career milestones, with bold labels and bottom borders:


<div class="kh-career-snapshot">
  <ul class="kh-snapshot-list">
    <li><strong>2023 – present</strong> Platform Architect at UNDRR</li>
    <li><strong>2018 – 2023</strong> Web Design Architect at EMBL</li>
    <li><strong>2015 – 2018</strong> Lead Developer at EMBL-EBI</li>
  </ul>
</div>
  • 2023 – present Platform Architect at UNDRR
  • 2018 – 2023 Web Design Architect at EMBL
  • 2015 – 2018 Lead Developer at EMBL-EBI

Layout#

Breakpoints#

Token Value Usage
$kh-breakpoint--sm 600px Small screens up
$kh-breakpoint--md 846px Medium screens up
$kh-breakpoint--lg 1024px Large screens up
$kh-breakpoint--xl 1300px Extra large
$kh-breakpoint--sm-down 599px Below small
$kh-breakpoint--md-down 845px Below medium
$kh-breakpoint--lg-down 1023px Below large

Grid system (.kh-grid)#

The base grid uses CSS Grid with auto-flow columns. Children reset their margin and max-width.


<div class="kh-grid" style="--kh-page-grid-gap: 1rem;">
  <div style="background: #d0d0ce; padding: 1rem; border-radius: 4px;">Column 1</div>
  <div style="background: #d0d0ce; padding: 1rem; border-radius: 4px;">Column 2</div>
  <div style="background: #d0d0ce; padding: 1rem; border-radius: 4px;">Column 3</div>
</div>
Column 1
Column 2
Column 3

Three-column grid (.kh-grid__col-3)#

Explicit three-column layout. Collapses to single column below 846px. Use .kh-grid__col--span-1 and .kh-grid__col--span-2 for asymmetric layouts:


<div class="kh-grid kh-grid__col-3" style="--kh-page-grid-gap: 1rem;">
  <div class="kh-grid__col--span-1" style="background: #d0d0ce; padding: 1rem; border-radius: 4px;">Span 1</div>
  <div class="kh-grid__col--span-2" style="background: #ffb81c; padding: 1rem; border-radius: 4px;">Span 2 (wider)</div>
</div>
Span 1
Span 2 (wider)

Stylized grid (.kh-grid-stylized)#

The signature layout pattern: a left gutter (16rem on desktop) with main content to the right. Used for most page sections. This page uses it for every section.


<div class="kh-grid-stylized" style="border: 2px dashed #d0d0ce; padding: 1rem;">
  <div style="background: #d0d0ce; padding: 1rem; border-radius: 4px; text-align: center;">← Gutter (16rem)</div>
  <div style="background: #ffb81c; padding: 1rem; border-radius: 4px;">Main content area</div>
</div>
← Gutter (16rem)
Main content area

Stack (.kh-stack)#

Vertical rhythm utility. Adds 1rem margin between sibling children. Use .kh-stack--800 for 2rem spacing:


<div class="kh-stack" style="border: 2px dashed #d0d0ce; padding: 1rem;">
  <div style="background: #d0d0ce; padding: 1rem; border-radius: 4px;">Item 1</div>
  <div style="background: #d0d0ce; padding: 1rem; border-radius: 4px;">Item 2 (1rem gap)</div>
  <div style="background: #d0d0ce; padding: 1rem; border-radius: 4px;">Item 3 (1rem gap)</div>
</div>
Item 1
Item 2 (1rem gap)
Item 3 (1rem gap)

Cluster (.kh-cluster)#

Horizontal flex layout with wrapping and center alignment. Used for navigation and grouped elements:


<div class="kh-cluster">
  <span class="kh-label">Item 1</span>
  <span class="kh-label">Item 2</span>
  <span class="kh-label">Item 3</span>
  <span class="kh-label">Item 4</span>
  <span class="kh-label">Item 5</span>
</div>
Item 1 Item 2 Item 3 Item 4 Item 5

Full bleed (.kh-u-fullbleed)#

Makes a section's background extend to the full viewport width while keeping content constrained. Used for the post title bar and footer:


<div class="kh-u-fullbleed kh-u-background-color--yellow--dark" style="padding: 2rem 0;">
  <p style="max-width: 70ch;"><strong>Full bleed section.</strong> The yellow background extends edge-to-edge while the text stays within the content width.</p>
</div>

Full bleed section. The yellow background extends edge-to-edge while the text stays within the content width.

Spacing utilities#

Utility classes for margin and padding. The naming follows a sizing scale.

Class Property Value
.kh-u-margin__bottom--200 margin-bottom 0.5rem
.kh-u-margin__bottom--600 margin-bottom 1.5rem
.kh-u-margin__top--1200 margin-top 3rem
.kh-u-padding__bottom--200 padding-bottom 0.75rem
.kh-u-padding__bottom--400 padding-bottom 1rem
.kh-u-padding__top--500 padding-top 1.5rem
.kh-u-padding__top--600 padding-top 2rem
.kh-u-padding__top--800 padding-top 3rem

Content width#

The .kh-content class constrains content to max-width: 70ch for readable line lengths. Add .kh-content--wide to remove the constraint.

Font variations#

Recursive exposes five variable axes. These helper classes apply preset combinations:


<p class="kh-font-headline" style="font-size: 32px;">Headline style — weight 800, casual 1, mono 0</p>
<p class="kh-font-code">Code style — mono 1, casual 0, no slant, no cursive</p>
<p class="kh-logo" style="font-size: 20px;">Logo style — weight 800, mono 1, casual 0</p>

Headline style — weight 800, casual 1, mono 0

Code style — mono 1, casual 0, no slant, no cursive

Display headlines#

Extra-large headings using .kh-font-headline--display. When two display headlines are adjacent, the first becomes 80px with a light weight:


<h2 class="kh-font-headline kh-font-headline--display">Display headline — 60px</h2>

Display headline — 60px

Variable font axes reference#

Axis Code Range Default Usage
Weight wght 300–800 400 Light to extra-bold
Monospace MONO 0–1 0 Proportional to monospace
Casual CASL 0–1 0 Formal to hand-drawn
Slant slnt -15–0 0 Upright to italic
Cursive CRSV 0–1 0.5 Controls cursive letterforms

Video embed#

The .kh-video wrapper provides a responsive 16:9 container. Iframes, objects, and embeds are positioned absolutely to fill the container:


<div class="kh-video">
  <div style="position:absolute;top:0;left:0;width:100%;height:100%;background:#d0d0ce;display:flex;align-items:center;justify-content:center;color:#707372;font-size:18px;">16:9 video placeholder</div>
</div>
16:9 video placeholder

Accessibility patterns#

A hidden link that becomes visible on focus, allowing keyboard users to skip to main content:

<a href="#main-content" class="kh-u-sr-only kh-skip-link">Skip to main content</a>

Tab to the top of this page to see it in action.

Screen reader only#

The .kh-u-sr-only class visually hides content while keeping it accessible to screen readers:

<h2 class="kh-u-sr-only">Section title visible only to screen readers</h2>

Focus indicators#

All interactive elements get a 1px dashed outline with 4px offset on focus. This applies to:

  • All links inside .kh-content
  • All elements with a kh-* class

CSS toggle#

The site's CSS can be toggled off via a checkbox in the footer (#kh-css-toggle). All custom styles are scoped under body:has(#kh-css-toggle:checked), so unchecking it reveals the bare HTML structure — useful for testing structural accessibility.

Reduced motion#

Animations (the dappled-light ambience effect and venetian blind sway) are gated behind @media (prefers-reduced-motion: no-preference) and disabled by default.

High contrast#

The ambient background effects are disabled entirely under @media (prefers-contrast: high).

Utility classes#

Visibility#

Class Behavior
.kh-u-show-for-mobile-only Visible below 600px, hidden above
.kh-u-show-for-medium-up Visible 846px and up
.kh-u-show-for-medium-only Visible 846px – 1023px only
.kh-u-show-for-large Visible 1024px and up
.kh-u-show-for-print-only Hidden on screen, visible when printed
.kh-u-do-not-print Visible on screen, hidden when printed
.kh-u-sr-only Visually hidden, accessible to screen readers

Text#

Class Effect
.kh-u-text--nowrap Prevents text from wrapping
.kh-u-text-color--grey Sets text color to #707372

Color#

Class Effect
.kh-u-background-color--yellow--dark Background #ffb81c

The .kh-link class provides blue link styling with purple visited state, independent of .kh-content context:

<a href="/example" class="kh-link">Styled link</a>
Class Purpose
.kh-navigation Navigation wrapper, pushed to right with margin-left: auto
.kh-navigation__list Flex list with gap spacing, no bullets
.kh-navigation__item Inline-flex list item
.kh-navigation__link Link with no underline, underlines on hover
.kh-navigation__link--active Bold weight for current page

Print#

Links in .kh-content lose their decoration on print. Elements with .kh-u-do-not-print are hidden. The .ken-link--print class shows the href URL below the link text.


CSS custom properties reference#

All custom properties are scoped to body:has(#kh-css-toggle:checked):

Colors#

Property Default
--kh-color--grey #707372
--kh-color--grey--darkest #373a36
--kh-color--blue #3b6fb6
--kh-color--blue--dark #193f90
--kh-color--orange #f49e17

Layout#

Property Default Notes
--kh-body-width 81.25em Max page width
--kh-page-grid-gap 1rem / 2rem Grid gap (responsive)
--kh-grid-stylized-module--prime 16rem Left gutter width

Spacing#

Property Default Notes
--kh-stack-margin 1rem Vertical stack gap
--kh-text-margin--bottom 16px Text element bottom margin
--kh-box-padding 1rem Box component padding
--kh-cluster-margin 0.5rem Cluster item spacing

Theming#

Property Purpose
--kh-box-theme-color--background Box background override
--kh-box-theme-color--foreground Box text color override
--kh-button-background-color Button background
--kh-button-border-color Button border
--kh-button-shadow-background-color Button shadow

Editorial style guide Colophon