Controls
Controls on this site live where a book’s imprint page would be: the footer colophon and the side rail. They are colophon furniture, not toolbar buttons. No floating action buttons, no sticky headers, no hamburger menus. The reader finds them when they look for them, and not before.
Every control uses a ghost-button pattern: no background, no border, --text-3 color that warms to --text on hover. They recede into the colophon until engaged.
For exact CSS values, see the implementation spec and tokens.json.
Theme toggle
Purpose
Switches between light and dark mode. Lives in the footer colophon and the side rail. Nowhere else.
Anatomy
| Part | Selector | Description |
|---|---|---|
| Button | .footer-theme-toggle | Ghost button with icon |
| Sun icon | .footer-theme-toggle .sun | Shown in dark mode (click to go light) |
| Moon icon | .footer-theme-toggle .moon | Shown in light mode (click to go dark) |
Specs
The toggle is a <button> with no background, no border, padding: 0. Icon visibility swaps per mode: the moon shows in light mode (offering dark), the sun shows in dark mode (offering light).
The toggle appears identically in the footer and side rail. Both instances use the same styling; the side rail version inherits its position from .side-rail-controls.
States
| State | Property | Value |
|---|---|---|
| Default | color | var(--text-3) |
| Hover | color | var(--text) |
Transition: color 0.2s ease.
Icon visibility:
| Mode | .sun | .moon |
|---|---|---|
| Light | display: none | display: block |
| Dark | display: block | display: none |
Keyboard shortcut
Pressing d toggles the theme. Press it now to try. Disabled when focus is in input, textarea, or [contenteditable].
Responsive behavior
| Breakpoint | Change |
|---|---|
<= 1079px | Side rail instance hidden (side rail itself hidden) |
| Footer settings hidden entirely |
Accessibility
The button uses an accessible label for screen readers. Focus is visible via the site’s global focus styling. The icon swap is purely visual; the button’s accessible name communicates the action regardless of which icon is displayed.
Code example
<button class="footer-theme-toggle" aria-label="Toggle theme">
<svg class="sun"><!-- sun icon --></svg>
<svg class="moon"><!-- moon icon --></svg>
</button>
Accent picker
Purpose
Lets the reader choose their accent color from the eight Flexoki hues. Progressive disclosure: a small colored dot invites curiosity, expanding on click to reveal the full palette.
Anatomy
| Part | Selector | Description |
|---|---|---|
| Picker container | .footer-accent-picker / .accent-picker | Flex row wrapper |
| Current dot | .accent-current | Colored circle showing active accent |
| Options container | .accent-options | Expandable row/column of color options |
| Individual option | .accent-option | Single color circle |
Specs
Current accent dot:
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--accent);
border: 2px solid var(--ui-2, var(--ui));
cursor: pointer;
padding: 0;
Options container starts collapsed at max-width: 0, opacity: 0, visibility: hidden, clipped with clip-path: inset(-4px 0 -4px -4px). The negative inset values prevent the clip from cutting off the dot borders.
When the .open class is applied, the container expands to max-width: 220px, opacity: 1, visibility: visible, margin-left: 8px, with clip-path: none. The expansion animation uses 0.3s cubic-bezier(0.22, 1, 0.36, 1) for max-width and margin, 0.2s ease for opacity.
Individual options are 16px circles with 2px solid transparent borders. On hover they scale(1.15). The active option gains border-color: var(--text-3).
Variants
Footer variant: Options expand horizontally (default flex direction). Container within .footer-accent-picker.
Side rail variant: Options expand vertically. The side rail overrides the accent options to use flex-direction: column and clip-path: inset(-4px -4px 0 -4px). When open: max-width: none, max-height: 220px, margin-left: 0, margin-top: 8px.
States
Current dot:
| State | Property | Value |
|---|---|---|
| Default | border-color | var(--ui-2, var(--ui)) |
| Hover | border-color | var(--text-3) |
Options container:
| State | max-width | opacity | margin-left | clip-path | visibility |
|---|---|---|---|---|---|
| Closed | 0 | 0 | 0 | inset(-4px 0 -4px -4px) | hidden |
.open | 220px | 1 | 8px | none | visible |
Individual option:
| State | Property | Value |
|---|---|---|
| Default | border-color | transparent |
| Hover | transform | scale(1.15) |
.active | border-color | var(--text-3) |
Onboarding hint
On first visit (no accent preference stored), the current dot pulses twice to draw attention:
@keyframes accent-hint {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.3); }
}
.accent-current.hint {
animation: accent-hint 0.6s ease 2;
}
Gated behind prefers-reduced-motion: no-preference.
Keyboard shortcut
Pressing c cycles to the next accent color — try it now, or find the colored dot in the footer. Disabled when focus is in input, textarea, or [contenteditable].
Responsive behavior
| Breakpoint | Change |
|---|---|
<= 600px | .accent-current grows to 22px x 22px |
<= 600px | .accent-option grows to 20px x 20px |
<= 1079px | Side rail variant hidden |
| Hidden entirely |
The size increase at <= 600px ensures touch targets are comfortable on mobile.
Accessibility
Each option button should carry an accessible label naming the color (e.g., “Orange accent”, “Cyan accent”). The current selection is communicated via the .active class and border treatment. The keyboard shortcut provides an alternative to the click-to-expand interaction.
Code example
<div class="footer-accent-picker accent-picker">
<button class="accent-current" aria-label="Choose accent color"></button>
<div class="accent-options">
<button class="accent-option active" style="background: #DA702C" aria-label="Orange accent"></button>
<button class="accent-option" style="background: #D14D41" aria-label="Red accent"></button>
<button class="accent-option" style="background: #D0A215" aria-label="Yellow accent"></button>
<button class="accent-option" style="background: #879A39" aria-label="Green accent"></button>
<button class="accent-option" style="background: #3AA99F" aria-label="Cyan accent"></button>
<button class="accent-option" style="background: #4385BE" aria-label="Blue accent"></button>
<button class="accent-option" style="background: #8B7EC8" aria-label="Purple accent"></button>
<button class="accent-option" style="background: #CE5D97" aria-label="Magenta accent"></button>
</div>
</div>
Font size control
Purpose
Cycles through font size options. Lives in the footer settings alongside the theme toggle and accent picker.
Anatomy
| Part | Selector | Description |
|---|---|---|
| Button | .footer-font-size | Ghost button with “Aa” label |
| Label | .footer-font-size .font-size-label | Text label that reflects current size |
Specs
Same ghost-button pattern as the theme toggle: no background, no border, --text-3 color. Uses Signifier at 0.85rem with padding: 4px. The label text transitions font-size 0.15s ease when cycling to provide subtle visual feedback. Press f to cycle through sizes, or find the “Aa” button in the footer.
States
| State | Property | Value |
|---|---|---|
| Default | color | var(--text-3) |
| Hover | color | var(--text) |
Transition: color 0.2s ease.
Code example
<button class="footer-font-size" aria-label="Change font size">
<span class="font-size-label">Aa</span>
</button>
Newsletter signup form
Purpose
Email subscription form on the homepage. Connects to Beehiiv via a Vercel serverless function (api/subscribe.ts) that proxies the API key server-side. No iframe, no third-party styling.
Anatomy
| Part | Selector | Description |
|---|---|---|
| Section | .newsletter | Full-width section after the two-column grid |
| Label | .section-label | ”NEWSLETTER” in standard section label style |
| Blurb | .newsletter-blurb | Subtitle-style description |
| Form row | .newsletter-row | Grid container aligning input and button to the two-column layout |
| Email input | .newsletter-row input | Bottom-border-only text field |
| Submit button | .newsletter-row button | Text-link style button |
| Status | .newsletter-status | Success/error feedback |
Specs
Form row (.newsletter-row): Mirrors the .home-columns grid so the button aligns with the right column.
display: grid;
grid-template-columns: 1fr 1fr;
gap: calc(var(--space-xl) * 1.5);
align-items: end;
Input (.newsletter-row input): Bottom border only — like writing on a line in a printed subscription card.
font-family: var(--font-serif);
font-size: 0.9rem;
color: var(--text);
background: transparent;
border: none;
border-bottom: 1px solid var(--ui-2);
width: calc(100% + var(--space-xl)); /* extends into grid gap */
Placeholder: italic, --text-3. Focus: border-bottom-color: var(--text). Transition: border-color 0.3s ease.
Button (.newsletter-row button): Text-link pattern matching “All writing →” / “All links →” navigation.
font-family: var(--font-sans);
font-size: 0.8rem;
font-weight: 400;
color: var(--text-2);
background: none;
border: none;
Blurb (.newsletter-blurb): Same treatment as .section-subtitle — --font-sans, 0.8rem, --text-2.
States
Input:
| State | border-bottom-color |
|---|---|
| Default | var(--ui-2) |
| Focus | var(--text) |
Button:
| State | color |
|---|---|
| Default | var(--text-2) |
| Hover | var(--text) |
| Disabled | opacity 0.5 |
Status messages (.newsletter-status):
| State | color |
|---|---|
| Success | var(--accent) |
| Error | var(--red) |
Responsive behavior
| Breakpoint | Change |
|---|---|
<= 600px | Grid collapses to single column, input resets to 100% width |
Code example
<section class="newsletter" data-animate>
<p class="section-label">Newsletter</p>
<p class="newsletter-blurb">A loosely structured newsletter from someone who's never been great at doing just one thing.</p>
<form class="newsletter-form" id="newsletter-form">
<div class="newsletter-row">
<input type="email" name="email" placeholder="your@email.com" required autocomplete="email" />
<button type="submit">Subscribe →</button>
</div>
<p class="newsletter-status" id="newsletter-status"></p>
</form>
</section>
Toast notification
Purpose
Confirms brief actions. “URL copied to clipboard.” Not a software notification. An editorial aside, set in serif italic on a paper-colored surface with a warm shadow. It appears, delivers its message, and leaves.
Anatomy
| Part | Selector | Description |
|---|---|---|
| Toast | .toast | Fixed-position notification element |
Specs
position: fixed;
bottom: var(--space-xl);
left: 50%;
transform: translateX(-50%) translateY(20px);
background: var(--bg);
color: var(--text);
padding: var(--space-sm) var(--space-lg);
border: 1px solid var(--ui);
font-family: var(--font-serif);
font-style: italic;
font-size: 0.9rem;
letter-spacing: 0.01em;
opacity: 0;
pointer-events: none;
z-index: 1000;
The serif italic text and paper background distinguish this from the “software toast” pattern. It reads like a parenthetical remark, not a system alert.
Box shadow is mode-dependent:
| Mode | box-shadow |
|---|---|
| Light | 0 2px 8px hsl(40deg 10% 50% / 0.1), 0 1px 2px hsl(40deg 10% 50% / 0.06) |
| Dark | 0 2px 8px hsl(30deg 5% 5% / 0.4), 0 1px 2px hsl(30deg 5% 5% / 0.2) |
The warm hue angles in the shadow (40deg light, 30deg dark) match the paper metaphor. No neutral grays.
States
| State | transform | opacity |
|---|---|---|
| Hidden | translateX(-50%) translateY(20px) | 0 |
.show | translateX(-50%) translateY(0) | 1 |
Transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.3s ease. The signature easing on the slide-up gives the toast a physical, decelerating settle.
Responsive behavior
| Breakpoint | Change |
|---|---|
display: none !important |
Accessibility
The toast is non-interactive (pointer-events: none). It should be announced via role="status" or an ARIA live region so screen readers catch the confirmation without requiring visual attention. Content is always a brief, complete sentence.
Code example
<div class="toast" id="toast" role="status">URL copied to clipboard</div>
Share modal
Purpose
Overlay for sharing pull quotes as generated PNG images. Contains a preview of the share image and action buttons (copy image, download, close). This is the only modal pattern on the site.
Anatomy
| Part | Selector | Description |
|---|---|---|
| Overlay | .share-modal-overlay | Full-screen backdrop |
| Modal card | .share-modal | Centered content card |
| Preview image | .share-modal-image | Generated PNG preview |
| Actions row | .share-modal-actions | Button group |
| Default button | .share-modal-actions button | Secondary action |
| Primary button | .share-modal-actions button.primary | Primary action |
| Close button | .share-modal-close | Top-right dismiss |
| Image template | .share-image-template | Offscreen rendering target |
Specs
Overlay: Fixed full-screen at z-index: 2000, background: rgba(0, 0, 0, 0.6). Centers the modal card with flexbox.
Modal card: background: var(--bg), border-radius: 8px, padding: var(--space-lg), max-width: 90vw, max-height: 90vh, box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2). The 8px border radius is the only rounded corner on the site, justified by the overlay context separating it from the page’s sharp-corner aesthetic.
Preview image: max-width: 500px, border-radius: 4px, border: 1px solid var(--ui).
Buttons: Montreuil at 0.85rem, padding: var(--space-sm) var(--space-md), border-radius: 4px.
Close button: Positioned absolute at top: var(--space-md), right: var(--space-md). No background, no border, font-size: 1.5rem, --text-2 color.
Image template: Hidden offscreen (left: -9999px), width: 600px, padding: 48px. Used to render the share card PNG with Signifier at 28px italic weight 300.
States
Overlay:
| State | opacity | visibility |
|---|---|---|
| Hidden | 0 | hidden |
.show | 1 | visible |
Transition: opacity 0.2s ease, visibility 0.2s ease.
Buttons:
| Variant | State | background | color | border-color |
|---|---|---|---|---|
| Default | Default | var(--bg) | var(--text) | var(--ui) |
| Default | Hover | var(--bg) | var(--text) | var(--accent) |
| Primary | Default | var(--text) | var(--bg) | var(--text) |
| Primary | Hover | var(--accent) | var(--bg) | var(--accent) |
Button transition: border-color 0.25s ease, background 0.25s ease.
Responsive behavior
| Breakpoint | Change |
|---|---|
<= 600px | Modal padding reduces to var(--space-md) |
<= 600px | Image max-width becomes 100% |
<= 600px | Buttons gain min-height: 44px for touch targets |
display: none !important |
Accessibility
The overlay should trap focus while open and restore it on close. Escape key closes the modal. The close button needs an accessible label. The generated share image should have meaningful alt text describing the quoted content.
The 44px button height at <= 600px meets WCAG 2.5.8 touch target guidance.
Code example
<div class="share-modal-overlay" id="shareModal">
<div class="share-modal">
<button class="share-modal-close" aria-label="Close">×</button>
<img class="share-modal-image" src="" alt="Share image preview" />
<div class="share-modal-actions">
<button class="primary">Copy image</button>
<button>Download</button>
</div>
</div>
</div>Specimens
Theme Toggle
Press D to toggle
Accent Picker
8 Flexoki hues. Press C to cycle.
Font Size
Cycles through size options
Toast