Kiluth Document Spec
Department
Summary
Defines what “Kiluth-styled document” means — the HTML/CSS spec, component vocabulary, and editorial house rules for producing PDFs that match Kiluth’s brand. Any tool (Claude Code skill, AI tool, manual HTML, server-side renderer) that conforms to this spec produces visually consistent Kiluth documents.
Table of Contents
| Section | |
|---|---|
| 1 | Summary |
| 2 | Purpose |
| 3 | Visual Rules (Automatic) |
| 4 | Component Vocabulary |
| 5 | Editorial House Rules |
| 6 | Page Layout & Print Behavior |
| 7 | Rendering Pipeline |
| 8 | Authoring Checklist |
Purpose
Kiluth documents (proposals, quotations, briefs, agreements, reports) share a single visual language. This spec is the canonical reference for that language. Read it if you want to:
- Produce a Kiluth-styled PDF from any source (markdown, HTML, AI prompt)
- Verify a third-party tool conforms to the brand
- Train or prompt an AI to generate Kiluth-styled content
- Extend the system with new components
| Outcome |
|---|
| Any output that obeys this spec looks like a Kiluth document, regardless of who or what produced it. |
Visual Rules (Automatic)
These rules are baked into the template’s CSS. You don’t author them — they apply to any conforming HTML automatically.
| # | Rule | Detail |
|---|---|---|
| 1 | Page size | 522 × 756 pt (custom, ~7.25” × 10.5”) |
| 2 | Page margins | 90pt top, 72pt sides, 102pt bottom (so the footer has breathing room) |
| 3 | Body font | Theinhardt 10pt (Latin) + IBM Plex Sans Thai 10pt (Thai), line-height 1.5 |
| 4 | Heading weights | Regular (400) + small -webkit-text-stroke to approximate the unavailable Theinhardt Medium |
| 5 | Vertical rails | Between every column except after the last, color #c8c6bf |
| 6 | Horizontal rules | Between every row except after the last, color #c8c6bf |
| 7 | Letterhead | Logo top-left, “Confidential” (or other label) top-center, contact info top-right |
| 8 | Footer | Address bottom-left, page number bottom-right |
| 9 | Every <h1> starts a new page | Automatic via page-break-before: always |
| 10 | Headings stick to their content | page-break-after: avoid on h1–h6 prevents widow titles |
| 11 | Last row of a multi-row table doesn’t get orphaned on a new page | tr:last-child:not(:first-child) { page-break-before: avoid } |
Component Vocabulary
Cover Page
Default cover is auto-generated from a title + a single label/value pair (typically “Department”). For docs that need additional cover fields (Customer, Date, Project Code, Parties), write a custom cover as the first element of the body content:
<section class="cover">
<h1>Document Title</h1>
<div class="cover-dept-label">Client</div>
<div class="cover-dept-value">Customer Co., Ltd.</div>
<div class="cover-dept-label">Date</div>
<div class="cover-dept-value">2026-05-24</div>
</section>A custom cover suppresses the default. Multiple label/value pairs auto-space with 18pt between groups.
Two-line cover (title + descriptor). Add a .cover-subtitle element directly under the <h1>:
<section class="cover">
<h1>Document Title</h1>
<div class="cover-subtitle">Short descriptor line</div>
<div class="cover-dept-label">Client</div>
<div class="cover-dept-value">Customer Co., Ltd.</div>
</section>.cover-subtitle renders at 13pt (smaller than h1, larger than body) with normal weight.
NEVER use a <table> for cover fields. The template styles .cover-dept-label / .cover-dept-value as a clean label-above-value stack. Wrapping the same fields in a <table> draws cell borders + vertical rails (universal table rules) and the cover renders as a data grid instead of a field list.
Pick subtitle OR cover field for the same content, not both. If “E-commerce Excluded” goes in a .cover-subtitle under the <h1>, don’t ALSO emit it as a Scope: E-commerce Excluded field. Either is fine; both is redundant duplication on the cover.
“Department” is owning-team metadata, not a customer-facing cover field. It’s passed via the --department flag to the renderer. Don’t add a <div class="cover-dept-label">Department</div> block unless the source document explicitly shows Department on its cover — for customer-facing docs (quotations, agreements), use Customer / Scope / Date instead.
Headings
<h1>Section Title</h1> <!-- 21pt, starts new page -->
<h2>Subsection</h2> <!-- 12pt, inline -->
<h3>Sub-subsection</h3> <!-- 11pt, inline -->Always use semantic <h1>–<h6> for titles. Never fake a heading with <p><strong>Title</strong></p> — the page-break-avoidance rules only fire on real heading tags.
Paragraph + inline
<p>Standard prose. <strong>Bold</strong>, <em>italic</em>,
<a href="https://www.kiluth.com">link</a>, <code>inline code</code>.</p>Table of Contents
Use a div-based stack, not a <table>:
<h1>Table of Contents</h1>
<div class="toc-list">
<div class="toc-item">Summary<span class="toc-page">2</span></div>
<div class="toc-item">Approach<span class="toc-page">5</span></div>
</div>Page numbers are static (estimated by section length). Include a TOC when the document is multi-section and > ~5 pages; skip for short docs.
Strip leading numbering from TOC entries. If a section heading is <h1>1. Project Summary</h1> or <h1>1. บทสรุปโครงการ</h1>, the matching .toc-item text is Project Summary / บทสรุปโครงการ — not 1. Project Summary / 1. บทสรุปโครงการ. The number stays on the heading itself; in the TOC it’s visual noise. Applies to 1., 1), (1), 01., and any other leading enumerator.
Multi-column tables (4+ cols) — Layout A vs Layout B
Total page content width is 378pt (522pt page − 72pt × 2 side margins). For tables with 4+ columns, pick a layout BEFORE composing.
Layout (A) — ALL SHORT COLUMNS. Every column is a label, number, or short phrase. Set explicit widths on the first row’s <td> cells summing to ≤378pt:
<table>
<thead>
<tr>
<th style="width: 32pt">#</th>
<th>Description</th>
<th style="width: 42pt">Days</th>
<th style="width: 70pt">Price (THB)</th>
</tr>
</thead>
...
</table>Layout (B) — ONE PARAGRAPH COLUMN. Use a 2-row-per-record pattern: top row carries the structured fields, bottom row spans full width with the paragraph:
<table>
<thead>
<tr><th>Category</th><th>Component</th><th>Provider</th><th>Monthly</th><th>Notes</th></tr>
</thead>
<tbody>
<tr><td>Frontend</td><td>Static Hosting</td><td>Vercel</td><td>1,500</td><td>Edge cache</td></tr>
<tr><td colspan="5" style="padding-left: 12pt; color: #6a6a6a; font-size: 8.5pt;">Where the website itself lives — Vercel stores and serves the web pages quickly around the world.</td></tr>
...
</tbody>
</table>Layout B is MANDATORY (no judgment call) when ANY trigger fires:
- A column header is “Explanation”, “Description”, “Justification”, “Rationale”, “Reason”, “Comment”, “Detail/Details”, or “Notes” (when Notes carries sentences vs. short phrases) — anything prose-implying
- Any cell in a column contains a complete sentence (capital letter + ends with period)
- Any cell in a column has >10 words
- The doc is a “Technical Expenses” / “Cost Breakdown” / “Pricing Detail” table with 5+ columns
Anti-pattern: Don’t squeeze a paragraph into a narrow 6th column — it wraps one-word-per-line and the doc looks broken. That’s Layout A misapplied.
Multi-row scope tables (sections + subsections + bullets)
For tables like a Website Development scope (sections numbered 1, 2, 4, … with optional subsections 1.1, 1.2, … and bullet lists describing each item):
ONE row per leaf section. If a section has NO subsections (e.g. “Project Management — 10 days — 50,000”), emit ONE row that carries the section number + name + days + price together. Do NOT emit a separate “section header row” PLUS a “data row” for the same section.
- Section WITH subsections (1.1, 1.2 etc.): one section-header row spanning Description (no Days/Price), then subsection rows below with Days/Price filled.
- Section WITHOUT subsections: one row carrying everything.
Bullets and sub-content stay INSIDE the table. Emit them as the NEXT row in the same table: <tr><td></td><td colspan="N-1"><ul><li>...</li></ul></td></tr> (leave the # cell empty). DO NOT close the table, drop a standalone <ul>, then start a new table fragment — that breaks the column grid and the table header repeats at the top of every new page (visible signal you got this wrong). One <table> element per logical table.
<table>
<thead><tr><th>#</th><th>Description</th><th>Days</th><th>Price (THB)</th></tr></thead>
<tbody>
<!-- Section WITH subsections -->
<tr><td colspan="4"><strong>1. UX Design</strong></td></tr>
<tr><td>1.1</td><td>Strategy Handoff & UX Kickoff</td><td>1</td><td>5,000</td></tr>
<tr><td></td><td colspan="3"><ul><li>Internal review</li><li>Handoff meeting</li></ul></td></tr>
<tr><td>1.2</td><td>Initiate UX Design</td><td>11</td><td>45,000</td></tr>
<tr><td></td><td colspan="3"><ul><li>Refine sitemap</li><li>Translate journeys</li></ul></td></tr>
<!-- Section WITHOUT subsections — ONE row, no separate header -->
<tr><td>5</td><td><strong>Project Management — Bonsoir Website</strong></td><td>10</td><td>50,000</td></tr>
<tr><td></td><td colspan="3"><ul><li>Project planning</li><li>Weekly check-ins</li></ul></td></tr>
</tbody>
</table>Plain 2-column table
<table>
<thead><tr><th>Column A</th><th>Column B</th></tr></thead>
<tbody>
<tr><td>row 1 a</td><td>row 1 b</td></tr>
<tr><td>row 2 a</td><td>row 2 b</td></tr>
</tbody>
</table>Vertical rail between cols + horizontal rules between rows appear automatically. Last row has no closing rule.
Multi-column grid table
For 3+ columns. Optional <th colspan="N"> row for a section title above the column headers.
<table>
<thead>
<tr><th colspan="4">Section title</th></tr>
<tr><th>#</th><th>Description</th><th>Day</th><th>Price</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>Project initiation</td><td>1</td><td>8,000</td></tr>
</tbody>
</table>Numbered table — class="numbered"
Narrow right-aligned first column (67pt). Use for numbered enumerations, checklists, system-setup steps.
<table class="numbered">
<thead><tr><th></th><th>Section title</th></tr></thead>
<tbody>
<tr><td>1</td><td>First item</td></tr>
<tr><td>2</td><td>Second item</td></tr>
</tbody>
</table>Checkbox glyphs (☐) work in the first column for checklist patterns.
Single-row callout
For one label paired with one piece of content (Link, Outcome, Reference). Set the first td width inline so the label column doesn’t take half the page:
<table>
<tbody>
<tr>
<td style="width: 67pt;">Outcome</td>
<td>The shipped feature reduces support tickets by 40%.</td>
</tr>
</tbody>
</table>✓ / ✕ correctness table
For do/don’t pairings. Unicode glyphs go in the label cell. Lock first-column width to 67pt for visual alignment with other label tables:
<table>
<thead><tr><th style="width: 67pt;"></th><th>Example</th></tr></thead>
<tbody>
<tr><td>✓ Correct</td><td>Lead with the answer, then explain.</td></tr>
<tr><td>✕ Incorrect</td><td>Bury the conclusion in a long preamble.</td></tr>
</tbody>
</table>Form table — class="form"
For Email Templates, Asana Cards, anything with a label column + a multi-paragraph body that needs to flow across pages cleanly. Use <tr class="title"> for the title row and <tr class="end"> for the LAST row of each logical block (Subject, Content, Title, Assignee, Description).
<table class="form">
<tbody>
<tr class="title"><th></th><th>Example Email Template</th></tr>
<tr class="end"><td>Subject</td><td>New Employee Information</td></tr>
<tr><td>Content</td><td>Hello [Name],</td></tr>
<tr><td></td><td>Welcome to Kiluth...</td></tr>
<tr><td></td><td>Best regards,<br>[HR Name]</td></tr>
</tbody>
</table>Calendar / grid table
For multi-column timeline grids (week-by-task layouts). Each cell carries <strong>Name</strong><br>Description:
<table>
<thead>
<tr><th>Month</th><th colspan="4" style="text-align: right;">1</th></tr>
<tr><th>Weeks</th><th>1</th><th>2</th><th>3</th><th>4</th></tr>
</thead>
<tbody>
<tr>
<td></td>
<td><strong>Draft UX Design</strong><br>Wireframe, User Flow</td>
<td><strong>Finalize UX Design</strong><br>Final UX</td>
<td>...</td><td>...</td>
</tr>
</tbody>
</table>Card stack
For repeating label/value blocks (Project Duration, Expected Deliverables, Persona profiles). Just stack multiple plain 2-col <table> blocks — the table margin gives the spacing between cards. The first row of each card is the title (Month, Category, Phase):
<table>
<tbody>
<tr><td style="width: 96pt;">Month</td><td>1</td></tr>
<tr><td>Main Objective</td><td>Prepare Backend Structure + Design UX/UI</td></tr>
<tr><td>Details</td><td>...</td></tr>
</tbody>
</table>
<table>
<tbody>
<tr><td style="width: 96pt;">Month</td><td>2</td></tr>
...
</tbody>
</table>Signatures — class="signatures"
Two signing parties side-by-side with no vertical divider. Flex layout, not a table:
<div class="signatures">
<div class="signature">
<div class="signature-label">Signed …………………………………….. Contractor</div>
<div>Mr Phuttiphan Samranruen</div>
<div>Account Executive</div>
<div>Kiluth Co., Ltd.</div>
</div>
<div class="signature">
<div class="signature-label">Signed …………………………………….. Client</div>
<div>………………………………….</div>
<div>Client Co., Ltd.</div>
</div>
</div>Page-break-inside is avoided so the whole block stays on one page.
Blockquote / code / image / horizontal rule
Standard semantic HTML — all styled to match Kiluth’s body conventions.
<blockquote>A pulled-out quote in muted gray with a left rail.</blockquote>
<pre><code>$ npm install
$ npm run build</code></pre>
<img src="path/to/image.png" alt="description">
<hr>Editorial House Rules
These are not enforced by CSS — they’re authoring discipline. Follow them when composing content.
| # | Rule | Why |
|---|---|---|
| 1 | No auto-numbering of section headings. Render heading text verbatim. If a heading text doesn’t start with a number, don’t add one. | Avoids the TOC and body both numbering, which reads like double-counting. |
| 2 | No invented prefixes (Section 1:, Chapter 1:, Part A:). | Same reason. |
| 3 | Don’t re-organize content into a hierarchy the user didn’t supply. Keep flat content flat. | Source fidelity. |
| 4 | TOC is optional. Include for multi-section docs > ~5 pages; skip for short docs (briefs, single-page memos). User preference wins. | Short docs don’t need navigation. |
| 5 | Prefer class="numbered" over <ul> / <ol> for top-level LISTS (sequences of short items: checklist, enumeration, ordered steps). Reserve bullet/numbered lists for items inside other components (form-table cells, table cells). | Kiluth house style — numbered tables read cleaner and more deliberately than browser default list styling. |
| 6 | Always use h1-h6 for titles. Never fake a heading with bolded paragraphs. | Page-break-avoid rules only fire on semantic heading tags. |
| 7 | Match source structure when one is referenced. Don’t invent stylistic flourishes the source doesn’t have. | Preserves intent. |
| 8 | Heading-vs-list distinction. Rule 5 applies to LISTS (item sequences). It does NOT apply to numbered section HEADINGS. If source has **1. Payment Milestones** / **2. Change Requests** as bold subheadings each followed by prose body, render as <h2>1. Payment Milestones</h2> + prose, NOT as a 3-col numbered table. Each numbered item with its own prose body = section (h2). Each numbered item as a one-liner = list (numbered table). | Loses heading semantics and makes prose feel like data. |
| 9 | Convert markdown bullets to real <ul><li>. When source has - item bullets inside a table cell, emit <ul><li>item</li></ul>. Don’t paste raw - prefixes as text — Chrome’s headless renderer doesn’t post-process them; literal ”- item” stays as ”- item” with no bullet character, no hanging indent. | Bullets only look like bullets when they ARE bullets. |
| 10 | Never add <div style="page-break-after: always;"> after an h1. Every <h1> already carries page-break-before: always. An extra break pushes the following content to a THIRD page and orphans the h1 alone on a wasted page. | Double break = wasted page. |
| 11 | Preserve bilingual content as-is. If source has 22 พฤษภาคม 2026 (22 May 2026), keep both Thai + English. If signature labels are bilingual (ลงชื่อ ... ผู้รับจ้าง (Contractor)), keep them bilingual — never collapse to English-only. | Honors source intent across both audiences. |
| 12 | Preserve source section numbering even when it has gaps. If source jumps 1, 2, 4, 5, 9 — keep the gaps. Don’t silently renumber to 1–5; if the gap looks unintentional, flag it during iteration (Stage A) before rendering. | Source fidelity. |
| 13 | Flag source defects before rendering. Empty TOTAL/Subtotal cells (compute and offer the sum), section numbering gaps (confirm intentional), column-header typos (fix and note). Mirror source 1:1 — but call out obvious problems instead of shipping them. | Faithful-mirroring trumps no-invention; flag-defects trumps faithful-mirroring. |
Page Layout & Print Behavior
When to break a page
| Trigger | How |
|---|---|
| New top-level section (h1) | Automatic — every <h1> carries page-break-before: always |
| New subsection on a fresh page (h2 without parent h1 above it) | Explicit: <div style="page-break-after: always;"></div> before the h2 |
| Two adjacent form tables (Email → Asana, etc.) | Inline on the second table: <table class="form" style="page-break-before: always;"> |
| Before a signature block at the end of a doc | Use h1 if it’s a “Signatures” section; otherwise the explicit break div |
When NOT to break
- Within a section (h2/h3 flow with their parent h1)
- Mid-paragraph or mid-table
- Just because there’s extra whitespace at the bottom — better than a forced fresh page that’s mostly empty
Title-content continuity
A title must never be separated from its content. If the title would land at the bottom of a page with the content on the next, the title gets pushed forward to where its content is. The template enforces this automatically via page-break-after: avoid on h1–h6 — but only if the title is a semantic heading tag.
Rendering Pipeline
The spec is the visual contract. How HTML becomes PDF is a separate concern, and there are multiple valid paths:
| Path | Use when |
|---|---|
Claude Code with the kiluth-document skill (internal) | Authoring + rendering inside the Kiluth workspace. AI-mediated content composition, dept-routed artifact storage, lessons feedback loop. |
Manual chrome --headless --print-to-pdf | One-off renders of hand-authored HTML. No tooling beyond a local Chrome install. |
Server-side Puppeteer / @sparticuz/chromium (planned) | Programmatic rendering from a backend service (e.g. Kiluth Chat, scheduled jobs). |
All three paths consume HTML that conforms to this spec and produce visually identical output.
The CSS template files live in the workspace at skills/kiluth-document/template-en.html and template-th.html. They are the source of truth for the visual rules.
Template wrapper (download the canonical HTML)
The spec describes the BODY content patterns (cover, tables, signatures, etc.). The full HTML wrapper — @page rules, @font-face for Theinhardt + IBM Plex Sans Thai, letterhead with embedded Kiluth logo SVG, footer with address/tax-ID/page-counter — lives in the template file. Authoring tools (Claude Code skill, kiluth-api-2 service) include it automatically; external/manual rendering paths must fetch it.
| File | URL |
|---|---|
| English template | https://docs.kiluth.com/static/kiluth-document/template-en.html |
| Thai template | https://docs.kiluth.com/static/kiluth-document/template-th.html |
The template uses two placeholders: {{LABEL}} (letterhead center label, default Confidential) and {{CONTENT}} (where your body HTML gets inserted). Substitute these strings, render with Chrome --headless --print-to-pdf, and you get a brand-conformant PDF.
External-AI authoring workflow:
- Fetch the template HTML for your language (en or th)
- Compose the body content per this spec (cover, sections, tables, signatures)
- Replace
{{LABEL}}with the letterhead label and{{CONTENT}}with your body - Save as a single HTML file and render with
chrome --headless --print-to-pdf=out.pdf in.html
The fonts (Theinhardt + IBM Plex Sans Thai) and the logo SVG load over the network during the print — make sure the rendering environment has network access.
Authoring Checklist
Before rendering any document, verify:
| # | Check |
|---|---|
| 1 | Every section title uses <h1>–<h6>, not bolded paragraphs |
| 2 | Cover is appropriate for doc type (default for simple docs, custom <section class="cover"> for docs with extra fields like Client/Date) |
| 3 | Top-level lists are <table class="numbered">, not <ul> / <ol> |
| 4 | Multi-paragraph email/asana/agreement-style content uses <table class="form"> with tr.title + tr.end |
| 5 | Two-party signature block uses <div class="signatures">, not a 2-cell <table> |
| 6 | TOC uses .toc-list / .toc-item, not a <table> |
| 7 | Section headings don’t carry invented numbering (Section 1:, Chapter A:) |
| 8 | Page breaks only via <h1> (automatic) or explicit style="page-break-*" directives — but NEVER page-break-after immediately after an <h1> |
| 9 | Content faithfully mirrors source structure — no extra hierarchy, no auto-renames |
| 10 | Cover uses .cover-dept-label / .cover-dept-value divs — NEVER a <table> (would draw cell borders) |
| 11 | Cover doesn’t duplicate the same value as both .cover-subtitle and a .cover-dept-value field |
| 12 | 4+ col tables with any prose-bearing column use Layout B (2-row-per-record with colspan paragraph row), not Layout A |
| 13 | Markdown - item bullets inside cells are converted to <ul><li>item</li></ul> (not pasted as literal text) |
| 14 | Numbered subheadings followed by their own prose body are <h2> + prose, NOT a <table class="numbered"> |
| 15 | TOC entries strip leading numbering (1. , (1), etc.) even when the source heading carries it |
| 16 | Bilingual content (Thai+English dates, signature labels) preserved as-is from source |
| 17 | Source defects flagged before rendering — empty TOTAL/Subtotal computed, gaps confirmed, typos noted |