DESIGN.md@code/emotional-recap · git:20260513.4b1537f · 2026-05-13 · sha256 5f6c307e6e1eb57b
DESIGN.md@code/emotional-recap git:20260513.4b1537fA
Immutable. This exact content is served forever at /api/v1/blob/5f6c307e6e1eb57b.
# /emotional-recap design
This file is the **renderer contract** for the emotional-recap skill. Every HTML, SVG, or Markdown artefact produced by the skill must conform to the tokens, principles, and guarantees below.
It is distinct from `preferences.md`. DESIGN.md is the skill's aesthetic contract (versioned, identical for every user). `preferences.md` is the user's runtime configuration (per-machine, free-edit). On conflict: **DESIGN.md wins for renderer guarantees (a11y, anti-patterns, token shape); preferences wins for user-tunable knobs** (tone of voice, citation depth, framework choice).
Updated after the 2026-05-13 audit. See `feedback-journal.md` for the audit signals folded in.
---
## 1. Aesthetic direction
The report is a **personal editorial spread**, not a SaaS analytics dashboard. It is intended to be re-read, screenshotted, possibly printed. It speaks to one reader (the user themselves) about a sensitive subject (their emotional state), and so the visual language must be:
- **Quietly confident.** Generous whitespace. Few but decisive moves. The hero is the only loud thing on the page.
- **Editorial, not corporate.** Closer to a long-read magazine page (The Atlantic, MIT Tech Review, The Browser) than a Mixpanel dashboard.
- **Warm without being cute.** Paper-warm backgrounds, sage / gold / indigo / rose accent palette. No emoji headers, no gamification, no fitness-app energy.
- **Designed to be re-read.** Type for sustained attention. Citations and caveats are first-class, not footnotes.
References (look-and-feel only — do not literally copy):
- Edward Tufte's small-multiples plates
- Jonathan Hoefler's typographic specimens
- Massimo Vignelli's NYC subway diagrams (restraint, hierarchy)
- The Browser newsletter's typesetting
- 1960s scientific monograph plates (data + serif body + clear caveats)
This is NOT:
- A productivity dashboard ("you completed 14 sprints!")
- A mood-tracker app ("track your streak!")
- An analytics report ("KPI ↑ 4.2% MoM")
- A diagnostic tool ("you scored 67/100 on emotional regulation")
---
## 2. Type system
### Family
| Role | Family | Fallback |
|----------|---------------------------------------------|-----------------------------------------------------------------------------------------------------|
| Display | **Fraunces** (variable serif, opsz + wght) | `"Iowan Old Style", "Charter", Georgia, serif` |
| Body | **IBM Plex Sans** | `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif` |
| Mono | **IBM Plex Mono** | `ui-monospace, "SF Mono", Menlo, monospace` |
| Editorial italic (word weather, pull-quotes) | **Fraunces italic, opsz 144** | Fraunces fallback above with italic + 600 weight |
Reasoning: per skill-creator manifest item 30 and frontend-design's "BOLD aesthetic direction," Inter / Roboto / Arial / system-only stacks are slop tells. Fraunces (display) + Plex (body) is a distinctive, editorial pairing. Both are open-source and embeddable.
**Self-containment policy**: if the report is generated with `embed_fonts: true` (a new preference, default `false`), inline WOFF2 data-URIs for Fraunces and Plex into the HTML `<style>` block. When `false`, fall back to the system stack — the renderer must verify visual hierarchy still reads correctly without the custom fonts.
### Scale (rem-based, root = 16px)
| Token | Size | Weight | Line | Letter-spacing | Use |
|------------------|-----------|--------|------|----------------|--------------------------------------------------------|
| `--t-hero` | 5.5rem | 300 | 0.9 | -0.04em | Hero valence number (one per report) |
| `--t-display` | 1.625rem | 400 | 1.3 | -0.01em | Vibe sentence under hero |
| `--t-h2` | 0.75rem | 600 | 1.2 | 0.18em UPPER | Section heads |
| `--t-h3` | 1.0625rem | 600 | 1.4 | -0.005em | Subsection / pattern titles |
| `--t-body` | 1rem | 400 | 1.65 | 0 | Paragraphs |
| `--t-meta` | 0.875rem | 400 | 1.5 | 0 | Captions, range labels, project meta |
| `--t-micro` | 0.75rem | 500 | 1.4 | 0.08em | Stat keys, eyebrows, axis labels |
| `--t-cite` | 0.8125rem | 400 | 1.55 | 0 | Citations list |
Tabular figures (`font-feature-settings: "tnum"`) wherever a numeric value lives — stat tiles, sparkline ranges, percentages, project metrics.
**Removed from the previous design** (audit fixes M2, M8):
- `font-feature-settings: "ss01", "cv11"` — only applies to Inter, dead code on Fraunces / Plex.
- `-webkit-font-smoothing: antialiased` — harms low-vision readers; let the browser handle font rendering.
---
## 3. Colour tokens
All text-bearing tokens must clear **WCAG AA (4.5:1 contrast)** against their default background. Decorative-only tokens (axis grid, faint markers) are allowed lower contrast but must be marked `aria-hidden="true"`.
### Light mode
```css
--bg: #f5f1ea; /* warm paper, was #faf8f5 */
--bg-elev: #ffffff; /* card surface */
--ink: #1c1c24; /* body text */
--ink-soft: #4a4a55; /* muted text — AA: 7.2:1 on --bg (was #6a6a78, 4.6:1) */
--ink-faint: #6e6c72; /* decorative only, aria-hidden */
--rule: #e0dad0; /* hairlines, borders */
--grid: #e8e4db; /* chart background grid */
```
### Quadrant palette (Russell circumplex — used everywhere)
Each quadrant gets three tones: a **fill** (for backgrounds, low-contrast), an **ink** (AA-compliant text), and a **line** (chart strokes, primary accent).
```css
/* Q1 — high arousal, positive valence — "excited / engaged" */
--q1-fill: #f0d29a; --q1-ink: #6b4a14; --q1-line: #b08338;
/* Q2 — high arousal, negative valence — "tense / frustrated" */
--q2-fill: #f0c8c8; --q2-ink: #882727; --q2-line: #b85959;
/* Q3 — low arousal, negative valence — "tired / low" */
--q3-fill: #d8d6e3; --q3-ink: #3f3f5a; --q3-line: #6b6b8a;
/* Q4 — low arousal, positive valence — "calm / focused" */
--q4-fill: #cfdcd1; --q4-ink: #2f4f3a; --q4-line: #5d7c66;
```
All `--q?-ink` tokens are verified ≥4.5:1 against `--bg`. Use these for any quadrant-coloured text (e.g. bright-spots heading, word-weather word polarity). The previous palette failed AA — fixed per audit H3.
### Dark mode
```css
--bg: #14141b;
--bg-elev: #1d1d27;
--ink: #ece9e2;
--ink-soft: #b5b3bc;
--ink-faint: #6a6872;
--rule: #2a2a37;
--grid: #232330;
/* Quadrant inks brighten; fills stay close-to-bg for ambient washes */
--q1-fill: #3a2e16; --q1-ink: #e8c277; --q1-line: #d4a04a;
--q2-fill: #3a1e1e; --q2-ink: #e89a9a; --q2-line: #c66b6b;
--q3-fill: #25253a; --q3-ink: #b0b0d0; --q3-line: #8a8aaa;
--q4-fill: #1f3025; --q4-ink: #9bb59f; --q4-line: #7a9580;
```
Dark-mode SVG quadrant fills use these tokens (not hard-coded hex with `fill-opacity`). Per audit M3, SVG `<rect>` elements use `fill="var(--q1-fill)"` etc. so the chart actually adapts.
### `color-mix()` policy (theme-parity rule)
Per audit M1: `color-mix()` is **forbidden in emitted CSS**. But the obvious-looking fix — "pre-compute the mix to a hex literal and bake it into the rule" — silently breaks dark mode (1225 bug). Hex literals don't change with `prefers-color-scheme`, so any element whose background is a baked-in light-mode mix becomes broken when a dark-mode reader opens the file.
**Mandatory replacement pattern**: every renderer pre-compute generates **both** light and dark variants and emits them as theme-scoped CSS custom properties. UI selectors then use `var(--*)`, and the cascade picks the right value per theme.
```css
:root {
--hl-card-bg: #ebebe3; /* renderer pre-computed: mix(q4-fill 60%, bg-elev) for LIGHT */
}
@media (prefers-color-scheme: dark) {
:root {
--hl-card-bg: #1f2a23; /* renderer pre-computed: same mix for DARK tokens */
}
}
.hl-card {
background: var(--hl-card-bg); /* NEVER background: #ebebe3 — that locks the theme */
}
```
**Forbidden in emitted CSS**: any hex literal as a background/colour/border of an element that holds text or visible surface. All such surfaces must resolve through a `var(--*)` whose value is theme-scoped. Renderer self-audit item 15 enforces this with a regex grep.
This rule applies to every renderer pre-compute. There are no exceptions for "small" mixes like a 5% tint.
---
## 4. Layout
### Information architecture (the fold-by-fold contract)
The report reads top-to-bottom in **four density zones**: brief → highlights → insights → raw data. The intent is that a reader who only sees the top 20% of the document gets the headline; a reader who scrolls one screen gets the patterns; a reader who scrolls to the bottom gets the data plates. Citations and caveats live at the very bottom.
| Zone | § | Section | Density | Visual shape |
|----------------|----|--------------------|---------|---------------------------------------------------------------|
| **Brief** | 01 | At a glance | High | Hero number + scale legend + vibe sentence + 4 stat tiles + day-badges row + quadrant chip |
| **Highlights** | 02 | Highlights | High | Compact grid of 3 "bright spot" chip-cards (quotes + date) |
| **Insights** | 03 | Insights | Medium | **Horizontal carousel** of pattern cards (observation / science / reframe). 3–7 cards, snap-scroll, no JS |
| **Anchors** | 04 | Worth trying | Medium | Numbered list of 2–4 evidence-informed micro-experiments, each anchored to a real citation. **Not prescriptions** — the framing is "research suggests X, your felt sense leads." |
| **Raw data** | 05 | Emotional arc | Low | Twin sparklines + arc narrative |
| | 06 | Affect map | Low | Russell scatter (jitter + density-bin fallback) |
| | 07 | Dominant emotions | Low | Plutchik wheel + ranked bars + quoted examples |
| | 08 | Day rhythm | Low | 24h focusable strip |
| | 09 | By project | Low | Diverging bars |
| | 10 | Word weather | Low | Typographic vocabulary cloud |
| **Reference** | 11 | Citations | Ref | Bibliographic list |
| | 12 | Caveats | Ref | Disclaimers |
The reordering — moving the engaging items (Highlights, Insights, Worth trying) above the heavy charts — is signalled by the user (2026-05-13-1150 feedback; reinforced 1209 with "science-based recommended actions") and now enforced by this DESIGN.md. The renderer must emit sections in this order. Reports that bury the carousel or actions below charts violate the contract.
**Twelve sections, numbered 01–12 contiguously.** The previous 01–11 contract was widened by one to admit Worth trying. No gaps; the `.num` span is `aria-hidden="true"` on every `h2`.
**The "anchors" zone is supportive, never directive.** Per the skill's first Principle ("supportive, not diagnostic"), the Worth trying section must NEVER read as a prescription. It frames each option as research-anchored, the user-led ("worth trying if it fits"), and is explicitly skippable. See §6 anti-patterns for the language guardrails.
### Geometry & rhythm
- **Max content width**: 720px.
- **Side padding**: 24px mobile, 32px desktop.
- **Vertical rhythm**: 56px between major sections; 24px within a section; 12px between paragraph and chart.
- **Section dividers**: top border on `h2`, not card chrome on every section. Cards are reserved for charts that *need* a contained surface.
- **Section numbering**: every `h2` carries a numeric prefix `<span class="num" aria-hidden="true">01</span>`. **No gaps in numbering** (per audit H6). Eleven sections, numbered 01–11.
- **Hero asymmetry**: the hero number may bleed 24px to the left of the body column on viewports ≥820px. This is the **one spatial moment of asymmetry**; everything else is structured. Per frontend-design guidance: don't sprinkle asymmetry, commit to one moment.
- **No nested cards.** Anti-pattern. If a section needs a card AND a chart, the chart is unboxed inside the section card; do not wrap the chart in a second card.
- **Carousel scroll**: section 03 is the one horizontal-scroll moment. The rest of the document scrolls vertically only. Per `prefers-reduced-motion: reduce`, the carousel still scrolls but without smooth-scroll behaviour.
---
## 5. Visualisation principles
### Universal rules
1. **Colour is never the sole encoding.** Always pair with size, shape, position, weight, or texture (audit H4).
2. **Dense or uniform data → degrade gracefully.** Detect at render time:
- If ≥85% of scatter points fall in the same 20×20px cell, **switch from per-dot rendering to a 2D heatmap grid** (each cell shaded by density, with a small legend).
- If `n_days < 2` or all values identical, render text inside the SVG: `<text x="…" y="…" fill="var(--ink-soft)">Not enough data yet — check back after another day.</text>`
3. **Tooltips must be focusable AND visible.** No `title=""`-only tooltips (audit H2). Hour cells, project bars, scatter dots become `<g tabindex="0" role="button" aria-label="…">` with inline-revealed details on focus/hover.
4. **Every SVG has** (audit H1):
- `role="img"`
- `<title>` (short label)
- `<desc>` (1–2 sentence description of the data shown)
- `aria-describedby` if the surrounding caption is the description
5. **Per-section visual identity** (audit M9). Each chart binds to its semantically appropriate accent — valence sparkline uses `--q-by-dominant-quadrant`; the arousal sparkline uses `--ink-soft`; project bars use `--q4-line` for positive, `--q2-line` for negative. Don't paint everything sage.
6. **Touch targets** (audit M4). Interactive chart elements (hour cells, project rows, focusable dots) are minimum 44×44px on touch viewports. On desktop, hour cells may be smaller; on `(pointer: coarse)` they expand or collapse the strip into a 2-row 12-hour format.
7. **Hero number context** (audit M7). The hero valence number must be paired with a micro-legend immediately below: `−1 negative · 0 neutral · +1 positive`. Non-negotiable.
### Russell scatter — special rules
This is the chart the audit broke. Specific fixes:
1. **Jitter every dot by ±6px in both axes** (audit C1). If two dots share the same `(valence, arousal)`, they will still spread visibly.
2. **Cap render count at 200 dots**; if more, deterministically subsample (every Nth message) and emit a caption `"N of M messages plotted"`.
3. **Density-bin mode**: if after jitter ≥85% of dots still cluster in one 32×32px cell, switch encoding entirely. Emit a 10×10 grid heatmap with cell opacity = `messages_in_cell / max_cell_count`. Show the centroid + 3 quartile rings instead of individual dots.
4. **Per-message quadrant tint**: each dot uses the line colour of *its own* quadrant — Q1 dots are `--q1-line`, Q2 dots `--q2-line`, etc. This adds a non-colour signal? No — it's *more* colour but tied to position, so colour reinforces position rather than being the sole channel. Combined with size variation (r=2 for low confidence, r=3 for high), this gives two redundant signals.
### Sparklines — special rules
- Centred zero-line for valence; bottom-anchored for arousal.
- Single accent colour (the dominant-quadrant line). Data points marked with `r=3` filled circles + invisible 12px hit area for focus.
- Range label appears below the sparkline with explicit axis hint: `Valence range: +0.01 → +0.08 · scale −1 to +1`. The current report's `+0.01 → +0.08` without scale is half a label.
### Day rhythm strip — special rules
- 24 cells, each `<g tabindex="0">` with `<title>` (short) and inline-revealed `<text>` on focus (long).
- On `prefers-reduced-motion: no-preference`, focus reveals an animated underline; otherwise instant.
- Empty hours (no data) render as `--grid` with `aria-label="0 messages"`; never as the same colour as a valence reading.
- Below the strip: textual summary of peak / dip hours with sample-size warning if either band has fewer than 5 messages.
### Plutchik wheel — special rules
- 8 wedges with opacity scaled by emotion frequency. Add a redundant ring of small filled circles outside the wheel, sized by frequency, so size is a non-colour signal of dominance.
- Wedge labels (joy, ant, …) are `--t-micro` placed outside the perimeter; do not abbreviate to 3-letter codes if the perimeter has room — readability over decoration.
### Word weather — special rules
- Polarity carried by **both colour AND weight**:
- Positive: `--q4-ink` + weight 500
- Negative: `--q2-ink` + weight 500 + underline (`text-decoration-thickness: 1px; text-decoration-color: currentColor`)
- Neutral: `--ink` + weight 400
- Per audit H4, the underline on negative words provides a non-colour channel.
- Size range: 14px → 36px linear by log-frequency.
---
## 6. Anti-patterns to avoid
Universal slop list (from skill-creator manifest item 30):
- Inter / Roboto / Arial / `system-ui` as primary font choice — already replaced (§2).
- Purple gradients on white — n/a; palette is sage/gold/indigo/rose by design.
- Glassmorphism / backdrop-blur for decoration — none used; do not add.
- Hero metrics without scale legend — fixed (§5 universal rule 7).
- Color-only encoding — fixed (§5 universal rule 1).
- `title=""`-only tooltips — fixed (§5 universal rule 3).
- Nested cards — fixed (§4).
- Bounce / spring easing on a wellness report — none used; do not add.
- `font-feature-settings: "ss01"` on fallback fonts — removed (§2).
- `-webkit-font-smoothing: antialiased` — removed (§2).
Skill-specific don'ts:
- **Don't gamify.** No streaks, no scores, no "level up" language. The data is observational; framing is editorial.
- **Don't emoji-prefix section heads.** Eyebrow numbering carries the visual signal.
- **Don't render the whole report as one giant card.** Long unboxed flow with selective card surfaces around charts.
- **Don't surface client / brand names in quoted text** (audit C2). The anonymiser is mandatory; see §7.
- **Don't say "you should"** anywhere — patterns end with a question, the Worth-trying section ends with an _invitation_ anchored to research. The skill's first Principle (supportive, not diagnostic) is encoded in the language of every section.
Language guardrails for the **Worth trying** section (§04). The section is the *only* place the report makes forward-looking suggestions, and it has the tightest constraints:
- **Never** use: "you should", "you must", "do X", "stop doing Y", "this will fix", "this is the answer".
- **Always** use: "research suggests…", "studies indicate…", "worth trying if…", "an option anchored in…", "this is one experiment, not a prescription".
- **Always cite.** Every Worth-trying item carries one specific real citation from `reference/citations.md`. If a candidate suggestion has no citation in the file, **drop the suggestion** — do not generate folk-psychology advice and then fabricate a source.
- **Never medicalise.** No "treatment", "therapy", "intervention", "symptom", "disorder". This is text-based pattern observation; the language stays in the editorial register.
- **Limit to 2–4 items per report.** A long list reads as a prescription pad. Two well-grounded options beat six generic ones.
- **Include the "skip with no judgement" line** in the section footer: _"None of these are required. Skip any that don't fit; your felt sense leads."_
- **Generate suggestions only from the patterns detected in this run.** No generic wellness tips ("drink more water"). Every Worth-trying item must be a direct response to a pattern flagged in §03 Insights, with a paper from the citation file backing it.
---
## 7. Renderer contract (hard guarantees)
The renderer **must** satisfy all of the following before writing an HTML file. If any check fails, abort the write and report the specific failure to the user.
### Accessibility
1. Every `<svg>` has `role="img"`, `<title>`, `<desc>`, and `aria-labelledby` or `aria-describedby` wiring those into the surrounding context.
2. Every decorative inline element (section number `.num`, swatch `.dot`, the `★` glyph) has `aria-hidden="true"`.
3. All text-bearing tokens used clear WCAG AA (≥4.5:1) against the default background. Renderer verifies before emit; fails closed.
4. No `title=""`-only tooltips. Any per-element hidden detail is exposed via focusable container with inline-revealed text on focus and hover.
5. Heading hierarchy is contiguous: H1 once, H2 numbered 01–11 with no gaps, H3 within sections.
6. Touch targets for interactive chart elements ≥44×44px on `(pointer: coarse)`.
### Privacy / anonymisation (audit C2)
The anonymiser runs as a **three-layer pipeline** in this order:
1. **Deny-list** (loaded from `~/.claude/skills/emotional-recap/anonymise.deny` + project memory): exact-match strings to redact and replace with `<client>`, `<person>`, `<project>`, or category tag. Seeded with known client/brand names from the user's memory (e.g. `verizon`, `unilever`, `morgan`).
2. **Heuristic strip**: tokens matching `[A-Z][a-z]{2,}` that aren't in the English dictionary OR aren't in a small whitelist of common words → redact to `<name>`.
3. **Structural strip** (already in place): file paths, URLs, secret-shaped strings, code blocks → paraphrase.
If a quote would be reduced to <40% of its original length after anonymisation, drop the quote and use a paraphrase ("a debugging exchange about the build pipeline").
### Browser compatibility
1. `color-mix()` is **forbidden in emitted CSS** (audit M1). Renderer pre-computes for both themes and emits theme-scoped CSS custom properties — never a baked hex on a rule (1225 bug fix; see §3 "color-mix policy").
2. No `prefers-color-scheme` dependency for critical contrast — both light and dark mode token sets must pass AA independently, and the renderer self-audit must verify this independently per theme (item 16).
3. Sized hero (`5.5rem`) must remain readable at viewports ≥320px wide. On `(max-width: 360px)`, hero size drops to `3.75rem`.
### Visual integrity
1. Russell scatter overplotting guard active (audit C1): jitter + cap + density-bin mode.
2. Sparse-data fallback active for sparklines, scatter, rhythm strip.
3. SVG fills bound to CSS custom properties, never hard-coded hex (audit M3).
4. No reliance on inline `style=""` for renderer-critical layout (audit L7) — utility classes only. Inline `style=""` allowed for runtime data values (e.g. `style="width: 60%"` for a bar fill) but not for layout / colour decisions.
### Provenance (every chart grounded in a real citation)
Per the 2026-05-13-1209 feedback _"for all charts and data grounded in truth"_: every chart-bearing section emits a `<p class="chart-source">` caption directly under the SVG, naming the framework or method and the real citation from `reference/citations.md`. The renderer fails closed if any chart is missing its source line.
| Section | Required caption |
|--------------------|--------------------------------------------------------------------------------------------------------|
| 05 Emotional arc | _Method: daily mean valence and arousal across the window. Scale: −1 to +1 valence, 0 to 1 arousal._ |
| 06 Affect map | _Framework: Russell circumplex of affect (1980)._ |
| 07 Dominant emotions | _Framework: Plutchik wheel of emotions (1980). Lexicon: NRC Emotion (Mohammad & Turney, 2013)._ |
| 08 Day rhythm | _Method: hourly mean valence aggregation. Circadian framing per Walker (2017); see also Yoo et al. (2007)._ |
| 09 By project | _Method: per-project valence mean, in-window. Opacity ∝ message count._ |
| 10 Word weather | _Lexicon: NRC Emotion (Mohammad & Turney, 2013). Linguistic markers: LIWC-style (Tausczik & Pennebaker, 2010)._ |
| 04 Worth trying | Each item carries its own inline citation (Anchor: <ref>). Section footer cites the same papers used for that run's patterns. |
### Renderer self-audit
Before writing the file, the renderer prints a one-line self-audit report:
```
Renderer audit: 16/16 contract items satisfied. Report: <path>.
```
The audit has grown over time as bugs surfaced:
- **1209**: added 13 (every chart-bearing section emits a `chart-source` caption with a real citation) and 14 (Worth trying section integrity — 2–4 items, each cited, no prescriptive language).
- **1225 (this update)**: added 15 (**theme-parity check** — no hex literal appears as `background`, `color`, or `border-color` on a text-bearing rule; every such surface must resolve via a `var(--*)`. Regex-grep the emitted CSS to enforce.) and 16 (**per-theme contrast check** — for every text-on-surface pairing, compute the contrast ratio independently in light and in dark mode tokens; fail if either ratio < 4.5:1).
If any item fails, print which one and refuse to write. The user can override with `--force` but the default is fail-closed.
The audit is not a substitute for visual inspection. If a render passes 16/16 but still _looks_ wrong, the audit needs another item — file a journal entry and grow the checklist. Quality is the union of "passes the rules" + "looks right in the actual theme the reader uses."
---
## 8. Component patterns
Each component below is the canonical HTML snippet. **All colour and font references use tokens.** Renderers must not deviate without updating this section.
### Hero
```html
<header class="hero" role="banner">
<div class="eyebrow">Emotional recap · {window} · {scope}</div>
<div class="hero-row">
<div>
<div class="hero-num" aria-label="Mean valence {valence_value} on a scale of negative one to positive one">
<span class="sign" aria-hidden="true">{sign}</span>{valence_abs}
</div>
<div class="hero-legend" aria-hidden="true">−1 negative · 0 neutral · +1 positive</div>
<div class="hero-meta">Mean valence · arousal {arousal_mean}</div>
</div>
<div class="hero-chip">
<span class="dot" aria-hidden="true" style="background: var(--{quadrant}-line);"></span>
{quadrant_label}
</div>
</div>
<p class="vibe">{vibe_one_liner}</p>
<p class="timestamp"><time datetime="{iso}">{human_timestamp}</time></p>
</header>
```
### Stats grid + day badges (section 01 — At a glance)
```html
<section aria-labelledby="s01">
<h2 id="s01"><span class="num" aria-hidden="true">01</span> At a glance</h2>
<p class="brief">{2-3 sentence summary — the headline of the whole report.}</p>
<div class="stats">
<div class="stat"><div class="k">Conversations</div><div class="v">{n}</div></div>
<!-- repeat 4 -->
</div>
<div class="badges" role="list" aria-label="Daily mood">
<div class="badge" role="listitem" style="--badge-color: var(--{q}-line);">
<div class="swatch" aria-hidden="true"></div>
<div class="day">{day_short}</div>
<div class="day-val">{valence}</div>
</div>
<!-- repeat per day -->
</div>
</section>
```
The 2–3 sentence brief is **the most important addition from the 2026-05-13-1150 feedback**: a top-of-document executive summary. Renderer generates this from the patterns + vibe + dominant quadrant. Keep it to <60 words.
### Highlights strip (section 02 — was Bright spots)
Compact chip-cards moved from the bottom of the report to the top fold. Three to five quotes maximum. Each card is small, scannable, and self-contained.
**Visual weight policy (1225 bug fix)**: highlight cards use the **same neutral surface as insight cards** (`--bg-elev`) with a 3px `--q4-line` left-border accent. No full tint. Heavy tinted blocks were dominating the dark-mode page and hiding the quotes inside them (light text on a light pre-computed mix). The lighter treatment is consistent with the rest of the report's card vocabulary and works in both themes.
```html
<section aria-labelledby="s02">
<h2 id="s02"><span class="num" aria-hidden="true">02</span> Highlights</h2>
<ul class="highlights" role="list">
<li class="hl-card" aria-label="Bright spot from {date}">
<span class="hl-glyph" aria-hidden="true">★</span>
<div class="hl-body">
<p class="hl-quote">"{anonymised excerpt}"</p>
<p class="hl-meta">{date}</p>
</div>
</li>
<!-- 2-4 more -->
</ul>
<p class="t-meta">Quick approvals compound — Fredrickson (2001) on broaden-and-build.</p>
</section>
```
CSS — all surfaces resolve through `var(--*)`, never a baked hex:
```css
.highlights {
list-style: none; padding: 0; margin: 16px 0 8px;
display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 12px;
}
.hl-card {
display: flex; gap: 12px; align-items: flex-start;
background: var(--bg-elev); /* same surface as insight cards */
border: 1px solid var(--rule);
border-left: 3px solid var(--q4-line); /* accent stripe */
border-radius: 10px;
padding: 14px 16px;
}
.hl-glyph {
color: var(--q4-line); /* line token, not fill — always visible accent */
font-size: 0.95rem; line-height: 1.4;
flex-shrink: 0; margin-top: 1px;
}
.hl-body { min-width: 0; flex: 1; }
.hl-quote {
font-family: var(--font-display);
font-style: italic;
font-size: 0.9375rem; line-height: 1.45;
color: var(--ink); /* ink on bg-elev → AA in both themes */
margin: 0;
}
.hl-meta {
font-family: var(--font-mono);
font-size: 0.6875rem; letter-spacing: 0.04em;
color: var(--ink-soft);
margin: 6px 0 0;
}
```
**Empty-content guard** (1225-adjacent): if anonymisation reduces a candidate quote below 3 visible words, the entire card is dropped (not rendered empty). If fewer than 3 highlight quotes survive, render a single explanatory chip: _"Quiet window — no standout positive markers this run. Highlights aren't every-window guaranteed."_ Renderer self-audit item 14 (Worth trying integrity) is paralleled here implicitly — don't render shells.
The compact grid pattern (`auto-fit minmax(240px, 1fr)`) gives one column on mobile, two on tablet, two-three on wide desktop. No JS needed.
### Chart source caption (universal — applies to every chart-bearing section)
Every chart-bearing section (§05 Arc, §06 Affect map, §07 Dominant emotions, §08 Day rhythm, §09 By project, §10 Word weather) emits this footer immediately after the SVG / chart:
```html
<p class="chart-source">
<span class="chart-source-label" aria-hidden="true">Source</span>
{framework or method} <span class="chart-source-cite">— {citation}</span>
</p>
```
```css
.chart-source {
font-size: 0.75rem; line-height: 1.5;
color: var(--ink-soft);
margin: 10px 0 0;
display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap;
border-top: 1px dashed var(--rule); padding-top: 8px;
}
.chart-source-label {
font-size: 0.625rem; letter-spacing: 0.16em; text-transform: uppercase;
color: var(--ink-faint); font-weight: 600;
font-family: var(--font-mono);
flex-shrink: 0;
}
.chart-source-cite { font-style: italic; color: var(--ink-soft); }
```
The exact caption text for each section is defined in DESIGN.md §7 "Provenance" table. Renderer must emit the caption or the self-audit fails (item 13).
### Worth trying (section 04 — evidence-informed options)
A numbered list of 2–4 micro-experiments, each tied to a real citation. Visual identity uses the Q1 (anticipation / forward-looking) token palette to distinguish from the Insights carousel (which uses the dominant-quadrant accent).
```html
<section aria-labelledby="s04">
<h2 id="s04"><span class="num" aria-hidden="true">04</span> Worth trying</h2>
<p class="t-meta">Evidence-informed options, one per pattern above. None of these are required — your felt sense leads.</p>
<ol class="actions">
<li class="action">
<div class="action-title">{title}</div>
<p class="action-body">{What the option is + why it might fit the pattern. Phrased as "research suggests" or "studies indicate", never "you should". 1–2 sentences.}</p>
<p class="action-anchor">
<span class="action-anchor-label" aria-hidden="true">Anchor</span>
{full inline citation, e.g. "Ariga & Lleras (2011) — brief mental breaks restore vigilance"}
</p>
</li>
<!-- 1-3 more action items, maximum 4 total -->
</ol>
<p class="t-meta action-footer">Anchored in the patterns above and the citations at §11. Skip any that don't fit.</p>
</section>
```
```css
.actions {
list-style: none; padding: 0; margin: 16px 0;
display: flex; flex-direction: column; gap: 12px;
counter-reset: action-counter;
}
.action {
counter-increment: action-counter;
position: relative;
padding: 16px 20px 16px 56px;
background: var(--bg-elev);
border: 1px solid var(--rule);
border-left: 3px solid var(--q1-line);
border-radius: 12px;
}
.action::before {
content: counter(action-counter, decimal-leading-zero);
position: absolute; left: 18px; top: 16px;
font-family: var(--font-mono);
font-size: 0.8125rem; letter-spacing: 0.04em;
color: var(--q1-ink); font-weight: 600;
}
.action-title { font-weight: 600; font-size: 1rem; margin: 0 0 6px; line-height: 1.35; }
.action-body { font-size: 0.9375rem; margin: 0 0 10px; }
.action-anchor {
font-size: 0.8125rem; line-height: 1.55;
color: var(--ink-soft);
margin: 0; padding-top: 10px;
border-top: 1px dashed var(--rule);
display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap;
}
.action-anchor-label {
font-size: 0.6875rem; letter-spacing: 0.14em; text-transform: uppercase;
color: var(--q1-line); font-weight: 600;
font-family: var(--font-mono);
flex-shrink: 0;
}
.action-footer { margin-top: 12px; font-style: italic; }
```
**Renderer rules for Worth trying**:
1. **Match items to detected patterns 1:1 where possible.** If §03 Insights surfaced 4 patterns, generate 2–4 actions that each respond to one of those patterns. Don't generate generic wellness advice; don't action a pattern that wasn't detected.
2. **Drop suggestions without citations.** If no real citation in `reference/citations.md` supports a candidate action, drop the action entirely. Never fabricate a reference.
3. **Citation diversity.** Don't repeat the same citation across all action items in one report. If two actions point to Csikszentmihalyi (1990), reframe or drop one.
4. **Language audit pre-write.** Scan all action-title and action-body text against the forbidden phrases in §6 ("you should", "you must", "stop", "fix", "treatment", "therapy", "intervention", "symptom"). Any match → refuse to write.
5. **2–4 items maximum.** Less is more. A long list reads prescriptive.
6. **Empty Worth-trying is allowed.** If the patterns this run don't admit a research-backed action with a real citation, render the section header with an `<p class="t-meta">No standout options this window — the patterns above weren't ones with strong evidence-based experiments. Skip ahead.</p>` and move on. Don't fill the slot with weak suggestions.
### Insights carousel (section 03 — was Patterns I noticed)
The middle-of-report carousel. Horizontal scroll with CSS snap; works without JS. Each card is fully accessible in source order — screen readers and keyboard users get the full list linearly.
```html
<section aria-labelledby="s03">
<h2 id="s03"><span class="num" aria-hidden="true">03</span> Insights</h2>
<p class="t-meta">{n} pattern{s} from the window. Scroll →</p>
<div class="carousel" role="region" aria-label="Pattern insights">
<article class="insight-card" aria-labelledby="i1">
<div class="insight-num" aria-hidden="true">01 / {n}</div>
<h3 id="i1">{pattern_title}</h3>
<p class="insight-observation">{observation}</p>
<details class="insight-science">
<summary>Why this matters</summary>
<p>{science_with_inline_citation}</p>
</details>
<p class="insight-reframe"><span class="reframe-label">Worth asking</span> {reframe_question}</p>
</article>
<!-- repeat -->
</div>
</section>
```
CSS:
```css
.carousel {
display: flex; gap: 16px;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
scrollbar-width: thin;
scrollbar-color: var(--rule) transparent;
padding: 4px 0 16px;
margin: 12px -24px 0; /* full-bleed on mobile */
padding-left: 24px; padding-right: 24px;
-webkit-overflow-scrolling: touch;
/* edge-fade affordance */
mask-image: linear-gradient(to right, transparent 0, #000 24px, #000 calc(100% - 24px), transparent 100%);
}
@media (prefers-reduced-motion: reduce) { .carousel { scroll-behavior: auto; } }
.carousel::-webkit-scrollbar { height: 6px; }
.carousel::-webkit-scrollbar-thumb { background: var(--rule); border-radius: 3px; }
.insight-card {
flex: 0 0 min(320px, 86%);
scroll-snap-align: start;
display: flex; flex-direction: column; gap: 10px;
padding: 18px 20px;
background: var(--bg-elev);
border: 1px solid var(--rule);
border-left: 3px solid var(--accent);
border-radius: 12px;
}
.insight-card .insight-num {
font-family: var(--font-mono);
font-size: 0.6875rem; letter-spacing: 0.12em;
color: var(--ink-soft);
}
.insight-card h3 { margin: 0; font-size: 1rem; font-weight: 600; line-height: 1.35; }
.insight-card .insight-observation { margin: 0; font-size: 0.9375rem; }
.insight-card details { font-size: 0.875rem; }
.insight-card summary {
color: var(--ink-soft); cursor: pointer; padding: 4px 0;
list-style: none;
}
.insight-card summary::before { content: "▸ "; color: var(--accent); }
.insight-card details[open] summary::before { content: "▾ "; }
.insight-card .insight-reframe {
margin: 0; padding-top: 10px;
border-top: 1px solid var(--rule);
font-size: 0.9375rem;
}
.insight-card .reframe-label {
display: block;
font-size: 0.6875rem; letter-spacing: 0.14em; text-transform: uppercase;
color: var(--ink-soft); font-weight: 600; margin-bottom: 4px;
}
```
A11y guarantees for the carousel:
1. Wrapping `<div role="region" aria-label>` makes it a discoverable landmark.
2. Cards are `<article>` in source order — screen readers get the full list, no scroll needed.
3. Each card has its own `aria-labelledby` pointing to its `<h3>`.
4. `<details>`/`<summary>` provides progressive disclosure for the Science paragraph — defaults closed so cards stay scannable.
5. Keyboard tab order traverses cards linearly; arrow keys are NOT custom-handled (default browser behaviour preserved).
6. No dots indicator — they require JS to sync with scroll position. The scrollbar + edge-fade + "Scroll →" instruction provide affordance.
7. Per `prefers-reduced-motion: reduce`, smooth scrolling disabled.
### Arc card (sparkline)
```html
<section aria-labelledby="s02">
<h2 id="s02"><span class="num" aria-hidden="true">02</span> Emotional arc</h2>
<div class="card">
<div class="t-micro">Valence · daily mean</div>
<svg class="spark" viewBox="0 0 600 80" preserveAspectRatio="none"
role="img" aria-labelledby="s02-vt s02-vd">
<title id="s02-vt">Valence sparkline</title>
<desc id="s02-vd">Daily mean valence ranging from {min} to {max} over {n_days} days. Trend: {trend}.</desc>
<line x1="10" y1="40" x2="590" y2="40" stroke="var(--rule)" stroke-dasharray="3,3"/>
<polyline fill="none" stroke="var(--{q}-line)" stroke-width="2.5" stroke-linecap="round" points="…"/>
<!-- focusable point groups -->
</svg>
<div class="t-meta">Valence range: {min} → {max} · scale −1 to +1</div>
<!-- arousal sparkline parallel structure -->
</div>
</section>
```
### Affect map (Russell scatter)
```html
<section aria-labelledby="s03">
<h2 id="s03"><span class="num" aria-hidden="true">03</span> Affect map</h2>
<div class="card">
<svg viewBox="0 0 320 320" role="img" aria-labelledby="s03-t s03-d" style="width:100%; max-width:480px;">
<title id="s03-t">Affect map — Russell circumplex scatter</title>
<desc id="s03-d">
{n_messages} messages plotted across valence and arousal axes. Centroid at quadrant {q}.
{mode_note} <!-- e.g. "Density-binned because most messages clustered near the centroid." -->
</desc>
<!-- quadrant fills bound to tokens -->
<rect x="160" y="0" width="160" height="160" fill="var(--q1-fill)"/>
<!-- … axes, labels … -->
<!-- per-dot OR per-cell density mode (see §5) -->
<circle cx="{x}" cy="{y}" r="{r}" fill="var(--{q}-line)" opacity="0.5"/>
<!-- centroid -->
<circle cx="{cx}" cy="{cy}" r="7" fill="var(--{q}-line)" stroke="var(--bg)" stroke-width="3"/>
</svg>
<p class="t-meta caption">{mode_caption}</p>
</div>
</section>
```
### Day rhythm strip (focusable hours)
```html
<div class="rhythm" role="group" aria-label="Hourly valence">
<g class="h" tabindex="0" role="button"
aria-label="{hour}:00 — {n_msgs} messages, mean valence {valence}"
style="background: {pre_computed_hex};">
<!-- visual content -->
</g>
<!-- ×24 -->
</div>
<p class="t-meta">Peak {peak_hour} · Dip {dip_hour}{sparse_note}</p>
```
### Pattern card (observation / science / reframe)
```html
<article class="pattern" aria-labelledby="p1">
<h3 id="p1">{n}. {title}</h3>
<p>{observation}</p>
<div class="label" aria-hidden="true">Science</div>
<p class="t-meta">{science_with_citation}</p>
<div class="label" aria-hidden="true">Worth asking</div>
<p>{reframe_question}</p>
</article>
```
### Word weather span
```html
<span class="w {pos|neg|neu}" style="font-size:{px}px;" aria-label="{word}, {polarity}, {frequency} occurrences">{word}</span>
```
Renderer notes: the visual style for `.w.neg` includes underline (CSS, not inline). The `aria-label` carries polarity textually so screen readers don't need to parse the colour.
---
## Provenance
This DESIGN.md was authored after the 2026-05-13 audit of the first generated report. Audit signals folded in: C1 (scatter overplotting), C2 (anonymisation), H1 (SVG aria), H2 (title tooltips), H3 (palette contrast), H4 (color-only encoding), H5 (Inter font), H6 (numbering), M1–M9, L1–L8. See the audit report in conversation history.
Future audits should append a `## Provenance` entry below this line rather than rewriting it — keep the history of why each rule exists.