Design System · v1.05

The Dossyer Design System

A complete token-based design system following the industry-standard three-tier architecture: primitive → semantic → component. Every visual decision is a token. Every token has a name, a value, and a reason.

Architecture

Following the W3C Design Tokens standard and systems like IBM Carbon, Material Design 3, and Open Props, our tokens are organized in three tiers. Each tier adds context to the tier below it.

Tier 1 — Primitive tokens define what options exist. Raw palette colors, spacing units, font stacks. These are never used directly in components.

Tier 2 — Semantic tokens define why a value is used. They reference primitives and carry contextual meaning: "this is the text color," "this is the card background." This is the layer that enables theming.

Tier 3 — Component tokens define where a value is applied to a specific component. Used sparingly — only when a component needs to deviate from semantics. Most components should reference Tier 2 directly.

The flow is always one-directional: primitives → semantics → components. A component token references a semantic token. A semantic token references a primitive. A primitive holds the raw value. Changing a primitive ripples through the entire system.

/* Tier 1: Primitive — the raw value */
--color-ink: #0C0B09;

/* Tier 2: Semantic — the meaning */
--color-text-primary: var(--color-ink);

/* Tier 3: Component — the specific override (rare) */
--card-headline-color: var(--color-text-primary);

1. Color

Tier 1 — Primitive Palette

Tier 1 · Primitives

The raw color values. These define what colors exist in the system. Named by what they are, not what they do.

Surface Scale (light → dark)

white
wh-1
wh-2
wh-3
wh-4
paper
ppr-2
ppr-3
rule

Ink Scale (dark → light)

ink
mid
light
faint

Red Family — Primary Brand Color

Used for: breaking news, urgency, errors, unread indicators, call-to-action emphasis. The base value (500) is our primary red. Lighter steps replace all rgba(184,50,37,...) patterns.

50
100
200
300
400
500
600
700
800
900
TokenValueUsage
--color-red-50#FCF6F3Tint background: update-breaking block bg, alert surfaces
--color-red-100#F9ECE9Hover/selected: breaking badge hover bg, velocity pill bg
--color-red-200#F1D5D1Borders: accent borders on red-tinted surfaces
--color-red-300#E3ACA6Muted: disabled state, placeholder illustrations
--color-red-400#CD6F66Secondary: icons, soft emphasis, chart accents
--color-red-500#B83225Base: breaking badge, unread dot, status-breaking
--color-red-600#9C2A1FEmphasis: section rules, accent borders
--color-red-700#84241BHover: button hover, interactive hover on red
--color-red-800#6B1D15Strong: text on red-100, high contrast
--color-red-900#531611Darkest: text on red-50, maximum contrast on tints

Navy Family — Primary Brand Color

Used for: interactive elements, links, focus rings, selected states, new-info updates, coverage center, navigation. The base value (500) is our primary navy. Lighter steps replace all rgba(26,58,92,...) patterns.

50
100
200
300
400
500
600
700
800
900
TokenValueUsage
--color-navy-50#F8F8F7Tint background: update-new block bg, info surfaces
--color-navy-100#EDEEEFHover/selected: chip hover bg, focus-ring fills
--color-navy-200#D6DBDFBorders: dividers on navy-tinted surfaces
--color-navy-300#AFB9C4Muted: disabled interactive, placeholder icons
--color-navy-400#6A7F94Secondary: secondary links, chart labels, soft icons
--color-navy-500#1A3A5CBase: links, focus rings, interactive, updated-badge
--color-navy-600#16314EEmphasis: strong links, active tab indicators
--color-navy-700#132A42Hover: button/link hover, interactive-hover
--color-navy-800#0F2235Strong: text on navy-100, high contrast
--color-navy-900#0C1A29Darkest: text on navy-50, maximum contrast on tints

Supporting Accent Colors

Gold, green, teal, and purple are used less frequently and don't need full 10-step scales yet. If any of these become primary brand colors in the future, build a full scale following the same pattern.

gold
green
teal
purple

Dark Surface

ask-bg
TokenValueNotes
--color-white#FFFEFCWarmest white. Card surfaces.
--color-white-1#FEFDF912% toward paper
--color-white-2#FCFAF725% toward paper
--color-white-3#FAF7F250% toward paper
--color-white-4#F8F4ED75% toward paper
--color-white-5#F5F0E8= --color-paper
--color-paper#F5F0E8Page background
--color-paper-2#ECE7DDRecessed surfaces
--color-paper-3#E1DCD1Deep recessed / disabled
--color-rule#D2C9BCBorders, dividers
--color-ink#0C0B09Darkest text
--color-ink-mid#36332CBody text
--color-ink-light#66605ASecondary text
--color-ink-faint#9E9890Tertiary / metadata
--color-red-50…900see scale above10-step red family
--color-navy-50…900see scale above10-step navy family
--color-gold#8A6E0AWarnings, revised status
--color-green#1A5230Success, confirmed
--color-teal#186060Secondary accent
--color-purple#503890Tertiary accent
--color-ask-bg#16192ADark surface for ask panel

Tier 2 — Semantic Colors

Tier 2 · Semantic

These define how colors are used. Components reference these tokens, not primitives. If we ever theme The Dossyer (dark mode, alternate brand), we swap this layer only.

TokenReferencesUsage
--color-bg-pagevar(--color-paper)Page background
--color-bg-cardvar(--color-white)Card / block surfaces
--color-bg-card-readvar(--color-white-2)Read state card surface
--color-bg-recessedvar(--color-paper-2)Inset areas, table stripes
--color-bg-darkvar(--color-ink)Dark bars, headers, footer
--color-bg-askvar(--color-ask-bg)Ask surface background
--color-bg-red-tintvar(--color-red-50)Breaking update bg, alert surfaces
--color-bg-navy-tintvar(--color-navy-50)Info update bg, new-info surfaces
--color-text-primaryvar(--color-ink)Headlines, strong text
--color-text-secondaryvar(--color-ink-mid)Body prose, descriptions
--color-text-tertiaryvar(--color-ink-light)Bylines, less important
--color-text-mutedvar(--color-ink-faint)Timestamps, metadata
--color-text-on-darkvar(--color-white)Text on dark backgrounds
--color-text-on-dark-mutedrgba(246,241,233,.4)Secondary text on dark
--color-border-defaultvar(--color-rule)Card borders, dividers
--color-border-subtlevar(--color-paper-2)Inner dividers, table rows
--color-interactivevar(--color-navy-500)Links, buttons, focus rings
--color-interactive-hovervar(--color-navy-700)Hover state for interactive
--color-interactive-bgvar(--color-navy-50)Hover bg on interactive elements
--color-interactive-bordervar(--color-navy-200)Focus ring, input border active
--color-status-breakingvar(--color-red-500)Breaking news, urgency
--color-status-breaking-bgvar(--color-red-50)Breaking update block bg
--color-status-breaking-bordervar(--color-red-500)Breaking accent border
--color-status-updatedvar(--color-navy-500)New information status
--color-status-revisedvar(--color-gold)Revised content
--color-status-successvar(--color-green)Confirmed, positive
--color-unread-dotvar(--color-red-500)Unread indicator
--color-cov-left#2563ebCoverage bar: left-leaning
--color-cov-centervar(--color-navy-500)Coverage bar: center
--color-cov-right#dc2626Coverage bar: right-leaning

Tier 3 — Component Color Tokens

Tier 3 · Component

Used sparingly. Only when a component needs a value that doesn't map cleanly to a semantic token.

TokenReferencesComponent
--badge-breaking-bgvar(--color-red-500)Breaking news badge bg
--badge-breaking-textvar(--color-text-on-dark)Breaking news badge text
--badge-updated-bgvar(--color-navy-50)Updated badge bg
--badge-updated-textvar(--color-navy-800)Updated badge text
--chip-bgvar(--color-navy-50)Suggest chip background
--chip-bordervar(--color-navy-200)Suggest chip border
--chip-hover-bgvar(--color-navy-100)Suggest chip hover
--velocity-hot-bgvar(--color-red-100)Velocity pill (hot)
--velocity-hot-textvar(--color-red-700)Velocity pill text (hot)
--market-up-bg#d4eddaMarket pill (green bg)
--market-up-text#155724Market pill (green text)
--market-dn-bgvar(--color-red-100)Market pill (red bg)
--market-dn-textvar(--color-red-800)Market pill (red text)

Migration: rgba() → Scale Tokens

The color scales replace all scattered rgba() opacity tints with named tokens. This table maps the most common patterns currently in the CSS to their scale equivalents.

Navy rgba() → Navy Scale

Old PatternNew TokenWhere Used
rgba(26,58,92,.03)var(--color-navy-50)Update block bg, tint surfaces
rgba(26,58,92,.04)var(--color-navy-50)Chip backgrounds
rgba(26,58,92,.06)var(--color-navy-50)Focus ring fill
rgba(26,58,92,.08)var(--color-navy-100)Hover states, badge bg
rgba(26,58,92,.1)var(--color-navy-100)Velocity pill, pressed states
rgba(26,58,92,.12)var(--color-navy-200)Border on tinted surfaces
rgba(26,58,92,.22)var(--color-navy-200)Strong hover borders

Red rgba() → Red Scale

Old PatternNew TokenWhere Used
rgba(184,50,37,.03)var(--color-red-50)Breaking update block bg
rgba(184,50,37,.1)var(--color-red-100)Velocity hot pill bg

When implementing: search the CSS for rgba(26,58,92 and rgba(184,50,37 and replace each with the closest scale token. The scale steps produce slightly different exact colors than the rgba values — that's expected and correct. The scale values are perceptually designed to be harmonious with each other, which hand-picked opacities aren't.


2. Typography

Tier 1 — Primitive Type Scale

Tier 1 · Primitives

Font Families

TokenStackRole
--font-displayGeorgia, 'Times New Roman', serifHeadlines, story titles
--font-proseGeorgia, 'Times New Roman', serifLong-form reading
--font-sans'DM Sans', system-ui, sans-serifUI, cards, navigation
--font-mono'Inconsolata', 'Courier New', monospaceLabels, metadata, code

Size Scale

A rationalized scale that covers every size currently in use. Each step has a name and a purpose.

TokenValueSpecimen
--size-hed-xl32pxStory Headline
--size-hed-lg22pxModal Headline
--size-hed-md19pxSection Title
--size-hed-sm17pxIntro Opener
--size-body16pxDossier prose reading
--size-body-sm15pxIntro body, descriptions
--size-ui14pxInterface text, navigation
--size-ui-sm13pxChips, secondary UI
--size-caption13pxTrend items, social topics
--size-caption-sm12pxSub-descriptions, fine print
--size-meta12pxTimestamps, source counts
--size-label11pxSECTION LABELS
--size-nano10pxHEADER BAR LABELS
--size-micro10pxBADGE MICRO TEXT

Font Weights

TokenValueUsage
--weight-regular400Body text, descriptions
--weight-medium500UI emphasis, sub-headers
--weight-semibold600Card headlines, strong labels
--weight-bold700Display headlines, intro opener
--weight-black900Trend rank numbers only

Line Heights

TokenValueUsage
--lh-tight1.12Large display headlines
--lh-hed1.22Story headlines
--lh-snug1.3Card headlines, compact text
--lh-ui1.5Interface text
--lh-body1.7Card descriptions, UI prose
--lh-prose1.88Long-form dossier reading

Letter Spacing

TokenValueUsage
--tracking-tight1pxCompact mono labels
--tracking-normal1.5pxSmall labels, micro badges
--tracking-wide2pxUpdate labels, footnotes
--tracking-label3pxSection labels, bar headers
--tracking-display4pxCover/masthead labels

Tier 2 — Semantic Type Styles

Tier 2 · Semantic

Composite type styles that combine family, size, weight, line-height, and tracking into named roles. Components should use these roles, not assemble typography from individual tokens.

type-headline-xl
Iran Struck Israel Overnight. The Cycle Is Accelerating.

--font-display · --size-hed-xl · --weight-bold · --lh-tight

type-headline-card
The Fed Held Rates and Powell Said the Quiet Part Out Loud

--font-display · --size-hed-md · --weight-semibold · --lh-snug

type-prose
The Federal Open Market Committee voted 11-1 to hold the federal funds rate at 3.5–3.75% — a decision that was widely expected. The dot plot still projects one cut in 2026, unchanged from December.

--font-prose · --size-body · --weight-regular · --lh-prose · max-width: --measure-prose

type-label
TRENDING NOW

--font-mono · --size-nano · --weight-medium · --tracking-label · uppercase

type-ui
Rates unchanged at 3.5–3.75%. One cut projected this year.

--font-sans · --size-ui · --weight-regular · --lh-ui

Style NameFamilySizeWeightLine-heightTracking
type-headline-xldisplayhed-xlboldtight
type-headline-lgdisplayhed-lgboldhed
type-headline-carddisplayhed-mdsemiboldsnug
type-headline-smdisplayhed-smboldsnug
type-proseprosebodyregularprose
type-bodysansbody-smregularbody
type-uisansuiregularui
type-ui-strongsansuisemiboldui
type-captionsanscaptionregularsnug
type-metamonometaregularui
type-labelmononanomedium1label
type-label-smmonomicromedium1normal

3. Spacing

Tier 1 — Primitive Spacing Scale

Tier 1 · Primitives

A 4px base grid. Every spacing value in the system is a multiple of 4.

TokenValueVisual
--space-14px
--space-28px
--space-312px
--space-416px
--space-520px
--space-624px
--space-832px
--space-1040px
--space-1248px
--space-1664px

Tier 2 — Semantic Spacing

Tier 2 · Semantic
TokenReferencesUsage
--space-inset-xsvar(--space-1)Tight inner padding (badges)
--space-inset-smvar(--space-2)Compact component padding
--space-insetvar(--space-4)Default component padding
--space-inset-lgvar(--space-6)Modal / card inner padding
--space-stack-smvar(--space-3)Small vertical gap between items
--space-stackvar(--space-5)Default vertical gap
--space-stack-lgvar(--space-8)Section vertical gap
--space-inline-smvar(--space-2)Small horizontal gap (badge row)
--space-inlinevar(--space-4)Default horizontal gap

4. Elevation (Shadows)

Tier 1 · Primitives
TokenValueUsage
--shadow-xs0 1px 2px rgba(12,11,9,0.04)Flat cards, read state
--shadow-sm0 1px 3px rgba(12,11,9,0.06), 0 1px 2px rgba(12,11,9,0.04)Default cards
--shadow0 4px 16px rgba(12,11,9,0.07), 0 1px 3px rgba(12,11,9,0.05)Hover cards, elevated panels
--shadow-lg0 12px 40px rgba(12,11,9,0.12), 0 4px 12px rgba(12,11,9,0.06)Dropdowns, notifications
--shadow-xl0 20px 60px rgba(12,11,9,0.16), 0 8px 20px rgba(12,11,9,0.08)Modals
Tier 2 · Semantic
TokenReferencesUsage
--elevation-flatvar(--shadow-xs)Read cards, dimmed surfaces
--elevation-cardvar(--shadow-sm)Default card resting state
--elevation-raisedvar(--shadow)Hovered cards, popovers
--elevation-overlayvar(--shadow-xl)Modal overlays

5. Radius

Tier 1 · Primitives
TokenValueVisual
--radius-sm4px
--radius6px
--radius-lg10px
--radius-xl14px
--radius-full50%
Tier 2 · Semantic
TokenReferencesUsage
--radius-badgevar(--radius-sm)Badges, status pills
--radius-cardvar(--radius)Cards, blocks, containers
--radius-inputvar(--radius-lg)Inputs, chips, user bubbles
--radius-modalvar(--radius-lg)Modal boxes
--radius-circlevar(--radius-full)Dots, avatars, indicators

6. Borders

Tier 2 · Semantic

Border tokens combine width + style + color into named patterns.

TokenValueUsage
--border-default1px solid var(--color-border-default)Card borders, dividers
--border-subtle1px solid var(--color-border-subtle)Inner dividers, table rows
--border-strong2px solid var(--color-border-default)Section dividers, emphasis
--border-accent3px solid var(--color-interactive)Update highlights left border

7. Motion

Tier 1 · Primitives
TokenValueFeel
--ease-outcubic-bezier(0.16, 1, 0.3, 1)Snappy deceleration. Default for UI.
--ease-in-outcubic-bezier(0.4, 0, 0.2, 1)Smooth entrance/exit. Modals, overlays.
--duration-fast0.15sHover states, color transitions
--duration-normal0.25sDefault UI transitions
--duration-slow0.4sModals, overlays, expand/collapse
Tier 2 · Semantic
TokenValueUsage
--transition-hoverall var(--duration-fast) var(--ease-out)Hover color/shadow changes
--transition-uiall var(--duration-normal) var(--ease-out)Default UI transitions
--transition-overlayopacity var(--duration-normal) var(--ease-in-out)Modal backdrop fade
--transition-expandmax-height var(--duration-slow) var(--ease-out)Accordion, collapse/expand

8. Z-Index

Tier 2 · Semantic

Named z-index layers prevent magic number collisions.

TokenValueUsage
--z-base0Default layer
--z-sticky10Sticky headers within scroll areas
--z-header50Masthead
--z-notification85Live update bar
--z-overlay200Modal overlays

9. Layout

Tier 2 · Semantic
TokenValueUsage
--measure-prose64chMaximum prose line width
--layout-page-max1220pxPage max-width
--layout-page-gapvar(--space-8)Gap between main + sidebar
--layout-modal-wide760pxStory dossier modal max-width
--layout-modal-narrow600pxSnap modal max-width
--layout-chat-column780pxAsk surface conversation width
--layout-sidebar-min310pxSidebar minimum width

9.5. Buttons & Interactive Elements

Buttons are the most duplicated pattern in the codebase. The system defines three base button types and two modifier axes (surface and size). All buttons share a common reset; variants differ only in color, padding, and border treatment.

Base Reset — Applied to All Buttons

Tier 2 · Semantic Component
.btn {
  font-family: var(--font-sans);
  font-weight: var(--weight-medium);
  border: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  transition: var(--transition-hover);
  line-height: 1;
  -webkit-font-smoothing: antialiased;
}
.btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  transform: none;
}

Button Types

1. Primary — Filled, high emphasis

For the main action in a context: send message, refresh, submit. Strong background, white text.

Token / ClassValueUsage
.btn-primarybg: var(--interactive), text: var(--text-on-dark)Chat send buttons, CTA actions
.btn-primary:hoverbg: var(--interactive-hover), scale: 1.04Hover lift effect
.btn-primary:disabledbg: var(--color-rule), no scaleDisabled state
.btn-primary.btn-dangerbg: var(--color-red-500)Destructive primary: ask send button (red brand)
.btn-primary {
  background: var(--interactive);
  color: var(--text-on-dark);
  border-radius: var(--radius-input);
  padding: var(--space-2) var(--space-4);
  font-size: var(--size-ui);
}
.btn-primary:hover {
  background: var(--interactive-hover);
  transform: scale(1.04);
}
.btn-primary.btn-danger {
  background: var(--color-red-500);
}
.btn-primary.btn-danger:hover {
  background: var(--color-red-700);
}

2. Ghost — Transparent, low emphasis

For secondary actions: navigation tabs, refresh, toggles. Transparent background with border or text-only, highlights on hover.

Token / ClassValueUsage
.btn-ghostbg: transparent, border: var(--color-rule)Light surface ghost buttons
.btn-ghost:hoverbg: var(--color-paper)Hover fill
.btn-ghost-darkbg: transparent, border: var(--border-on-dark)Dark surface ghost: nav tabs, refresh
.btn-ghost-dark:hoverbg: var(--bg-on-dark-hover)Hover highlight on dark
.btn-ghost {
  background: transparent;
  border: 1px solid var(--color-rule);
  color: var(--text-secondary);
  border-radius: var(--radius-card);
  padding: var(--space-2) var(--space-3);
  font-size: var(--size-ui-sm);
}
.btn-ghost:hover {
  background: var(--color-paper);
  color: var(--text-primary);
}
.btn-ghost-dark {
  background: transparent;
  border: 1px solid var(--border-on-dark);
  color: var(--text-on-dark-muted);
  border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-3);
  font-size: var(--size-label);
}
.btn-ghost-dark:hover {
  background: var(--bg-on-dark-hover);
  color: var(--text-on-dark-mid);
}

3. Chip — Small, pill-shaped, inline actions

For suggest chips, source tags, filter pills, preloaded questions. Tinted background, compact padding, pill radius.

Token / ClassValueUsage
.btn-chipbg: var(--color-navy-50), border: var(--color-navy-200)Light surface chips: preloads, sources, suggest
.btn-chip:hoverbg: var(--color-navy-100), lift -1pxHover
.btn-chip-darkbg: var(--border-on-dark), border: var(--text-on-dark-faint)Dark surface chips: since-banner, ask hints
.btn-chip {
  background: var(--color-navy-50);
  border: 1px solid var(--color-navy-200);
  color: var(--interactive);
  border-radius: var(--radius-input);
  padding: var(--space-2) var(--space-3);
  font-size: var(--size-caption);
  line-height: var(--lh-snug);
}
.btn-chip:hover {
  background: var(--color-navy-100);
  border-color: var(--color-navy-200);
  transform: translateY(-1px);
}

Icon Buttons

Buttons that contain only an icon (no text label). Built by combining the base .btn with .btn-icon and a type modifier.

Sizing

ClassDimensionsIcon SizeUsage
.btn-icon-sm28×28px--size-icon-sm (16px)Close buttons, dismiss
.btn-icon34×34px--size-icon (20px)Default: chat send, actions
.btn-icon-lg40×40px--size-icon-lg (24px)Feature actions, hero CTAs
.btn-icon {
  width: 34px; height: 34px;
  padding: 0;
  border-radius: var(--radius-input);
}
.btn-icon .icon {
  width: var(--size-icon); height: var(--size-icon);
}
.btn-icon-sm {
  width: 28px; height: 28px;
  border-radius: var(--radius-card);
}
.btn-icon-sm .icon {
  width: var(--size-icon-sm); height: var(--size-icon-sm);
}

Icon Button Variants

Combine .btn-icon with a type to get the right surface treatment:

CombinationExampleUsage
.btn .btn-icon .btn-primaryNavy circle with white arrow iconChat send button (story, snap)
.btn .btn-icon .btn-primary .btn-dangerRed circle with white arrow iconAsk surface send button
.btn .btn-icon-sm .btn-ghostTransparent with × iconClose button on light modals
.btn .btn-icon-sm .btn-ghost-darkTransparent with × icon on dark barModal close, banner dismiss

Icon Button with Lucide

<!-- Primary icon button (chat send) -->
<button class="btn btn-icon btn-primary">
  <i data-lucide="arrow-right" class="icon"></i>
</button>

<!-- Ghost close button on dark bar -->
<button class="btn btn-icon-sm btn-ghost-dark">
  <i data-lucide="x" class="icon"></i>
</button>

<!-- Chip with icon + text -->
<button class="btn btn-chip">
  <i data-lucide="refresh-cw" class="icon-sm"></i>
  Refresh
</button>

Migration: Existing → New System

Map the 15 existing button patterns to the new base+modifier system:

Old ClassNew ClassesNotes
.ask-send-btn.btn .btn-icon .btn-primary .btn-dangerRed send button
.chat-btn.btn .btn-icon .btn-primaryNavy send button
.bar-dark-close.btn .btn-icon-sm .btn-ghost-dark× close on dark header
.since-x.btn .btn-icon-sm .btn-ghost-dark× dismiss on banner
.mnav-btn.btn .btn-ghost-darkNav tab
.mast-refresh.btn .btn-ghost-darkRefresh + icon
.schip.btn .btn-chip-darkSince-banner chip
.suggest-chip.btn .btn-chip-darkSuggest on dark surface
.suggest-chip-light.btn .btn-chipSuggest on light surface
.chat-preload-chip.btn .btn-chipPreloaded question chip
.src-chip.btn .btn-chipSource tag chip
.social-toggle.btn .btn-ghostExpand/collapse toggle

The new system reduces 15 independent button implementations to 3 base types × 2 surface variants × 3 sizes = 18 possible combinations from one set of composable classes. Each existing button can be expressed by combining 2–3 classes.


10. Naming Conventions

Token names follow a consistent structure across all categories:

/* Pattern: --{category}-{property}-{variant} */

/* Color */
--color-text-primary
--color-bg-card
--color-border-default
--color-status-breaking

/* Typography */
--font-display
--size-hed-xl
--weight-semibold
--lh-prose
--tracking-label

/* Spacing */
--space-4         /* primitive: 16px */
--space-inset     /* semantic: default padding */

/* Elevation */
--shadow-sm       /* primitive */
--elevation-card  /* semantic */

Rules: always kebab-case, always lowercase. Primitives use numeric or scale names. Semantics use descriptive purpose names. Component tokens prefix with the component name.


10.5. Iconography

Library: Lucide Icons

The Dossyer uses Lucide Icons — an open-source, MIT-licensed SVG icon library. Lucide is a fork of Feather Icons with broader coverage (1,500+ icons), active community development, and a clean editorial aesthetic that matches our warm, minimal brand.

Why Lucide

Implementation: Inline SVG Registry

Instead of a CDN dependency, our single-file architecture uses an inline SVG icon registry — a JS object mapping icon names to SVG path data, plus an icon(name, class) function that returns complete SVG markup. This means:

// Icon registry — SVG paths keyed by name
const ICONS = {
  'arrow-right': '<path d="M5 12h14M12 5l7 7-7 7"/>',
  'logo-x': '<path d="M18.244 2.25h3.308l-7.227..."/>',
  ...
};

// Render function — returns inline SVG string
function icon(name, extraClass) {
  // stroke-based for UI, fill-based for logos
}

Usage in HTML and JS

<!-- Direct inline SVG in static HTML -->
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
  stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>

<!-- Via JS icon() function in dynamic content -->
element.innerHTML = icon('arrow-right', 'icon-sm');

Icon Sizing Tokens

Tier 1 · Primitives
TokenValueUsage
.icon-xs11pxVelocity badges, spotlight labels, any text at 9–10px (--size-nano, --size-label). Rule: icons ~1.2× companion text size. Minimum text size in the system is 10px.
--size-icon-sm16pxInline with small text (11–13px), platform logos, metadata icons
--size-icon20pxDefault icon size — sidebar items, cards
--size-icon-lg24pxFeature icons, culture section, headers

Icon CSS Classes

.icon     { width: var(--size-icon); height: var(--size-icon);
            stroke: currentColor; stroke-width: 1.75;
            flex-shrink: 0; vertical-align: middle; }
.icon-sm  { width: var(--size-icon-sm); height: var(--size-icon-sm); }
.icon-lg  { width: var(--size-icon-lg); height: var(--size-icon-lg); }

Icon-to-Emoji Migration

Replace all emoji characters with Lucide icon elements. Key mappings:

Old (Emoji)New (Lucide)Context
🏀trophySports / March Madness
🏰building-2Entertainment / Disney
🌡️thermometerWeather
🔥flameVelocity: hot/breaking
𝕏twitterX / Twitter platform
📕book-openReddit
🟠code-2Hacker News
💼briefcaseLinkedIn
🦋cloudBluesky
🎵musicTikTok
📰file-textSubstack
starMarket spotlight

10.6. Dark Surface Text

Tier 2 · Semantic

Text on dark backgrounds (masthead, footer, ask surface, dark header bars) uses a warm-tinted white at various opacities. These tokens replace scattered rgba(246,241,233,...) values.

TokenValueUsage
--text-on-darkvar(--color-white)Primary text on dark: headlines, strong labels
--text-on-dark-hirgba(246,241,233,.8)High emphasis: active nav items, bright labels
--text-on-dark-midrgba(246,241,233,.6)Medium emphasis: hover states, secondary text
--text-on-dark-mutedrgba(246,241,233,.4)Muted: bar labels, timestamps, metadata
--text-on-dark-faintrgba(246,241,233,.2)Faint: decorative text, version labels, rules
--border-on-darkrgba(246,241,233,.12)Borders on dark surfaces
--bg-on-dark-hoverrgba(246,241,233,.06)Hover highlight on dark surfaces

These replace the ~23 raw rgba(246,241,233,...) patterns currently in the CSS. The mapping:

Old PatternNew Token
rgba(246,241,233,.8)var(--text-on-dark-hi)
rgba(246,241,233,.65) / .7var(--text-on-dark-mid)
rgba(246,241,233,.35) / .4) / .45)var(--text-on-dark-muted)
rgba(246,241,233,.2) / .25) / .3)var(--text-on-dark-faint)
rgba(246,241,233,.1) / .12) / .15)var(--border-on-dark)
rgba(246,241,233,.06) / .08)var(--bg-on-dark-hover)

10.7. Chat Component (.td-chat)

One component, three placements: ask surface drawer, story dossier modal, snap modal. No avatars — just right-aligned user bubble and left-aligned prose response that matches the story text.

BEM Structure

ClassPurpose
.td-chatContainer — border-top divider, inherits parent padding
.td-chat__label"ASK ABOUT THIS STORY" header (label-mono)
.td-chat__hintsContextual prompt chips container (flex-wrap)
.td-chat__chipIndividual prompt chip — one class for all contexts
.td-chat__msgsScrollable message area (max-height: 0 → 400px)
.td-chat__input-rowInput + send button row
.td-chat__inputText input field

Message Rendering

ElementClassSpecs
User question.cmsg.user > .bub-uRight-aligned, DM Sans var(--size-ui), paper-2 bg, subtle border, max-width: 80%
Response.cmsg > .bub-tbLeft-aligned, Georgia var(--size-body), var(--lh-prose), var(--text-secondary). Matches story prose exactly.
Typing.cmsg > .chat-typingNo bubble — italic Georgia, var(--text-muted), transparent bg, animated dots

The chat component has zero side padding — it inherits from its parent container (story-modal-body at 96px sides, snap-body at 40px sides, ask-drawer at its own max-width). The border-top divider aligns with the content edges.


10.8. Citation System

A unified registry pattern ensuring every citation — from story prose or chat responses — shares one sequential numbering system, one render component, and one collapsible section.

Architecture

ComponentPurpose
createCitationRegistry(footnotes, sourceLinks)Factory. Creates a registry seeded with story footnotes. Holds a lookup for deduplication and a fuzzy link resolver.
registry.resolve(sourceText)Returns {num, isNew}. Existing → same number. New → next sequential. Tries fuzzy match against sourceLinks for URL.
renderCitationItem({num, text, link})Single render function for ALL citations. Link or no link. Story or chat. One component.
buildFootnotesHTML(footnotes, id, sources, srcUrlMap)Builds the collapsible section. Citations (numbered, linked) + additional coverage (not numbered, linked where available).
textToHtmlChat(text, registry)Converts [Source, Date] → globally-numbered superscripts via registry. Returns {html, newEntries}.
appendCitationsToDOM(fnSectionId, newEntries)Inserts new citations above coverage line. Updates count badge. Uses renderCitationItem.

Data Types (not interchangeable)

TypeWhat it isHas number?Has superscript in text?
CitationA specific claim attributed to a specific sourceYes — sequential [1], [2], etc.Yes — inline in prose
Additional sourceAn outlet that covered the story but isn't directly citedNo — no fake numbersNo — no superscript

Collapsible Section

Collapsed by default. Toggle reads "SOURCES & CITATIONS" with chevron-right icon (rotates 90° on expand) and a count badge showing the number of numbered citations. Additional coverage appears below citations as "Also covered by: ..." with comma-separated linked source names.

Citation Format

All citations follow the format [Source Name, Month Year] — e.g. [Reuters, March 2026]. The date is required because it enables the registry's fuzzy link resolver to match against sourceLinks keys.


11. Implementation Checklist

When applying this design system to the codebase:

  1. Add all Tier 1 and Tier 2 tokens to :root — including brand color scales, dark surface text tokens, icon sizing, and semantic elevation/transition/z-index tokens
  2. Replace every raw hex color with the appropriate semantic token
  3. Replace every raw font-size with the appropriate size token
  4. Replace rgba(26,58,92,...) patterns with navy scale tokens
  5. Replace rgba(184,50,37,...) patterns with red scale tokens
  6. Replace rgba(246,241,233,...) patterns with dark surface text tokens
  7. Replace #fff with var(--text-on-dark)
  8. Create and apply .label-mono utility class across all 20 selectors that repeat the mono+uppercase+tracking pattern
  9. Load Lucide Icons via CDN and replace all emoji characters with Lucide icon elements
  10. Add --size-icon tokens and .icon / .icon-sm / .icon-lg utility classes
  11. Verify: no raw hex outside :root, no raw font-size, no brand rgba patterns, no emojis in UI

New Patterns (v01.42)

Components and patterns added since v1.03 that are now part of the system.

Card Status Badges

Unified badge system replacing the old unread-dot and updated-badge. Three states tied to the since banner's visual language:

ClassAppearanceWhen used
.card-status--newRed bg, white textUnread stories. Matches since banner "New" badge.
.card-status--readPaper bg, faint textAfter markAsRead() swaps class + text.
.card-status--updatedNavy-100 bg, navy textStory updated after being read.

Notification System

One concept, two surfaces. They never overlap and share the same navy color:

SurfacePurposePosition
Since bannerCatch-up: "what happened while you were away"Top, below masthead. Fixed single line, chip overflow → "N more" label.
Update tabReal-time doorbell: "something changed while reading"Hanging from masthead. Clicking scrolls to top + reveals since banner.

Inline Send Button

The .td-chat__input-row is the styled container (background, border, border-radius). The <input> inside has no border/background. The send button sits at the right edge. Corner radius rule: container uses --radius-input (10px), nested button uses --radius (6px) — one step down.

Accessibility Utilities

UtilityPurpose
a11yInteractive(selector)Adds role="button", tabindex="0", Enter/Space handler to clickable non-button elements
a11yModalOpen/CloseStores trigger element, returns focus on modal close
a11yUpdateCollapsed(selector, isExpanded)Sets tabindex=-1 on children of collapsed containers, restores on expand
announceToSR(text)Pushes text to aria-live region for screen reader announcements
.sr-onlyVisually hidden, accessible to assistive tech
.skip-linkSkip navigation link, visible on Tab focus

Elevation Rule

All hover shadows use --elevation-raised. No raw var(--shadow) on hover states. Resting states use --elevation-card or --elevation-flat.

Typography Floor

No text below 10px. The smallest token is --size-micro: 10px. This is a hard rule. If something needs to be smaller, it probably doesn't need to be text.

Focus Ring Context

SurfaceFocus treatment
Light backgroundsoutline: 2px solid var(--color-navy-500)
Dark backgrounds (masthead, bars)outline-color: var(--color-white)
Inside overflow:hidden (cards, sidebar)box-shadow: inset 0 0 0 2px var(--color-navy-500)
Since banner childrenbox-shadow: inset 0 0 0 2px var(--color-white)

v1.05 Additions (app v01.43–v01.44)

New Tier 1 Primitives

TokenValuePurpose
--color-blue-600#2563ebCoverage bar: left
--color-red-ext-500#dc2626Coverage bar: right (distinct from brand red)
--color-green-50#d4eddaMarket up background
--color-green-900#155724Market up text
--radius-btn10pxButtons — ends --radius-input borrowing
--z-masthead90Masthead stacking (was hardcoded)

--size-micro is now an explicit alias of --size-nano (duplicate resolved; one source of truth).

New Components & Utilities

NameKindNotes
velBadge(level, label, icon?)JS componentSingle source for velocity badges. Levels: hot / up / watch. Icons from registry.
.btn-alertButton variantRed action button extending .btn (replaces since-update-btn one-off).
.td-chat__error + .td-chat__retryChat stateError surface with retry, light + dark variants.
.img-fallbackComponentStyled placeholder replacing any failed image (global capture-phase listener).
.ticker-symbol, .updates-container, .cov-note, .live-edition-noteUtilitiesReplace former inline styles; honest coverage note for live stories.
scrollLockJS utilityReference-counted body scroll lock (acquire/release).
trapFocus / releaseFocusTrapJS utilityModal focus containment, wired into a11yModalOpen/Close.
escapeAttr, stripHtmlJS utilityAttribute-safe escaping; DOM-based HTML→text.
initIconography()JS initRenders .js-brand-icon and .js-vel placeholders from the ICONS registry on load.

Removed: .text-prose, .icon-brand, .btn-icon-lg (dead code, zero usages).

v01.45–57 additions (live pipeline arc)

New utilities/components: .cov-caption (estimate label under live coverage bars), .img-fallback--{markets|tech|world|politics|culture} topic-tinted placeholder variants + .img-fallback-label, .live-edition-note, fmtET()/fmtETAt() (all timestamps in real Eastern Time), topicPlaceholder(). Behavior rules: every live card renders a visual (photo or topic placeholder); the markets spotlight populates from live data or hides — stale data never displays; market values/percents are normalized app-side. Main markets table carries #mkt-main-table; renderers must scope to owned DOM (class selectors leak across component instances — see handoff v01.52).