# 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. Default page background parchment `#f5f4ed`; the white-paper print variant in production.md is the explicit exception 2. Single accent: ink-blue `#1B365D`, no second chromatic color 3. All grays warm-toned (yellow-brown undertone), no cool blue-grays 4. One serif family per page for headlines and body. Add a distinct sans only for genuine UI chrome 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. Surfaces are flat by default; whisper shadows are reserved for real floating screenshots, popovers, or overlays 10. **No italic in print templates**. No `font-style: italic` in any PDF template or demo. Exception: landing page (screen-only) allows italic for captions, feature subtitles, and a closing tagline; these need not be poetic 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, and key labels. */ --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; /* Quiet filled container - brighter than parchment */ --inline-code-bg: #f0eee6; /* Screen inline annotation - one warm-gray step darker 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, controls */ --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` resolves to two registered chromatic tints: | Token | Hex | Use | |---|---|---| | `--tag-bg` | `#E4ECF5` | the default tag swatch | | `--brand-tint` | `#EEF2F7` | the lightest fill, when a tag must recede | Use the token, never a hand-mixed `rgba()`. A tint outside these two is a new token: add it to `tokens.json` first, or `scripts/tokens.py` will fail the sync guard across the templates that define it. `--inline-code-bg` is a screen-only warm-neutral surface, not a third accent tint. Use it for small literals embedded in explanatory web copy, where blue would imply focus or a clickable state. Print templates keep their existing ivory inline-code treatment. --- ## 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", "Source Han Serif CN", "Noto Serif CJK SC", "Noto Serif 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。 **Ladder discipline**: sizes must land ON the scale, never between its steps. Components that each pick their own size drift into 12.5 / 13.5 / 14.5 / 17.5 neighbours; a reader cannot tell a 13.5 from a 14, so the difference registers as noise instead of hierarchy. Audit a screen stylesheet with `grep 'font-size:' | sort | uniq -c`: every value should be a scale step, and the 12px floor should carry only uppercase micro-labels and badges, never prose (prose stops at 14). **Serif sets larger than sans at the same number**: a tall x-height serif at 15px already reads bigger than 15px sans. Do not raise a serif body size to fix perceived readability; check the ladder step first. ### 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+ on a print body - loose feel, web rhythm, not print. The CJK screen-body row above is the one exception, and only at slide scale. - 1.00-1.05 - lines collide except at extreme display sizes ### Letter-spacing - Body text: **0** - Chinese and Japanese body text with TsangerJinKai02: **0.3pt**, the baseline every shipped CN/JA template uses (`long-doc.html`, `one-pager.html`, `slides-weasy.html`); 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 ### External principles cross-check A cross-check against Pierrick Calvez, "A Five-Minute Guide to Better Typography" (external reference, read once for calibration, not reprinted here). Where the guide agrees with Kami it sharpens a rule already stated above; where it conflicts, the Kami invariant wins. Use this list to resist "improving" Kami toward habits that suit a Western multi-weight editorial page but break this constraint system. **Agrees with Kami (apply):** - **Set blocks, not glyphs.** Type is a beautiful group of letters, not a group of beautiful letters. Judge a paragraph as a shape and an even gray field, not one admirable character at a time. This is why Kami pins measure, line-height, and tracking per context rather than tuning individual words. - **Optical alignment beats mathematical alignment.** Text is aligned when it looks aligned, not when the box edges match. Nudge the optical edge back when a quotation mark, a bullet, a large display cap, or a hanging figure pushes a line visually past the margin. This is the same instinct behind the existing display-tracking and «cap both tracks» rules; it is a manual eyeball pass, not a token. - **Measure (line length) for reading body: about 40 to 70 characters per line.** Too wide and the eye loses the next line's start; too narrow and rhythm breaks. This is the character-count basis for the reading-measure caps already stated for screen prose (Section 11 «Documentation site», about 720px) and for the natural print measure held by the A4 margins in Section 3. Keep body reading inside this band; do not let a full-frame screen column run edge to edge. - **Line-spacing scales with measure and length.** Short blocks read fine near 1.2x the font size; long reading passages want roughly 1.5x. Map this onto Kami's locked tiers, do not invent your own: tight headlines 1.10-1.30, dense body 1.40-1.45, reading body 1.50-1.55. The guide's "1.5x" lands on Kami's reading tier; it is not a licence to reach 1.6+ on a print body (still forbidden above). - **Hierarchy comes from contrast, not ornament.** Separate levels with size, weight, and space, and when a step is unclear either skip a weight rather than adding a faint one, or open the size gap. Kami reaches the same end through its fixed ladder: use the next registered size step and the 500/400 weight pair, plus spacing, never a new in-between size (see «Ladder discipline») and never a decorative rule (see «Subtractive rule»). - **Left-align body; centre only short display lines.** Ragged-right left alignment gives the eye a stable return edge for running text. Centring is for a cover title, a short subtitle, or a single pinned callout line, never for paragraphs or lists. This matches the left-edge discipline in «Feature rows» and the centred-cover exceptions in the Deck Recipe. - **Kerning and tracking are optical tools for large and small type.** Spend them on display sizes and all-caps or small-caps labels, exactly where the Letter-spacing rules above already allow it. Do not track body copy for effect. **Conflicts with Kami (do not import):** - **Multi-weight typeface families.** The guide advises choosing a face with many weights (Light, Regular, Medium, Bold) and orchestrating them. Kami forbids this for the serif: body is 400, headings are 500, and that is the whole range. No 700 (synthetic bold is banned), no Light. Emphasis is carried by size, space, and ink-blue, per «Weight» and invariant 5. Do not add a weight step to a Kami serif document. - **Western ornamental punctuation habits.** Editorial guides written for English print lean on the em dash and decorative punctuation. Kami constrains dashes and decoration deliberately: see the no-em-dash rule in `references/anti-patterns.md` #28 and the list-marker rule in «Lists» (no faux en-dash bullets). Do not import em-dash-heavy phrasing or ornamental marks from the guide. - **Do not reprint the guide.** Keep this a distilled cross-check. Do not paste a full translation or a substantial verbatim excerpt of the source into the repo. --- ## 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) | **Proximity law (a heading belongs to what follows)**: the gap UNDER a section head must be clearly smaller than the gap ABOVE it, ideally by 2x or more. Get this backwards (generous below, tight above) and the head reads as the tail of the previous section, no matter how large its type is. A screen page runs on three steps and nothing else: inside a block, head-to-content, section-to-section. **A cramped head is an internal problem**: when a heading block feels dense, the cause is usually its own internals (eyebrow-to-title, title-to-lede), not the section gaps. Open those by a few points before touching the macro rhythm. **Two-column rows: cap both tracks, justify to the edges.** In an image-plus-copy row, let the copy column grow freely (`1fr`) and the text strands itself against an empty far edge. Cap both tracks and push the slack into the gutter instead: both outer edges stay aligned to the container, and the copy sits at its natural measure. **A repeated row is no place for ornament.** A connector line, a badge, or an index number that looks charming once becomes noise on the fifth repetition. Empty space between the two columns is the answer, not a graphic that fills it. ### 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) - Macro scale (font size, padding): multiply print pt values by ~1.6 - Micro scale (display tracking, border, radius): halve the print value. Wide display tracking falls apart at slide scale; body letter-spacing stays at the print baseline. --- ## 4. Components ### Cards / Containers ```css .card { background: var(--ivory); border-radius: 4pt; padding: 16pt 20pt; } ``` A lifted surface is carried by the fill, not by an outline: `--ivory` against `--parchment` is the whole gesture. Do not add a closed hairline border. Below 1pt a closed border plus a radius renders as a double ring in WeasyPrint (`production.md` pitfall #2), and `scripts/lint.py` fails templates for it. When a card needs more weight than its fill, strengthen its label or opening sentence. Do not add an accent edge or ring all four sides. Print radii stay within 2-6pt and follow physical scale, from compact chips to large media frames. Larger steps (8pt and up) belong to screen surfaces only, where `landing-page.html` sets its own scale. Do not use radius alone to create emphasis. ### Subtractive rule A line earns its place only when it separates content regions, encodes state, or carries a data relationship. Decorative eyebrow ticks, short cover rules, contact rules, heading side bars, quotation side bars, and callout accent edges do none of those jobs and do not ship. Use type scale, labels, alignment, whitespace, and ivory fills to establish hierarchy. Keep table hairlines, chart axes, diagram connectors, input boundaries, full-width region separators, and current-state indicators. Apply the deletion test: if hiding a line preserves meaning, state, grouping, and navigation, delete it and restore any needed pause with spacing. ### Screen buttons ```css /* Primary - brand-colored */ .btn-primary { background: var(--brand); color: var(--ivory); padding: 8px 16px; border-radius: 8px; border: 1px solid var(--brand); } /* Secondary - warm-sand */ .btn-secondary { background: var(--warm-sand); color: var(--dark-warm); padding: 8px 16px; border-radius: 8px; border: 1px solid var(--border); } ``` ### Tags Two tiers, both on registered tokens. A gradient tag oversells itself at this size; if the reader's eye lands on the tag's background shape before the text inside, the tag is too strong. **Default**: ```css .tag { background: var(--tag-bg); /* #E4ECF5 */ color: var(--brand); font-size: 9pt; font-weight: 600; padding: 1pt 6pt; border-radius: 4pt; letter-spacing: 0.4pt; text-transform: uppercase; } ``` **Recede** (dense pages, or several tags in one row): ```css .tag { background: var(--brand-tint); /* #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. Inside an already filled editorial highlight, do not stack a filled tag on top of the fill. Use an ivory surface and a text-only ink-blue label; the label names the passage without becoming a second color block. **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 { margin: 12pt 16pt; padding: 4pt 0; color: var(--olive); line-height: 1.55; } ``` ### Code ```css .code-block { background: var(--ivory); border-radius: 4pt; /* fill only; no border, see «Cards» */ 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; } ``` Resume templates use a quiet full-width bottom rule because it separates major content regions. 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 uppercase eyebrow text, then the serif title, with any meta right-aligned and an optional 0.5pt full-width hairline closing the block. The hairline is structural: it separates the header from the document body. No leading tick, centered version block, short decorative rule, or full-height side bar. Resume (name header) and letter (letterhead) keep their purpose-built headers. 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`, restrained `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 for a legible crop 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. If the page is still sparse, merge or rebalance the content; do not inflate the screenshot merely to occupy space. 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 (uppercase eyebrow text + "Project Version" serif title + date on the right + structural hairline). Group entries under text-only h2 section heads (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. 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: 0.6pt solid var(--border); } table td, .kami-table td { padding: 5pt 8pt; border-bottom: 0.25pt solid var(--border); vertical-align: top; } table.compact th, .kami-table.compact th { padding: 3pt 6pt; font-size: 8pt; } table.compact td, .kami-table.compact td { padding: 2.5pt 6pt; font-size: 8pt; line-height: 1.4; } table .total td, .kami-table .total td { font-weight: 500; border-top: 0.6pt solid var(--border); border-bottom: none; color: var(--near-black); } ``` **Variants** (combine freely on the same element): | Class | Purpose | |---|---| | `.compact` | 8pt font, 3pt header and 2.5pt cell vertical padding. Use for 5+ columns, 8+ rows, or verified page-fit pressure. | | `.financial` | Right-align all columns except the first, enable `tabular-nums`. For revenue, pricing, metrics. | | `.striped` | Optional neutral `var(--ivory)` background on even rows. Use only for 8+ body rows when row tracking remains difficult at normal viewing size. | **Total row**: add `.total` to the final `` for a bold summary row with the same `0.6pt` neutral rule as the header. A total gains hierarchy from weight and placement, not a second color. **Acceptance rule**: every table rule uses `var(--border)`. Header and total rules are `0.6pt`; body rules are `0.25pt`. Normal tables keep at least `6pt` header and `5pt` cell vertical padding. Fixed-length one-pagers and resumes may step down once to `5pt` / `4pt`; compact tables never go below `3pt` / `2.5pt`. Start without striping. No category-colored values, brand-colored rules, tinted headers, vertical grid, or framed table. At normal viewing size, row separation must come from whitespace before line weight. Add neutral striping only when an 8+ row render still fails the row-tracking check. ```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-suffix { margin-left: 0.06em; font-size: 0.58em; font-weight: 400; vertical-align: 0.08em; } ``` This inline form is the print one, and it holds only because print labels are fixed short strings that never wrap. The value and label share a baseline; a label that wraps to a second line dangles below it and reads broken. Keep every label short enough for one line and set `white-space: nowrap`, so an over-long label surfaces as overflow during QA instead of silently wrapping. Fix by shortening the words, not by letting it wrap. On screen the labels are translated, retitled, and read at 375px, so a landing-page metric stacks instead (`flex-direction: column`). Same for slides. `production.md` pitfall #20 owns that call. Treat a multiplication sign in a display value as a suffix, not a second digit. In values such as `10×`, keep the true multiplication sign, set it near 60% of the numeral size, and optically align it to the numeral body. A full-size `×` reads like a letter and steals attention from the value. ### Section Header (`.kami-section-header`) Lightweight, text-only section opener for content slides. ```css .kami-section-header { margin-bottom: 36px; } .kami-section-header .eyebrow { 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 h1 { font-family: var(--serif); font-size: 38px; font-weight: 500; line-height: 1.1; color: var(--near-black); } ``` **Spacing rule**: keep 14-20px between eyebrow and H1, then let the section's outer margin create the larger pause. Do not insert a short rule or dot to manufacture hierarchy. ### 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); 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 0; } .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
  • 能力锚点 A:一句具体说明能力来源的事实陈述
  • 能力锚点 B:一句具体说明锚点稀缺性的事实陈述
``` ```css .value-anchors { list-style: disc; padding-left: 16pt; margin: 12pt 0 18pt 0; } .value-anchors li { padding: 9pt 0; 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::marker { color: var(--brand); } .value-anchors li strong { color: var(--brand); font-weight: 500; margin-right: 6pt; } ``` Use the native disc marker. A custom short bar is decoration disguised as list structure. ### Decoration density: subtractive by default Long-doc and proposal layouts use one rule: decoration does not create structure. Brand color appears in text, digits, labels, and intentional fills. Containers use ivory fill and restrained radius. Lines remain neutral and appear only when they separate content regions, encode state, or show a data relationship. Do not layer a brand line, fill, radius, and border onto the same component. If removing a line leaves the same meaning and grouping, the line was ornament. --- ## 5. Depth & Separation **Core rule**: the default surface is flat. Establish hierarchy with type, alignment, whitespace, one quiet fill, or one neutral hairline. Do not stack a fill, border, radius, and shadow on the same component, and do not make card hover create artificial elevation. Use a visible `outline` for keyboard focus. Reserve a whisper shadow for a real product screenshot, popover, or other element that physically floats above the page; never use it to make an ordinary card look more important. ### Section-level light/dark alternation Long docs alternate parchment `#f5f4ed` and `#141413` dark sections. This section-level light change creates the strongest contrast. **Forbidden**: `box-shadow: 0 2px 8px rgba(0,0,0,0.3)` and relatives. --- ## 6. Print & Pagination ### break-inside protection ```css .card, .metric, .project-item, .quote, .code-block, figure, .callout, .takeaway, .module, .module-note, .glance-grid, .pricing-card, table.compact { break-inside: avoid; } /* Headings should never sit alone at the bottom of a page */ h1, h2, h3 { break-after: avoid; } /* Widow / orphan minimums for body text */ body { widows: 3; orphans: 3; } p { widows: 2; orphans: 2; } ``` CSS alone cannot prevent "the last two lines of a chapter pushed onto a fresh page". For long-doc / proposal output, follow up with a render-time density check (see production.md "Verify & Debug"). Long-doc table-of-contents rows should link to stable chapter ids and use WeasyPrint `target-counter(attr(href), page)` for rendered page numbers. Do not hand-fill page numerals; any pagination-affecting edit will make them drift. **Cascading break-inside**: when two `break-inside: avoid` blocks sit next to each other and the first would split, both get pushed to the next page together. A chapter with more than two `break-inside: avoid` blocks (quote + table + callout, etc.) near a page boundary is at high risk of leaving 40-80mm of trailing whitespace on the previous page. Fix by splitting the chapter, or downgrade one block (allow the table to break with a repeating header ``). ### Force break ```css .page-break { break-before: page; } ``` ### Page background extending past margins ```css @page { size: A4; margin: 20mm 22mm; background: #f5f4ed; /* extends past margin area, prevents printed white edges */ } ``` --- ## 7. Quick decisions When you're not sure "what should I use": The answer is almost always a component a template already ships. Copy that one and edit its content; assembling a new container from a recipe is how a document ends up carrying three unrelated emphasis languages on one page. | Need | Use | |---|---| | Big headline | serif 500, size by level, line-height 1.10-1.30 | | Reading body | serif 400, 9.5-10pt, line-height 1.55. Every locale: CN templates pin `--sans: var(--serif)`, so one page carries one typeface | | Emphasize a number | `color: var(--brand)`, no bold | | Raise a passage above body text | `.callout`: ivory fill + 3pt radius, no accent edge. Only padding tightens on denser pages | | Quote someone | `long-doc` `blockquote` / `.quote`: indented olive text with reading space, no fill or side rule | | Show code | `long-doc` `pre` / `code`: ivory fill, 4pt / 2pt radius, no border | | Show key figures | `one-pager` `.metric`: baseline row, transparent, no container. Numbers carry themselves; a filled card around them is the most common drift | | Start a section | serif scale and margin only; no leading tick, side bar, or short rule | | Mark out one item in a list | ivory fill, stronger label, or a short lead sentence; no accent edge | | Cover page | `long-doc` cover: display heading, right-aligned author/date, heavy whitespace | | Buttons (screen only) | `landing-page` `.btn-primary` / `.btn-ghost`. Print documents have no buttons | Nothing here fits -> return to first principles: **serif carries authority, sans carries utility, warm gray carries rhythm, ink-blue carries focus**. Then add the smallest thing that works, and prefer an existing class over a new one. --- ## 8. Deck Recipe Slides in kami use WeasyPrint HTML to PDF as the primary rendering path. The pptx path (`slides.py`) is available as a fallback when the user explicitly requires an editable PPTX file. ### Architecture **Why WeasyPrint over python-pptx:** pptx output passed through LibreOffice loses CJK font weight, tracking, and glyph spacing. WeasyPrint embeds fonts exactly, giving pixel-level CSS control. Use `assets/templates/slides-weasy.html` (CN) or `assets/templates/slides-weasy-en.html` (EN) as the starting point. ### Page size Default `280mm 158mm`. Change in `@page` and `.slide` together. | Size | `@page` | Use when | |---|---|---| | Compact (default) | `280mm 158mm` | Standard density, fits most content | | Standard | `297mm 167mm` | Slightly more room per slide | | Wide | `338mm 190mm` | Heavy content, many data points | ### Typography Global parameters for the slide body: ```css body { font-size: 13pt; line-height: 1.55; letter-spacing: 0.3pt; /* CJK: critical for breathing room */ } ``` Heading scale: | Element | Size | Weight | Notes | |---|---|---|---| | `h2` | 24pt | 500 | Page title; `margin-bottom: 14pt` | | `h3` | 15pt | 500 | Section heading; `color: var(--brand)` | | `.eyebrow` | 9.5pt | 400 | Mono, `letter-spacing: 2pt`, `color: var(--stone)` | | `.lead` | 12pt | 400 | Below `h2`; `color: var(--olive)` | Content element scale: | Class | Size | Notes | |---|---|---| | `.mt` | 16pt | Module title, used with `.ml` | | `.ml` | 24pt | Large letter prefix in `var(--brand)`, paired with `.mt` | | `.ms` | 7.5pt mono | Module sub-label; `border-bottom` separator | | `.mb` | 11pt | Module body description | | `.mi` | 11pt | Module line item; `padding: 8pt 0` | | `.mc` | 9.5pt | Delivery rhythm or cadence note; `border-top` | | `.co` | 11pt bold | Bottom callout; `position: absolute; bottom: 12mm` | ### Layout patterns **Two-column (`.c2`)**: CSS Grid, `grid-template-columns: 1fr 1fr; gap: 22pt`. Use for side-by-side modules with independent heights. **2×2 aligned (`.t2x2`)**: HTML ``, not CSS Grid. Grid does not guarantee row alignment across cells; table rows share height naturally. ```html
``` **Pinned callout (`.co`)**: `position: absolute; bottom: 12mm; left: 20mm; right: 20mm`. The whitespace above it is intentional, not empty. ### Table styles ```css table.data td { padding: 8pt; border-bottom: 0.25pt solid var(--border); font-size: 11pt; } table.data td:first-child { font-weight: 500; color: var(--dark-warm); /* hierarchy without a second table color */ } ``` ### SVG constraints - `viewBox` width fixed at `920`; adjust height to content - `max-height: 105mm` on `svg` element to prevent overflow - WeasyPrint does not support `fill="url(#gradient)"` or CSS Grid inside SVG - Draw arrowheads as explicit `` elements; `marker-end` with `orient="auto"` does not rotate in WeasyPrint ### Content rules | Rule | Detail | |---|---| | No section divider slides | Use `.eyebrow` for section numbering instead; saves one slide per section | | No CJK parentheses | Replace `(...)` with `·` or `,` | | Ghost deck test | Read only slide titles in order. They must tell the argument; disconnected titles mean the structure is not ready | | One evidence shape | Each slide has one primary proof form: chart, table, screenshot, code, quote, or conclusion. Split mixed evidence | | One line per bullet | Trim until each item fits on one line; never let it wrap | | Empty space ≥50% | Draft defect. Order: merge with a neighbor > remove the slide and fold its useful point into another slide > convert existing evidence only when another form is clearer. Never add a callout, chart, or image just to occupy space. | | Empty space 25-50% | Acceptable when one complete assertion and its proof genuinely need the room. Otherwise merge or tighten the slide; do not add supporting matter solely for density. | | Cover | No horizontal rule; title centered `38pt`; subtitle on one line; bottom meta centered | Image-heavy decks carry two acceptance bars: the visual brief (crop notes, prompt fragments, generation instructions) is internal working material and never appears in slide titles, body copy, or captions; and the deck uses the existing `.c2`, `table.t2x2`, `.co`, data table, and inline figure patterns unless the source material clearly needs something else. How you plan toward that (slot map, outline, or otherwise) is your call; whatever you sketch is a rhythm check, not a locked layout registry. If the user provides a real PPTX or brand template and explicitly asks to preserve it, do a template inventory before content editing: thumbnail the source deck, identify reusable layout families, then map each section to an existing layout. Do not do this for the default WeasyPrint or Marp paths; Kami templates are already the inventory. ### Troubleshooting | Symptom | Fix | |---|---| | Content overflows to next page | Add `max-height` or trim content | | 2×2 columns misaligned | Switch from CSS Grid to `table.t2x2` | | Large blank at slide bottom | First check item count (target 3-5 items per slide). Merge or remove a weak slide before changing layout. Only reduce page size when the entire deck is uniformly sparse. | | CJK text looks tight | Add `letter-spacing: 0.3pt` | ### Core principles 1. `letter-spacing` matters more than `font-size` for CJK density 2. No white card panels on parchment; use spacing first, then one neutral hairline only when a boundary must be explicit ### Marp variant Marp is an optional third path, alongside WeasyPrint HTML and python-pptx. Use it only when the user explicitly asks for Marp, "markdown slides", or a deck that lives in a `.md` file. The repo does not bundle `marp-cli`; rendering happens with the user's local install. Files: | Asset | Path | |---|---| | CJK theme CSS (CN, JP/KO best-effort) | `assets/templates/marp/slides-marp.css` | | EN theme CSS | `assets/templates/marp/slides-marp-en.css` | | CJK sample deck | `assets/templates/marp/slides-marp.md` | | EN sample deck | `assets/templates/marp/slides-marp-en.md` | Shared with WeasyPrint slides: every design token (`--parchment`, `--brand`, `--serif`, `--mono`), the Kami class scale (`.eyebrow`, `.lead`, `.mt`, `.ml`, `.mb`, `.mc`, `.co`, `.c2`, `table.t2x2`, `table.data`, `section.cover`), and the 280×158mm page size. The Marp theme is a port, not a redesign. Marp-specific additions on top of that port: the theme styles bare `

`, `