# Design System ## Principles kami's aesthetic compresses into one sentence: **warm parchment canvas, ink-blue accent, serif carries hierarchy, avoid cool grays and hard shadows**. This is not a UI framework. It is a constraint system for print, designed to keep pages stable, clear, and readable. **The ten invariants** (each has a real cost, think before overriding): 1. Page background parchment `#f5f4ed`, never pure white 2. Single accent: ink-blue `#1B365D`, no second chromatic color 3. All grays warm-toned (yellow-brown undertone), no cool blue-grays 4. English: serif for everything (headlines and body). Chinese: serif headlines, sans body. Sans only for UI elements (labels, eyebrows, meta) in both 5. Serif weight locked at 500, no bold 6. Line-heights: tight headlines 1.1-1.3, dense body 1.4-1.45, reading body 1.5-1.55 7. Letter-spacing: Chinese body 0.3pt for comfortable reading; English body 0; tracking only for short labels and overlines 8. Tag backgrounds must be solid hex, never rgba (WeasyPrint renders a double rectangle) 9. Depth via ring shadow or whisper shadow, never hard drop shadows 10. **No italic in print templates**. No `font-style: italic` in any PDF template or demo. Exception: landing page (screen-only) uses italic for poetic lines (gallery captions, feature subtitles, footer ethos) This system is a fusion of Anthropic's visual language and real Chinese / English resume iteration. Details below. --- ## 1. Color **Single accent, warm neutrals only, zero cool tones** - this is the core. ### Brand ```css --brand: #1B365D; /* Ink Blue - the only chromatic color. CTAs, accents, section-title left bar. */ --brand-light: #2D5A8A; /* Ink Light - brighter variant, for links on dark surfaces. */ ``` **Rule**: ink-blue covers ≤ **5% of document surface area**. More than that is ornament, not restraint. ### Surface ```css --parchment: #f5f4ed; /* Page background - warm cream, the emotional foundation */ --ivory: #faf9f5; /* Card / lifted container - brighter than parchment */ --warm-sand: #e8e6dc; /* Button default / interactive surface */ --dark-surface: #30302e; /* Dark-theme container - warm charcoal */ --deep-dark: #141413; /* Dark-theme page background - not pure black, slight olive undertone */ ``` **Never**: `#ffffff` pure white as page background. `#f8f9fa` / `#f3f4f6` or any cool-gray surface. ### Text ```css --near-black: #141413; /* Primary text - deepest but not pure black, warm olive undertone */ --dark-warm: #3d3d3a; /* Secondary text, table headers, links */ --olive: #504e49; /* Subtext - descriptions, captions. zh-CN TsangerJinKai02 不需要 override. JA override: #4d4c48 (YuMincho thin strokes need darker text) */ --stone: #6b6a64; /* Tertiary - dates, metadata */ ``` Four levels: near-black (primary) > dark-warm (secondary) > olive (subtext) > stone (tertiary). No fifth level needed. **Mnemonic**: every gray has a **yellow-brown undertone**. In `rgb()`, warm gray is R ≈ G > B (or R > G > B with small gaps). Cool gray is R < G < B or R = G = B (neutral). ### Border ```css --border: #e8e6dc; /* Primary border - section dividers, table headers, card borders */ --border-soft: #e5e3d8; /* Secondary border - row separators, subtle dividers */ ``` ### Semantic warm accent (the one sanctioned exception) ```css --breaking-bg: #f0e0d8; /* Changelog .tag.breaking background - muted warm peach */ --breaking-fg: #8b4513; /* Changelog .tag.breaking text - warm brown */ ``` The "no second chromatic color" rule has exactly one approved exception: the breaking-change badge in the changelog template needs a warm warning tint to read as "caution" without importing a red. Both values are warm-toned (R > G > B), registered as `--breaking-*` tokens in `tokens.json`, and enforced by the off-palette lint guard. Any other off-token color is a violation. Do not add a second semantic accent without registering it here and in `tokens.json`. ### Translucent -> Solid conversion (TAGS MUST BE SOLID) **Why**: WeasyPrint's alpha compositing for padding vs glyph areas produces a visible double rectangle on zoom. See `production.md` Part 4 Pitfall #1. Ink Blue `#1B365D` over parchment `#f5f4ed`: | rgba alpha | Solid hex | |---|---| | 0.08 | `#EEF2F7` | | 0.14 | `#E4ECF5` | | **0.18** | **`#E4ECF5`** ← default tag | | 0.22 | `#D0DCE9` | | 0.30 | `#D6E1EE` | The default tag swatch `#E4ECF5` is registered as the `--tag-bg` token in `tokens.json`, so its 15 template definitions stay under the sync guard. --- ## 2. Typography ### Stacks ```css /* Single serif per page. --sans always equals var(--serif). */ /* English */ font-family: Charter, Georgia, Palatino, "Times New Roman", serif; /* Chinese */ font-family: "TsangerJinKai02", "Source Han Serif SC", "Noto Serif CJK SC", "Songti SC", "STSong", Georgia, serif; /* Japanese */ font-family: "YuMincho", "Yu Mincho", "Hiragino Mincho ProN", "Noto Serif CJK JP", "Source Han Serif JP", "TsangerJinKai02", Georgia, serif; /* Mono, with CJK fallback for comments and labels */ font-family: "JetBrains Mono", "SF Mono", "Fira Code", Consolas, Monaco, "TsangerJinKai02", "Source Han Serif SC", monospace; ``` Any font-family that may render Chinese or Japanese must include a CJK fallback, including `@page` footer text, `pre`, `code`, and SVG labels. A pure mono stack can render missing glyph boxes in WeasyPrint. ### Size scale (pt for print A4, px for screen) **Print:** | Role | Size | Weight | Line-height | Use | |---|---|---|---|---| | Display | 36pt | 500 | 1.10 | Cover title, one-pager hero | | H1 Section | 22pt | 500 | 1.20 | Chapter titles | | H2 | 16pt | 500 | 1.25 | Subsection | | H3 | 13pt | 500 | 1.30 | Item titles | | Body Lead | 11pt | 400 | 1.55 | Intro paragraphs | | Body | 10pt | 400 | 1.55 | Reading body | | Body Dense | 9.2pt | 400 | 1.42 | Dense body (resume, one-pager) | | Caption | 9pt | 400 | 1.45 | Notes, figure captions | | Label | 9pt | 600 | 1.35 | Small labels, corner tags | | Tiny | 9pt | 400 | 1.40 | Footer, minor metadata | **Screen (px)** ≈ pt × 1.33 (9pt ≈ 12px, 18pt ≈ 24px). **Minimum floor**: web text >= 12px, PDF text >= 9pt. **Slide caption floor**: slides 上 caption 至少 24px (不是 12px)。Print 9pt 在投影距离不可读,slide caption 用 pt x 2.67。 ### Weight - **Serif body**: 400 (W04 font file) - **Serif headings**: 500 (W05 font file, real bold, not synthetic) - **Sans body**: 400 default - **Sans labels / small titles**: 500 or 600 - **Forbidden**: 900 black, 100 thin **Design principle**: Serif uses only two weights (400/500), no synthetic bold (600/700), maintaining restrained typography. - `strong { font-weight: 500 }` in long-doc templates locks bold to W05, preventing browsers from synthesizing 700 on top of W05 - **Web only**: W04 covers weight 400-500 (single `font-weight: 400 500` declaration); W05 is PDF-only because WeasyPrint cannot synthesize bold ### Line-height Print documents are **tighter** than English web body. English web typically runs 1.6-1.75; in print at pt sizes that feels loose and floats. | Tier | Value | Use | |---|---|---| | Tight headline | 1.10-1.30 | Display, H1, H2 | | Dense body | 1.40-1.45 | Resume, one-pager, dense information | | Reading body | 1.50-1.55 | Long-doc chapters, letters | | Label / caption | 1.30-1.40 | Small labels, multi-line metadata | | CJK screen body | 1.55-1.65 | 中日文 serif 在 slide scale (27-33px) 下比 print x1.33 更需松行高 | **Forbidden**: - 1.60+ - loose feel, web rhythm, not print - 1.00-1.05 - lines collide except at extreme display sizes ### Letter-spacing - Body text: **0** - Chinese and Japanese body text with TsangerJinKai02: **0.1–0.2pt** to compensate for the font's natural density; section titles and Mincho samples: **0** - Chinese lede text (14–22pt) with TsangerJinKai02: **0.03–0.06em** to open up large-body paragraphs without breaking density; EN and JA lede: **0** (only TsangerJinKai02 needs density compensation) - Chinese and Japanese display text (24pt+): **0.2–1pt** optical spacing for visual breathing room at large sizes; scale with font size - English headings may use subtle optical tightening when needed; keep it localized, never inherited by body copy - Small labels (< 10pt): +0.2 to +0.5pt for readability - All-caps overlines: +0.5 to +1pt mandatory - **Slide-specific**: print tracking x0.5 at slide scale. Eyebrow max 3px (not 8px), display titles -0.5pt. Large type at 40pt+ will look scattered at print tracking values --- ## 3. Spacing ### Base unit: 4pt (4px on screen) | Tier | Value | Use | |---|---|---| | xs | 2-3pt | Inline adjacent elements | | sm | 4-5pt | Tag padding, dense layout | | md | 8-10pt | Component interior | | lg | 16-20pt | Between components / card padding | | xl | 24-32pt | Section-title margins | | 2xl | 40-60pt | Between major sections | | 3xl | 80-120pt | Between chapters (long docs) | ### Page margins (A4) | Document | Top | Right | Bottom | Left | |---|---|---|---|---| | Resume (dense) | 11mm | 13mm | 11mm | 13mm | | One-Pager | 15mm | 18mm | 15mm | 18mm | | Long Doc | 20mm | 22mm | 22mm | 22mm | | Letter | 25mm | 25mm | 25mm | 25mm | | Portfolio | 12mm | 15mm | 12mm | 15mm | **Rule**: denser = smaller margins, more formal (letter) = larger margins. ### Slide-scale spacing Print uses mm/pt; slides (screen) use px. The scale relationships differ: ```css --slide-pad: 80px; /* slide four-side padding baseline */ ``` **Key rules**: - Slide padding-top: 72-80px (print is 96-120px; slides are more compact) - Slide letter-spacing = print value / 2 (8px tracking "falls apart" on screen; halve it) - Macro scale (font size, padding): multiply print pt values by ~1.6 - Micro scale (letter-spacing, border, radius): multiply by ~0.6 --- ## 4. Components ### Cards / Containers ```css .card { background: var(--ivory); border: 0.5pt solid var(--border-cream); border-radius: 8pt; padding: 16pt 20pt; } .card-featured { border-radius: 16pt; box-shadow: 0 4pt 24pt rgba(0,0,0,0.05); /* whisper shadow */ } ``` Radius scale: 4pt -> 6pt -> 8pt (default) -> 12pt -> 16pt -> 24pt -> 32pt (hero containers). ### Buttons ```css /* Primary - brand-colored */ .btn-primary { background: var(--brand); color: var(--ivory); padding: 8pt 16pt; border-radius: 8pt; box-shadow: 0 0 0 1pt var(--brand); /* ring shadow */ } /* Secondary - warm-sand */ .btn-secondary { background: var(--warm-sand); color: var(--dark-warm); padding: 8pt 16pt; border-radius: 8pt; box-shadow: 0 0 0 1pt var(--border); } ``` ### Tags Three tiers from weak to strong visual weight: **Lightest solid** (default, most restrained): ```css .tag { background: #EEF2F7; /* 0.08 solid equivalent */ color: var(--brand); font-size: 9pt; font-weight: 600; padding: 1pt 5pt; border-radius: 2pt; letter-spacing: 0.4pt; text-transform: uppercase; } ``` **Standard solid** (when more contrast needed): ```css .tag { background: #E4ECF5; /* 0.18 solid equivalent */ color: var(--brand); padding: 1pt 6pt; border-radius: 4pt; } ``` **Gradient brush** (only when "hand-painted" feel is required - use sparingly): ```css .tag { background: linear-gradient(to right, #D6E1EE, #E4ECF5 70%, #EEF2F7); color: var(--brand); padding: 1pt 5pt; border-radius: 2pt; } ``` **Philosophy**: tint depth should be one step lighter than what decoration wants. Prefer pale over saturated. In iteration, "gradient brush" often steals focus - lightest solid wins most of the time. **Never**: `background: rgba(201, 100, 66, 0.18)` - WeasyPrint double-rectangle bug. ### Lists Use native list markers, brand-colored: ordered lists carry numbers, unordered lists carry a disc. Do not fake a bullet with a `::before` en-dash; a dash marker reads like AI default output, not editorial typesetting. The `ul.dash` class is an alias for the same native rendering, kept only so existing markup keeps working. ```css ul, ol { padding-left: 16pt; line-height: 1.55; } ul li::marker { color: var(--brand); } ol li::marker { color: var(--brand); font-weight: 500; } ul.dash { padding-left: 16pt; } /* native disc, no en-dash hack */ ul.dash li::marker { color: var(--brand); } ``` ### Quote ```css .quote { border-left: 2pt solid var(--brand); padding: 4pt 0 4pt 14pt; color: var(--olive); line-height: 1.55; } ``` ### Code ```css .code-block { background: var(--ivory); border: 0.5pt solid var(--border-cream); border-radius: 6pt; padding: 10pt 14pt; font-family: var(--mono); font-size: 9pt; line-height: 1.5; } ``` ### Section Title ```css .section-title { font-family: var(--serif); font-size: 14pt; font-weight: 500; color: var(--near-black); margin: 24pt 0 10pt 0; border-left: 2.5pt solid var(--brand); border-radius: 1.5pt; padding-left: 8pt; } ``` Resume exception: resume templates use a quiet bottom rule instead of the brand left bar, and project rows stay borderless to avoid double rules and page-top orphan lines. Document header signature: across the document templates (one-pager, changelog, equity-report, long-doc cover) the page header opens with an uppercase eyebrow led by the 8pt x 1.5pt brand tick (a short horizontal bar, not a round bullet, which reads juvenile next to CJK), then the serif title, with any meta right-aligned and a 0.5pt hairline rule closing the block. This is the shared opener: no centered version block, no full-height left bar. The full-height brand left bar reads heavy and crude as a page-top frame, so keep it a section-level and pull-quote device only. Resume (name header) and letter (letterhead) keep their purpose-built headers and are exempt. Hero product shot (one-pager): a product brief earns one real screenshot as its visual anchor, not a decorative texture. Frame it in a wrapper with `overflow: hidden`, `border-radius`, and a soft shadow (no closed sub-1pt border, which trips the `thin-border-radius` lint and risks a double ring); size the wrapper by `height` with the image set to `object-fit: cover` so dead background (wallpaper, chrome margins) is trimmed evenly while the app window stays whole. Give it a single caption that adds a fact, not a restatement. Adjust the wrapper height to fill the page rather than stretching text or leaving bottom whitespace. Changelog best practice: a release-notes doc uses the same editorial language as the one-pager, not a centered version block. Open with the left-aligned header (eyebrow tick + "Project Version" serif title + date on the right + hairline rule). Group entries under h2 section heads carrying the brand left bar (Breaking / Features / Fixes, or Highlights / Fixes), and drop any section that does not apply. Each entry is a numbered item with a bold lead-in, then a colon and the detail; the numbers carry sequence and restart per section, so no per-item bullet glyph is added (one repeated mark per row reads as clutter). Keep acknowledgements a quiet labelled note, never a filled card, and split the footer into left description and right URL. One locale per file, no bilingual stacking. A breaking entry may carry an inline `.tag.breaking` chip; that is the only inline tag worth keeping. ### Table (kami-table) Unified table component across all templates. Base class applies to bare `` or `.kami-table`. ```css table, .kami-table { width: 100%; border-collapse: collapse; font-size: 9.5pt; margin: 12pt 0; break-inside: avoid; } table th, .kami-table th { text-align: left; font-weight: 500; color: var(--dark-warm); padding: 6pt 8pt; border-bottom: 1pt solid var(--border); } table td, .kami-table td { padding: 5pt 8pt; border-bottom: 0.3pt solid var(--border-soft); vertical-align: top; } ``` **Variants** (combine freely on the same element): | Class | Purpose | |---|---| | `.compact` | 8pt font, tighter padding. For data-dense tables in resume/one-pager. | | `.financial` | Right-align all columns except the first, enable `tabular-nums`. For revenue, pricing, metrics. | | `.striped` | Alternating `var(--ivory)` background on even rows. Improves scanability for wide tables. | **Total row**: add `.total` to the final `` for a bold summary row with a `1pt` brand top border. ```html
CategoryQ1Q2
Revenue$12.4M$14.1M
Total$12.4M$14.1M
``` ### Metric Key numbers side-by-side (one-pager header, resume top, portfolio cover): ```css .metrics { display: flex; gap: 24pt; } .metric { flex: 1; display: flex; align-items: baseline; gap: 6pt; } .metric-value { font-family: var(--serif); font-size: 16pt; font-weight: 500; color: var(--brand); font-variant-numeric: tabular-nums; /* align digits in columns */ } .metric-label { font-size: 9pt; color: var(--olive); white-space: nowrap; } ``` Metric labels never wrap. The value and label share a baseline (`align-items: baseline`); a label that wraps to a second line dangles below that baseline and reads broken. Keep every label short enough for one line and set `white-space: nowrap`, so an over-long label is caught as overflow during QA instead of silently wrapping. Fix by shortening the words, not by letting it wrap. ### Section Header (`.kami-section-header`) Lightweight section opener for content slides. Has an eyebrow and a horizontal rule. ```css .kami-section-header { margin-bottom: 36px; } .kami-section-header .eyebrow { display: flex; align-items: center; /* dot is geometric, center beats baseline */ gap: 8px; font-family: var(--sans); font-size: 12px; font-weight: 500; letter-spacing: 1.5px; text-transform: uppercase; color: var(--stone); margin-bottom: 14px; } .kami-section-header .eyebrow::before { content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--brand); flex-shrink: 0; } .kami-section-header .rule { height: 1px; background: var(--border-warm); margin-bottom: 36px; /* gap below rule >= 36px (>= 2x the gap above) */ } .kami-section-header h1 { font-family: var(--serif); font-size: 38px; font-weight: 500; line-height: 1.1; color: var(--near-black); } ``` **Spacing rule**: eyebrow to rule: 14px; rule to H1: **≥ 36px** (the gap below must be at least double the gap above, creating a visual anchor). ### Code Card (`.kami-code-card`) For displaying pseudocode or code snippets in slides. More structured than a plain code block. ```css .kami-code-card { background: var(--ivory); border: 1px solid var(--border-cream); border-radius: 8px; padding: 20px 24px; overflow: hidden; } .kami-code-card pre { font-family: var(--mono); font-size: 13px; /* 14px for larger slides */ line-height: 1.55; color: var(--near-black); margin: 0; white-space: pre; } /* Syntax colors: existing tokens only, no new colors */ .kami-code-card .k { color: var(--brand); } /* keyword / string */ .kami-code-card .c { color: var(--stone); } /* comment */ /* Optional line numbers: 1px left divider */ .kami-code-card.numbered { display: grid; grid-template-columns: auto 1fr; gap: 0 16px; } .kami-code-card .line-nums { font-family: var(--mono); font-size: 13px; line-height: 1.55; color: var(--stone); text-align: right; border-right: 1px solid var(--border-soft); padding-right: 12px; user-select: none; } ``` **Content philosophy**: use pseudocode style. Comments should outnumber code lines. The reader sees logic, not syntax. ### Syntax Highlighting Code blocks with `class="language-*"` on the `` element get Pygments-based highlighting at build time. The palette uses existing tokens only: | Token | Hex | Token var | |---|---|---| | Keyword | `#1B365D` | `--brand` | | Comment | `#6b6a64` | `--stone` | | String | `#504e49` | `--olive` | | Number | `#3d3d3a` | `--dark-warm` | | Function/Class | `#141413` | `--near-black` | ```html
def analyze(data):
    """Transform raw data."""
    return transform(data)
``` Blocks without `class="language-*"` stay monochrome. Requires `pip install Pygments`; without it, blocks pass through unstyled. ### Glance Grid Four key-number cells, placed after the TOC or on a chapter-opening page of a long-doc / proposal. ```html
REPORTING PERIOD
Q1 2026
Three core themes
``` ```css .glance-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14pt; margin: 18pt 0; } .glance-cell { padding: 12pt 0 10pt 14pt; border-left: 2pt solid var(--brand); border-radius: 1.5pt; } .glance-label { font-family: var(--mono); font-size: 8.5pt; color: var(--brand); letter-spacing: 1pt; text-transform: uppercase; font-weight: 500; } .glance-value { font-size: 18pt; font-weight: 500; color: var(--near-black); font-variant-numeric: tabular-nums; letter-spacing: 0.5pt; } .glance-note { font-size: 9pt; color: var(--olive); line-height: 1.4; } ``` ### Module Block Proposal A / B / C structure: each module gets a brand-colored letter, a Chinese title, and an uppercase English subtitle. ```html
A
模块标题
MODULE SUBTITLE

...

``` Visual recipe: letter at 28pt brand, title at 17pt, English subtitle at 10pt mono brand `letter-spacing: 2pt`, head separated from body by a 0.3pt warm-gray hairline (not brand color). ### Module Note (group explanation) A short note that explains the relationship between two or more modules. Same family as `.callout`, lighter weight, no decorative bar. ```html
ABOUT B + C

B 是上游能力建设,C 是下游验证。两者构成一个最小闭环。

``` ivory background + 4pt radius + `module-note-label` at 8.5pt brand uppercase mono. ### Position Table Three-column industry-comparison table whose final row highlights the current project / subject. ```html ...
DirectionReference projectApproach
.........
本项目
``` `.highlight` row: ivory fill + brand text. Do not bold the entire row; let the `` carry the emphasis. ### Pricing Card Headline-figure price block. Eyebrow + price + short note. ```html
PROJECT TERM
¥ XXX,XXX / term
Paid by milestone
``` Digits: serif 500, 44pt, `tabular-nums`, `letter-spacing: 0.5pt`. Without the letter-spacing, large digits crowd each other and read as too dense. For the without-price variant (see writing.md "Proposal voice"), drop the `.pricing-price` block and follow the eyebrow with the Value Anchors list below. ### Value Anchors Replaces pricing line-item breakdowns with a short list of capability anchors. Pairs with Pricing Card or stands alone in the without-price variant. ```html ``` ```css .value-anchors { list-style: none; padding: 0; margin: 12pt 0 18pt 0; } .value-anchors li { position: relative; padding: 9pt 0 9pt 18pt; border-bottom: 0.3pt solid var(--border-soft); line-height: 1.55; font-size: 10.5pt; } .value-anchors li:last-child { border-bottom: none; } .value-anchors li::before { content: ""; position: absolute; left: 0; top: 17pt; width: 8pt; height: 1.5pt; background: var(--brand); } .value-anchors li strong { color: var(--brand); font-weight: 500; margin-right: 6pt; } ``` The 8pt × 1.5pt brand bar (`::before`) replaces the round `