Every change is a system change. When you fix something, you're not fixing one instance — you're creating or improving the component that handles that class of problem. If the fix doesn't work for the next case too, it's not done.
This means:
Every fix, addition, or change should be treated as building a foundation for a scalable system — never as a one-off patch. Before implementing anything, ask: Does a system already exist for this? Should this be a repeatable component? Will this approach work when there are 10 of these instead of 1? If the answer reveals that you're about to write a one-off, stop and design the system first.
This is the meta-rule. It governs how you approach every other rule. The difference between a bandaid and a system:
The test: if someone asks "why is it done this way?" the answer should be "because the system works this way" — not "because we needed to fix that one thing."
Never use a raw value. Colors, spacing, font sizes, shadows, radii, transitions — all come from CSS variables. If the value you need doesn't exist as a token, create the token first, then use it.
This applies to: hex colors, pixel values, font-family strings, timing functions, and any value that could conceivably change globally.
If a pattern appears in more than one place, it's a component. Build the component, then use it everywhere. When the design changes, you change the component once.
Examples: modal overlays, dark header bars, chat input rows, citation footnotes, label typography, card hover states.
If JS logic follows the same algorithm with different inputs, it's one function with parameters. Don't copy-paste and change the IDs.
The chat send flow (get input → inject user bubble → show typing → call API → render response → show suggest chip) is one algorithm. It should be one function that takes a config object: {inputId, msgsId, btnId, history, systemPrompt, chipVariant}.
Use base classes + modifier classes, not separate parallel definitions. A snap modal header isn't a different thing from a story modal header — it's the same .bar-header component with a .bar-header--sticky modifier.
When you need a value between two tokens, build the scale first. Don't pick a hex value by eye — create the intermediate steps as tokens, then choose from the scale. (See: --white-1 through --white-5.)
Every code change gets a new version number. Never overwrite a previous version. The title tag, footer, and filename must all match.
Files are named: the_dossyer_vMM.NN.html — two-digit major version, two-digit minor version, with dots preserved.
| Segment | Meaning |
|---|---|
MM (major) | 00 = prototype phase. Increments to 01 when we move to production-grade code, separate files, or a build system. |
NN (minor) | Increments by 1 for every change that touches code. Bug fixes, UI tweaks, feature additions — all get a bump. |
vN suffix<title> tag: Dossyer · v00.XXv00.XX · Prototypethe_dossyer_v00.XX.htmlThe blueprint document (the_dossyer_blueprint_v2.html) maintains a running version history. Each entry follows this format:
v00.XX — Brief description of what changed. What was added or fixed, not how. The code itself is the how.
Keep entries concise — what changed and why, not how. The code itself is the how.
Each support document is versioned independently with a vN suffix in its filename. The blueprint maintains a running version history for the app. Support docs are updated when their scope changes — not on every app patch.
| Document | File Pattern |
|---|---|
| Product Blueprint | the_dossyer_blueprint_vN.html |
| Voice & Tone Bible | the_dossyer_voice_bible_vN.html |
| System Prompt Architecture | the_dossyer_system_prompt_vN.html |
| System Rules | the_dossyer_system_rules_vN.html |
| Audit Prompt | the_dossyer_audit_prompt.html |
Before adding a new component, check if an existing one can be extended. The default action is reuse. The exception is "this genuinely needs different behavior, not just different content."
These are the shared components in the system. Any new feature should use these before creating new ones.
--white → --white-1 through --white-5 → --paper → --paper-2 → --paper-3 for surface colors. --ink → --ink-mid → --ink-light → --ink-faint for text. Brand color scales: --color-red-50 through --color-red-900, --color-navy-50 through --color-navy-900. Supporting: --color-gold, --color-green, --color-teal, --color-purple. Three-tier architecture: primitive → semantic → component.--space-1 (4px) through --space-16 (64px). 4px base grid. Legacy aliases --sp-N still work but canonical names are --space-N.--shadow-xs → --shadow-sm → --shadow → --shadow-lg → --shadow-xl. Layered composites for depth.--radius-sm (4px), --radius (6px), --radius-lg (10px), --radius-xl (14px), --radius-full (50%). Semantic: --radius-badge, --radius-card, --radius-input, --radius-modal..bar-dark base class (ink bg, flex, space-between, mono label) + .bar-dark-label + .bar-dark-note. One component for all dark headers..overlay base (fixed, inset, backdrop blur, opacity transition) + .overlay-box (white bg, radius, shadow, scale transition). Variants: .overlay-box--wide (760px), .overlay-box--narrow (600px). Modifier: .overlay--center for vertically centered modals.createCitationRegistry(footnotes, sourceLinks) → registry.resolve(sourceText) for globally sequential numbering. renderCitationItem({num, text, link}) — single render function for all citations. buildFootnotesHTML() builds collapsible section. appendCitationsToDOM() adds chat citations to shared section. Citations are numbered; additional coverage sources are not.sendChatMessage(cfg) — unified engine. Config: {msgsId, btnId, inputId, q, history, systemPrompt, chipClass, chipAction, citeRegistry, fnSectionId}. Thin wrappers: sendSnapMsg(), sendStory(), sendAskMessage(). CSS: .td-chat BEM component with __label, __hints, __chip, __msgs, __input-row, __input. No avatars — user bubble right-aligned, response prose left-aligned, matching story typography..story-update + type modifiers (.update-breaking, .update-revised). Data: updates:[{type, time, content}]..unread-dot + .is-read on card + .has-update + .updated-badge. Managed by readStories Set and markAsRead().ICONS object mapping names to SVG paths + icon(name, class) render function. No CDN dependency. 31 UI icons + 8 brand logos (official colors, exempt from token system). Sizing: .icon-xs (11px), .icon-sm (16px), .icon (20px), .icon-lg (24px). Rule: icons ~1.2× companion text size..btn base reset + types: .btn-primary (filled), .btn-ghost/.btn-ghost-dark (transparent), .btn-chip/.btn-chip-dark (pill). Icon sizing: .btn-icon (34px), .btn-icon-sm (28px), .btn-icon-lg (40px). Modifier: .btn-danger (red variant).These utility classes are implemented and applied:
| Class | What it does | Replaces |
|---|---|---|
.label-mono | font-mono, size-nano, letter-spacing 3px, uppercase, ink-faint | 11+ inline declarations of the same pattern |
.label-mono-sm | Same but size 8px, spacing 1.5px | Tab labels, toggle labels |
.text-prose | font-prose, size-body, lh-prose, ink-mid, measure-prose auto-centered | Repeated prose container declarations |
Ordered by impact — what to consolidate first:
sendChatMessage(config) with thin wrappers..overlay + .overlay-box with --wide/--narrow variants..bar-dark base class..label-mono applied via grouped selector to 20 selectors.Every story follows this shape. All fields are required unless marked optional.
| Field | Type | Notes |
|---|---|---|
| img | string | Unsplash URL with w/h/fit params |
| imgCaption | string | Photo credit line |
| badge | string | CSS class: b-breaking, b-markets, b-tech, etc. |
| badgeText | string | Display text for badge row |
| headline | string | Georgia headline — tonal register must match content |
| byline | string | Format: "[N] sources · [status] · Last updated [time]" |
| prose | HTML string | Main body. Citations as [Source, Date]. |
| quote | {text, attribution} | Optional. Empty text = no quote rendered. |
| prose2 | HTML string | Optional. Continuation after quote. |
| known | string[] | Confirmed facts list |
| unknown | string[] | Not yet known list |
| cov | {l, c, r} | Coverage distribution percentages. Must sum to 100. |
| sources | string[] | All source names |
| sourceLinks | object | Optional. Maps citation text → URL |
| updates | array | Optional. [{type, time, content}]. Types: breaking, new, revised |
| Field | Type | Notes |
|---|---|---|
| platform | string | Display name for source |
| src | string | Source attribution line |
| vel | HTML string | Velocity badge HTML |
| title | string | Headline |
| text | HTML string | Body. Citations as [Source, Date] or sup.cite wrapped. |
| askQ | string | Pre-seeded chat placeholder question |
| sourceLinks | object | Optional. Maps citation text → URL |
| Thing | Convention | Example |
|---|---|---|
| CSS variables | Lowercase, hyphenated | --ink-mid, --shadow-lg |
| CSS classes | Lowercase, hyphenated. BEM-lite for modifiers. | .story-card, .update-breaking |
| JS functions | camelCase | sendChatMessage, markAsRead |
| JS constants | UPPER_SNAKE | STORIES, SNAPS, AI_CONTEXT |
| HTML IDs | Lowercase, hyphenated | ask-chat-input, card-0 |
| Files | the_dossyer_v00.XX.html | Two-digit minor, dots preserved |
Accessibility is a system, not a checklist. Build reusable utilities: a11yInteractive(selector) for keyboard support, a11yModalOpen/Close for focus management, announceToSR() for screen reader announcements. One function call covers a new element.
max-height:0, display:none, off-screen via transform), its interactive children need tabindex="-1". Use a11yUpdateCollapsed(containerSelector, isExpanded) and call it from every toggle function.box-shadow inside overflow:hidden containers — outline gets clipped.a11yInteractive runs at page load. If a modal injects new interactive HTML (e.g., footnotes toggle), re-run a11yInteractive on the new selector after injection.<button> gets focus, keyboard, ARIA for free. Only use a11yInteractive on non-semantic elements that can't be changed to buttons.No text below 10px. The font size floor is --size-micro: 10px. Every size token from micro through body must be ≥ 10px. If something needs to be smaller, it probably doesn't need to be text.
:root — don't add font-size overrides to individual elements. The whole point of tokens is that one change fixes everything.Two components doing one job is a system problem. If two UI elements convey the same concept ("what's new"), they need to be one system with states, not two independent components. Rule 0 applies to UX patterns, not just code.
--elevation-raised. No raw var(--shadow) on hover states — use semantic elevation tokens.