mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat(design-systems): add structured tokens.css schema (default + kami) Compile each brand's DESIGN.md prose into a machine-readable :root block agents paste verbatim, removing the "Primary → --accent" translation step where most token misuse happens. Daemon prompt injection lands in a follow-up; lint-artifact already enforces the shared token vocabulary so no rule changes needed. Schema validated across two contrasting aesthetics: - default (sans-serif, cobalt, B2B utility) — stress test the shallow form, 2-level fg / 2-level surface - kami (serif, parchment, ink-blue, print-first) — stress test the rich form, 4-level fg ramp, 3-level surface, ring elevation, i18n font stacks, and solid-hex tag tints (print renderers double-paint alpha) Schema growth from kami's stress test (5 new optional slots, all backward-compatible — default aliases via var() to existing tokens): - --fg-2 / --meta (4-level fg ramp) - --surface-warm (3-level surface) - --border-soft (2-level border) - --elev-ring (ring elevation as first-class level) Brand-specific extensions live in tokens.css with explicit "NOT in shared schema" labels and a documented promotion path (≥2 brands need it → promote to schema slot). components.html in each brand is a self-contained reference fixture that exercises every token through real layouts. Both fixtures lint clean against apps/daemon/src/lint-artifact.ts. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(design-systems): add token-fixture drift guard Each design system in design-systems/<brand>/ ships two files agents consume in tandem: tokens.css (canonical token bindings) and components.html (a self-contained fixture whose first <style> embeds the same :root paste so the file renders standalone). The fixture's :root block is a copy of tokens.css's :root block, kept in sync only by an inline comment. This adds scripts/check-tokens-fixture-sync.ts and registers it in pnpm guard. The check pairs each brand's tokens.css with its components.html and asserts the unscoped :root block is byte-equivalent after canonical normalization (CSS comments stripped, whitespace collapsed, separator spacing normalized). Brands missing one half of the pair, or with no :root rule in either file, fail the guard. Scoped overrides like :root[lang="zh-CN"] are not required to appear in the fixture (per the kami fixture's inline comment they are pasted only when an artifact's <html lang> matches), so the check only compares the unscoped :root block. Verified: pnpm guard passes for default + kami, fails on intentional value drift, fails on missing token, tolerates whitespace-only formatting differences. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(design-systems): point fixture CTAs to real files Both default and kami components.html advertised in-page anchors (#tokens, #spec, #surface, #accent, #type, #components) but defined no matching ids, so every CTA was a no-op when the fixture was opened locally — flagged by mrcfps in #1231. Re-point each link to a real artifact in the same brand directory: - "View tokens" / "Inspect tokens" / "Inspect typography" → ./tokens.css - "Read the spec" / "Read the rule" → ./DESIGN.md Browsers render these as raw source views, which is the desired UX for a reference fixture: clicking the CTA shows the underlying contract instead of jumping to nothing. Agents copying the fixture also learn the pattern of "buttons link to actual sibling resources". The :root token block is unchanged, so the token-fixture drift guard still passes for both brands. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(design-systems): codify token schema (A1/A2/B/C layers) The two-brand pilot (default + kami) settled the shape of the shared token schema; this commit codifies it as a machine-readable contract and enforces it in pnpm guard, addressing lefarcen's review on #1231: > the optional-vs-required split won't generalize cleanly when brand > #3 needs different Layer A tokens or when multiple brands converge > on the same extension (promoting C→B→A). Consider surfacing that > limitation in the PR narrative or in a future SCHEMA.md. Schema lives under design-systems/_schema/ as three files: - tokens.schema.ts — TypeScript declaration of every shared token with its layer (A1-identity / A1-structure / A2 / B-slot), plus per-brand C-extension allowlists and a global C-prefix allowlist - defaults.css — CSS mirror of A2 fallback values, used as the human-readable contract reviewer's-eye copy and the future input to the derive script - AGENTS.md — schema layer model, C → B-slot → A2 promotion rules, when-not-to-add-a-token guidance Layer model: A1-identity 8 tokens — bg/surface/fg/muted/border/accent + font-display/font-body. The brand IS these values; no fallback is defensible. A1-structure 18 tokens — type scale (8), leading (2), tracking (1), section-y (3), container (4). Structural decisions vary per brand by design and have no cross-brand default. A2 26 tokens — accent states, semantic colors, motion, base spacing scale, radius, elevation, focus, font-mono. Required in every tokens.css; fallback lives in defaults.css for the future derive script to inline when DESIGN.md does not specify the value. B-slot 4 tokens — fg-2 / meta / surface-warm / border-soft. Brand may bind independently or alias the named sibling via var(...) for components that target the richer ramp. C-extension n tokens — brand-specific names (kami's tag-bg-*, leading-display, accent-light, etc.). Allowlisted per-brand in BRAND_EXTENSIONS or globally by prefix in BRAND_EXTENSION_PREFIXES. Promote when a second brand adopts the same name. Why A2 fails the guard today: Artifacts are generated by agents pasting one brand's :root block into a single <style>; there is no global stylesheet that supplies fallbacks at runtime. A tokens.css missing an A2 declaration would silently break any var() reference in the fixture. Until the derive script (PR-B) lands and inlines defaults, every brand's tokens.css must declare every A2 token directly. The guard enforces this strictly. Why --font-mono lands in A2 (not A1): 149 brands' DESIGN.md files were surveyed: 87 (58%) declare a monospace stack, 62 (42%) do not — including major brands like bmw / nike / apple / notion / mastercard / meta. Agent paste cannot rely on the brand author having written it down; a defaultable A2 fallback (with CJK brands like kami overriding) is safer than forcing every brand author to add a field they may not realize their kbd / code-block components need. Five guard checks, each registered as its own entry in scripts/guard.ts so failures attribute to a specific contract: 1. token-fixture sync — components.html :root ↔ tokens.css :root byte-equivalent (existing) 2. A1 required tokens — every brand declares every A1 token 3. A2 required tokens — every brand declares every A2 token 4. unknown token allowlist — every declared token is in schema or brand-extension allowlist 5. A2 defaults parity — defaults.css ↔ tokens.schema.ts fallback byte-equivalent Verified on default + kami: - 26 A1 tokens declared in both brands - 26 A2 tokens declared in both brands - 129 total declarations, all match shared schema or brand extensions - defaults.css ↔ tokens.schema.ts parity holds - sanity test: drifting --motion-fast in defaults.css fails check 5 with a clear divergence message The PR description originally listed "Dedicated SCHEMA.md" as explicitly NOT in this PR ("Once 3+ brands ship, extracting a single source of truth becomes worthwhile"). That boundary moves: lefarcen's review surfaced the schema-generalization risk, and the schema must exist as a machine-enforced contract before the derive script can read it. The TS file replaces the markdown that was deferred. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web/tests): pass missing designTemplates prop to ProjectView Pre-existing typecheck regression on main: PR #955 (b5eb8c16, "generic skills + split skills/design-templates + finalize-design API") added required `designTemplates: SkillSummary[]` to ProjectView Props but updated only two of the three test fixtures that render ProjectView directly. The third — ProjectView.api-empty-response.test.tsx — was missed, so `pnpm typecheck` (and CI on any PR merging into main) fails on: apps/web/tests/components/ProjectView.api-empty-response.test.tsx (168,6): error TS2741: Property 'designTemplates' is missing in type ... The other two ProjectView tests already pass `designTemplates={[]}`, so this aligns this fixture with the existing pattern. Out of scope for #1231 strictly, but the regression blocks the merged-state typecheck CI runs that #1231 triggers, and the one-line fix here restores main's typecheck health for everyone. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(design-systems): enforce B-slot required tokens in pnpm guard Closes mrcfps + lefarcen review comment thread on #1231: > The guard validates A2 required tokens here, but there's no > sibling check for B-slot aliases (--fg-2, --meta, --surface-warm, > --border-soft). Per the schema docs, every brand must declare > A1 + A2 + B-slot names so shared components can safely read > var(--fg-2) etc. Without a B-slot guard, a brand can omit those > aliases, pass pnpm guard, and break any artifact that references > them. Same artifact-paste constraint as A2: agents render artifacts by pasting one brand's :root block into a single <style>; there is no runtime cascade, so a missing B-slot makes any var(--fg-2) reference resolve to nothing. Until now the schema narrative claimed B-slots were optional with a var() default, but no machine check enforced declaration — a contract gap reviewers reasonably refused to merge. This commit closes the gap in three places so machine and narrative agree: 1. scripts/check-tokens-fixture-sync.ts - Add checkDesignSystemBSlotRequiredTokens, mirroring the A2 check but using getBSlotNames() from the schema. - Failure message names each missing slot AND the schema-suggested alias (--fg-2 (default alias: var(--fg))) so a brand author fixing the failure has a copy-pasteable resolution. - Renumber section comments: 5 checks → 6 checks. 2. scripts/guard.ts - Register the new check between A2 required and unknown allowlist so failures attribute to a specific contract. 3. design-systems/_schema/AGENTS.md - Update the layer table: B-slot row's "If omitted" column changes from "resolves via var() to a richer sibling" to "guard fails — brand must declare, either as var(--sibling) (collapsed) or independent value (richer)". - Add a "Why B-slot is required (and what the alias is for)" section that distinguishes the schema-suggested alias from a runtime fallback, with worked examples for default (alias) and kami (independent bind). Verified on default + kami: - pnpm guard passes all 6 design-system checks - 4 B-slot tokens declared in both brands (default aliases via var(), kami binds independently — both forms satisfy the contract) - pnpm typecheck clean across the workspace - Sanity test: removing --fg-2 + --meta from default/tokens.css fires the new guard with a precise per-token alias hint: [default] design-systems/default/tokens.css is missing 2 B-slot tokens (alias the named sibling via var(...) or bind independently): --fg-2 (default alias: var(--fg)), --meta (default alias: var(--muted)) The schema contract is now machine-enforced end-to-end (A1 + A2 + B-slot all required-with-fixed-form-of-fallback). The derive script in PR-B can rely on every brand's tokens.css containing every shared slot name. Co-authored-by: Cursor <cursoragent@cursor.com> * test(e2e): skip leading-underscore meta-directories under design-systems/ CI for #1231 went red on `Validate workspace` after merging origin/main. Cause is a clean collision between two recently-landed changes: - main #1270 (be77dc03"Default English resource i18n fallback") tightened tests/localized-content.test.ts so every directory under design-systems/ is run through assertResourceId() with the strict RESOURCE_ID_PATTERN /^[a-z0-9][a-z0-9-]*$/. - this branch #1231 introduced design-systems/_schema/ as the home of the shared token contract (tokens.schema.ts, defaults.css, AGENTS.md). The leading underscore signals "meta-directory, not brand" — the same convention SCSS partials, Jekyll, Hugo all use. The two changes never met until CI built the merge commit, where assertResourceId('_schema') deterministically failed: Error: Design system directory _schema has malformed resource id: _schema at invariant tests/localized-content.test.ts:66:11 at assertResourceId tests/localized-content.test.ts:71:3 at readDesignSystemResources tests/localized-content.test.ts:202:8 Fix tightens readDesignSystemResources's directory filter so the leading-underscore convention is recognised explicitly: .filter((entry) => entry.isDirectory() && !entry.name.startsWith('_')) This aligns with what apps/daemon/src/design-systems.ts:listDesignSystems already does implicitly — it requires DESIGN.md per directory, so _schema/ was always invisible at runtime; the test was the only place that surfaced it. Verified locally on the post-merge tree: - pnpm test (e2e vitest) — tests/localized-content.test.ts: 4 passed - pnpm guard — all 6 design-system checks pass on default + kami - pnpm typecheck — clean across the workspace (after pnpm install to pull deps for tools/pr that arrived with main) The fix is intentionally narrow (one filter line in one test) and documents the convention inline so future meta-directories under design-systems/ (e.g. _archive/, _drafts/) are covered for free. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: chaoxiaoche <chaoxiaoche@192.168.10.16> Co-authored-by: Cursor <cursoragent@cursor.com>
601 lines
22 KiB
HTML
601 lines
22 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>kami — reference components</title>
|
|
<meta
|
|
name="description"
|
|
content="Reference fixture for design-systems/kami. Every visible
|
|
token comes from tokens.css; the page itself follows kami's
|
|
print-first rules — parchment background, ink-blue accent
|
|
capped at ≤5%, serif at one weight, no italic, no rgba tints,
|
|
no hard drop shadows, no cool grays."
|
|
/>
|
|
|
|
<style>
|
|
/* :root paste — sourced verbatim from
|
|
design-systems/kami/tokens.css. Keep this block in sync
|
|
when tokens.css changes. CJK overrides (`:root[lang="zh-CN"]`,
|
|
`:root[lang="ja"]`) live in tokens.css and should be pasted
|
|
only when the artifact's <html lang="..."> matches. */
|
|
:root {
|
|
--bg: #f5f4ed;
|
|
--surface: #faf9f5;
|
|
--surface-warm: #e8e6dc;
|
|
|
|
--fg: #141413;
|
|
--fg-2: #3d3d3a;
|
|
--muted: #504e49;
|
|
--meta: #6b6a64;
|
|
|
|
--border: #e8e6dc;
|
|
--border-soft: #e5e3d8;
|
|
|
|
--accent: #1b365d;
|
|
--accent-on: #faf9f5;
|
|
--accent-light: #2d5a8a;
|
|
--accent-hover: var(--accent);
|
|
--accent-active: #142a48;
|
|
|
|
--success: #4a6b3a;
|
|
--warn: #8a6b1f;
|
|
--danger: #8a3a30;
|
|
|
|
--font-display:
|
|
Charter, Georgia, Palatino, "Times New Roman", serif;
|
|
--font-body:
|
|
Charter, Georgia, Palatino, "Times New Roman", serif;
|
|
--font-mono:
|
|
"JetBrains Mono", "SF Mono", "Fira Code", Consolas, Monaco,
|
|
"TsangerJinKai02", "Source Han Serif SC", monospace;
|
|
|
|
--text-xs: 11px;
|
|
--text-sm: 12px;
|
|
--text-base: 14px;
|
|
--text-md: 15px;
|
|
--text-lg: 17px;
|
|
--text-xl: 22px;
|
|
--text-2xl: 32px;
|
|
--text-3xl: 48px;
|
|
--text-4xl: 96px;
|
|
|
|
--leading-display: 1.1;
|
|
--leading-tight: 1.25;
|
|
--leading-body: 1.55;
|
|
--leading-dense: 1.4;
|
|
|
|
--tracking-display: -1.2px;
|
|
--tracking-eyebrow: 1.2px;
|
|
--tracking-label: 0.4px;
|
|
|
|
--space-1: 4px;
|
|
--space-2: 8px;
|
|
--space-3: 12px;
|
|
--space-4: 16px;
|
|
--space-5: 20px;
|
|
--space-6: 24px;
|
|
--space-7: 28px;
|
|
--space-8: 32px;
|
|
--space-12: 48px;
|
|
--space-18: 72px;
|
|
--space-22: 88px;
|
|
|
|
--section-y-desktop: 72px;
|
|
--section-y-tablet: 48px;
|
|
--section-y-phone: 32px;
|
|
|
|
--radius-xs: 2px;
|
|
--radius-sm: 4px;
|
|
--radius-md: 8px;
|
|
--radius-lg: 12px;
|
|
--radius-xl: 16px;
|
|
--radius-pill: 9999px;
|
|
|
|
--elev-flat: none;
|
|
--elev-ring: 0 0 0 1px var(--border);
|
|
--elev-ring-accent: 0 0 0 1px var(--accent);
|
|
--elev-raised: 0 4px 24px rgba(0, 0, 0, 0.05);
|
|
|
|
--focus-ring: 0 0 0 2px var(--accent-active);
|
|
|
|
--motion-fast: 150ms;
|
|
--motion-base: 200ms;
|
|
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
|
|
|
|
--container-max: 1120px;
|
|
--container-gutter-desktop: 64px;
|
|
--container-gutter-tablet: 32px;
|
|
--container-gutter-phone: 16px;
|
|
|
|
--tag-bg-faint: #eef2f7;
|
|
--tag-bg-soft: #e4ecf5;
|
|
--tag-bg-base: #d6e1ee;
|
|
--tag-bg-strong: #d0dce9;
|
|
}
|
|
|
|
/* ─── Reset (minimal) ───────────────────────────────────────── */
|
|
*, *::before, *::after { box-sizing: border-box; }
|
|
html, body { margin: 0; padding: 0; }
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
font-family: var(--font-body);
|
|
font-size: var(--text-base);
|
|
line-height: var(--leading-body);
|
|
font-weight: 400;
|
|
-webkit-font-smoothing: antialiased;
|
|
/* Print fidelity — kami is page-first. The on-screen render
|
|
still benefits from the same color-adjust hint so background
|
|
parchment doesn't get optimized away in print preview. */
|
|
-webkit-print-color-adjust: exact;
|
|
print-color-adjust: exact;
|
|
}
|
|
strong { font-weight: 500; } /* kami forbids synthetic bold (>500) */
|
|
em, i { font-style: normal; } /* kami forbids italic — keep upright */
|
|
|
|
/* ─── Layout primitives ─────────────────────────────────────── */
|
|
.container {
|
|
max-width: var(--container-max);
|
|
margin-inline: auto;
|
|
padding-inline: var(--container-gutter-desktop);
|
|
}
|
|
section {
|
|
padding-block: var(--section-y-desktop);
|
|
}
|
|
section + section {
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
@media (max-width: 1023px) {
|
|
.container { padding-inline: var(--container-gutter-tablet); }
|
|
section { padding-block: var(--section-y-tablet); }
|
|
}
|
|
@media (max-width: 639px) {
|
|
.container { padding-inline: var(--container-gutter-phone); }
|
|
section { padding-block: var(--section-y-phone); }
|
|
}
|
|
|
|
/* ─── Typography ────────────────────────────────────────────── */
|
|
h1, h2, h3 {
|
|
font-family: var(--font-display);
|
|
font-weight: 500;
|
|
margin: 0;
|
|
}
|
|
h1 {
|
|
font-size: var(--text-4xl);
|
|
line-height: var(--leading-display);
|
|
letter-spacing: var(--tracking-display);
|
|
}
|
|
h2 {
|
|
font-size: var(--text-2xl);
|
|
line-height: var(--leading-tight);
|
|
letter-spacing: 0.4px;
|
|
}
|
|
h3 {
|
|
font-size: var(--text-lg);
|
|
line-height: 1.3;
|
|
}
|
|
p { margin: 0; }
|
|
|
|
.lede {
|
|
font-size: var(--text-md);
|
|
line-height: var(--leading-body);
|
|
color: var(--muted);
|
|
}
|
|
.body-muted { color: var(--muted); }
|
|
.body-meta { color: var(--meta); font-size: var(--text-sm); }
|
|
.body-sm { font-size: var(--text-sm); }
|
|
|
|
/* `.eyebrow` is the only place sans is used (and "sans" literally
|
|
equals serif here per DESIGN.md §3 — kami has no separate
|
|
sans family). Uppercase + tracking 1.2px — well above the
|
|
craft 0.06em floor. */
|
|
.eyebrow {
|
|
font-family: var(--font-display);
|
|
font-size: var(--text-sm);
|
|
font-weight: 500;
|
|
line-height: 1;
|
|
color: var(--meta);
|
|
text-transform: uppercase;
|
|
letter-spacing: var(--tracking-eyebrow);
|
|
}
|
|
|
|
/* Section number pattern — DESIGN.md §4: the number IS the
|
|
marker. No underline, no left bar, no eyebrow. Same serif
|
|
as the title, ink-blue, 14px. */
|
|
.section-num {
|
|
font-family: var(--font-display);
|
|
font-weight: 500;
|
|
font-size: var(--text-base);
|
|
color: var(--accent);
|
|
letter-spacing: var(--tracking-label);
|
|
font-variant-numeric: tabular-nums;
|
|
margin-block-end: var(--space-3);
|
|
}
|
|
|
|
.stack-3 > * + * { margin-block-start: var(--space-3); }
|
|
.stack-4 > * + * { margin-block-start: var(--space-4); }
|
|
.stack-6 > * + * { margin-block-start: var(--space-6); }
|
|
|
|
/* ─── Buttons ───────────────────────────────────────────────
|
|
* kami buttons use `box-shadow: 0 0 0 1px ...` as the edge
|
|
* instead of border, so the rectangle aligns perfectly with
|
|
* the fill. Hover lifts via whisper shadow only — no color
|
|
* shift, no transform. This is kami's elevation-led
|
|
* interaction, encoded in tokens. */
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
padding: 8px 14px;
|
|
border: none;
|
|
border-radius: var(--radius-md);
|
|
font-family: var(--font-display);
|
|
font-weight: 500;
|
|
font-size: var(--text-sm);
|
|
line-height: 1;
|
|
letter-spacing: var(--tracking-label);
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
transition:
|
|
box-shadow var(--motion-base) var(--ease-standard);
|
|
}
|
|
.btn:focus-visible {
|
|
outline: none;
|
|
box-shadow: var(--focus-ring);
|
|
}
|
|
.btn-primary {
|
|
background: var(--accent);
|
|
color: var(--accent-on);
|
|
box-shadow: var(--elev-ring-accent);
|
|
}
|
|
.btn-primary:hover {
|
|
/* Layered: ring stays as edge; whisper adds lift. */
|
|
box-shadow: var(--elev-ring-accent), var(--elev-raised);
|
|
}
|
|
.btn-secondary {
|
|
background: var(--surface-warm);
|
|
color: var(--fg-2);
|
|
box-shadow: var(--elev-ring);
|
|
}
|
|
.btn-secondary:hover {
|
|
box-shadow: var(--elev-ring), var(--elev-raised);
|
|
}
|
|
.btn-ghost {
|
|
background: transparent;
|
|
color: var(--accent);
|
|
box-shadow: var(--elev-ring-accent);
|
|
}
|
|
.btn-ghost:hover {
|
|
box-shadow: var(--elev-ring-accent), var(--elev-raised);
|
|
}
|
|
|
|
/* ─── Cards ─────────────────────────────────────────────────
|
|
* Cards lift one shade above bg (ivory on parchment). 1px
|
|
* hairline border + whisper shadow on hover — no transform,
|
|
* no brightness shift. */
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
padding: var(--space-7) var(--space-7) var(--space-6);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-3);
|
|
transition: box-shadow var(--motion-base) var(--ease-standard);
|
|
}
|
|
.card:hover {
|
|
box-shadow: var(--elev-raised);
|
|
}
|
|
|
|
/* ─── Tags / chips ──────────────────────────────────────────
|
|
* Solid hex backgrounds, NEVER rgba — print renderers double-
|
|
* paint alpha. The pre-blends live as --tag-bg-* tokens.
|
|
* radius is 2px, 4px max — kami forbids pill chips with heavy
|
|
* borders. */
|
|
.tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
font-family: var(--font-display);
|
|
font-weight: 500;
|
|
font-size: var(--text-sm);
|
|
line-height: 1;
|
|
color: var(--accent);
|
|
letter-spacing: var(--tracking-label);
|
|
padding: 4px 8px;
|
|
border-radius: var(--radius-xs);
|
|
background: var(--tag-bg-soft);
|
|
}
|
|
.tag-faint { background: var(--tag-bg-faint); }
|
|
.tag-strong {
|
|
background: var(--tag-bg-strong);
|
|
padding: 4px 10px;
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
|
|
/* ─── Quote ─────────────────────────────────────────────────
|
|
* Left rule in ink-blue, olive body, 1.55 line-height. The
|
|
* single place letter-spacing 0.05em earns its keep. */
|
|
.quote {
|
|
border-left: 2px solid var(--accent);
|
|
padding: var(--space-1) 0 var(--space-1) var(--space-3);
|
|
font-family: var(--font-display);
|
|
font-weight: 400;
|
|
font-size: var(--text-md);
|
|
line-height: var(--leading-body);
|
|
color: var(--muted);
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
/* ─── Metric (DESIGN.md §4) ─────────────────────────────────
|
|
* Big ink-blue value with tabular-nums so columns of metrics
|
|
* align across the row; olive label below. */
|
|
.metric {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
.metric-value {
|
|
font-family: var(--font-display);
|
|
font-weight: 500;
|
|
font-size: var(--text-xl);
|
|
line-height: 1.1;
|
|
color: var(--accent);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.metric-label {
|
|
font-family: var(--font-display);
|
|
font-weight: 400;
|
|
font-size: var(--text-sm);
|
|
color: var(--muted);
|
|
}
|
|
|
|
/* ─── Dash list (DESIGN.md §4) ──────────────────────────────
|
|
* Bullets are en-dashes in ink-blue. Never filled discs. */
|
|
ul.dash {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-2);
|
|
}
|
|
ul.dash li {
|
|
position: relative;
|
|
padding-left: var(--space-4);
|
|
line-height: var(--leading-body);
|
|
}
|
|
ul.dash li::before {
|
|
content: "\2013"; /* en-dash */
|
|
position: absolute;
|
|
left: 0;
|
|
color: var(--accent);
|
|
}
|
|
|
|
/* ─── Code block ────────────────────────────────────────────
|
|
* Ivory bg + hairline border + warm fg. Mono font has CJK
|
|
* fallback so labels in CJK code don't render as boxes. */
|
|
.code {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
padding: var(--space-3) var(--space-4);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-sm);
|
|
line-height: var(--leading-body);
|
|
color: var(--fg);
|
|
white-space: pre;
|
|
overflow-x: auto;
|
|
}
|
|
.code .k { color: var(--accent); } /* keyword */
|
|
.code .c { color: var(--meta); } /* comment */
|
|
|
|
/* ─── Links ─────────────────────────────────────────────────
|
|
* Default link color is ink-blue. Underline on hover at
|
|
* generous offset so the rule reads as a deliberate gesture,
|
|
* not a default. */
|
|
a {
|
|
color: var(--accent);
|
|
text-decoration: none;
|
|
}
|
|
a:hover {
|
|
text-decoration: underline;
|
|
text-underline-offset: 3px;
|
|
}
|
|
|
|
/* ─── Section-specific layout ───────────────────────────── */
|
|
.hero {
|
|
padding-block: var(--space-22) var(--space-18);
|
|
}
|
|
.hero h1 { margin-block-end: var(--space-6); max-width: 14ch; }
|
|
.hero .lede { max-width: 56ch; }
|
|
.hero-actions {
|
|
display: flex;
|
|
gap: var(--space-3);
|
|
margin-block-start: var(--space-8);
|
|
}
|
|
|
|
.metric-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: var(--space-12);
|
|
}
|
|
@media (max-width: 639px) {
|
|
.metric-row { grid-template-columns: 1fr 1fr; }
|
|
}
|
|
|
|
.features-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: var(--space-5);
|
|
margin-block-start: var(--space-8);
|
|
}
|
|
@media (max-width: 767px) {
|
|
.features-grid { grid-template-columns: 1fr; }
|
|
}
|
|
|
|
.quote-block {
|
|
max-width: 56ch;
|
|
margin-block-start: var(--space-6);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main class="container">
|
|
<!-- ════════════════════════════════════════════════════════════
|
|
HERO — exercises: section-num pattern, h1 (96px serif at
|
|
tracking -1.2px), .lede, .btn-primary (ring-shadow edge),
|
|
.btn-secondary (warm-sand bg), .tag (solid hex). The grid
|
|
is single-column intentionally — kami heroes don't use
|
|
asymmetric two-column layouts; the page IS the column.
|
|
═══════════════════════════════════════════════════════════════ -->
|
|
<section class="hero" data-od-id="hero">
|
|
<p class="section-num">00 · Reference</p>
|
|
<h1>One paper, set carefully, can hold every brand decision.</h1>
|
|
<p class="lede">
|
|
A token system distilled from the kami print rules: warm
|
|
parchment canvas, single ink-blue accent capped under five
|
|
percent of the page, serif at one weight. The fixture you
|
|
are reading uses the same token block agents paste into
|
|
every artifact.
|
|
</p>
|
|
<div class="hero-actions">
|
|
<a href="./tokens.css" class="btn btn-primary">View tokens</a>
|
|
<a href="./DESIGN.md" class="btn btn-secondary">
|
|
Read the spec
|
|
</a>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ════════════════════════════════════════════════════════════
|
|
METRICS — exercises: .metric pattern with tabular-nums
|
|
values, ink-blue at the W500 weight, olive label. Numbers
|
|
describe the fixture itself, not invented claims.
|
|
═══════════════════════════════════════════════════════════════ -->
|
|
<section data-od-id="metrics">
|
|
<p class="section-num">01</p>
|
|
<h2>Schema in numbers</h2>
|
|
|
|
<div class="metric-row" style="margin-block-start: var(--space-8)">
|
|
<div class="metric">
|
|
<div class="metric-value">42</div>
|
|
<div class="metric-label">tokens in :root</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value">3</div>
|
|
<div class="metric-label">font stacks (EN · zh · ja)</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value">≤5%</div>
|
|
<div class="metric-label">accent surface cap</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ════════════════════════════════════════════════════════════
|
|
FEATURE CARDS — exercises: .card (ivory bg, hairline
|
|
border, whisper shadow on hover), h3, body text, dash list,
|
|
tag (solid hex bg).
|
|
═══════════════════════════════════════════════════════════════ -->
|
|
<section data-od-id="components">
|
|
<p class="section-num">02</p>
|
|
<h2>What this fixture exercises</h2>
|
|
|
|
<div class="features-grid">
|
|
<article class="card">
|
|
<span class="tag tag-faint">Surface</span>
|
|
<h3>Three-tier surface ramp</h3>
|
|
<p class="body-muted body-sm">
|
|
Parchment for the page, ivory for cards, warm sand for
|
|
secondary buttons. Cards lift one half-shade above the
|
|
page; the contrast is what gives them edge without a
|
|
shadow.
|
|
</p>
|
|
<ul class="dash body-sm">
|
|
<li>--bg covers the page</li>
|
|
<li>--surface lifts containers</li>
|
|
<li>--surface-warm fills secondary controls</li>
|
|
</ul>
|
|
</article>
|
|
|
|
<article class="card">
|
|
<span class="tag">Accent</span>
|
|
<h3>One color, two states</h3>
|
|
<p class="body-muted body-sm">
|
|
Ink blue is the only chromatic move. Hover holds the
|
|
same color — kami expresses lift through elevation, not
|
|
brightness. Active darkens by a hand-picked value, not a
|
|
formula.
|
|
</p>
|
|
<ul class="dash body-sm">
|
|
<li>--accent appears at most twice on this page</li>
|
|
<li>--accent-hover identical to --accent</li>
|
|
<li>Active state is a hand-picked deeper ink</li>
|
|
</ul>
|
|
</article>
|
|
|
|
<article class="card">
|
|
<span class="tag tag-strong">Type</span>
|
|
<h3>One weight does the work</h3>
|
|
<p class="body-muted body-sm">
|
|
Serif at weight 500 carries every heading. There is no
|
|
bold, no italic, no second face. Hierarchy comes from
|
|
size, tracking, and color — never from another type
|
|
family.
|
|
</p>
|
|
<ul class="dash body-sm">
|
|
<li>--font-display equals --font-body equals serif</li>
|
|
<li>Display tracking compresses (-1.2px at 96px)</li>
|
|
<li>Eyebrow tracking opens (1.2px on uppercase)</li>
|
|
</ul>
|
|
</article>
|
|
|
|
<article class="card">
|
|
<span class="tag">Elevation</span>
|
|
<h3>Ring before whisper before drop</h3>
|
|
<p class="body-muted body-sm">
|
|
Three sanctioned levels. A 1px hairline ring is the
|
|
default edge; a 4-24-rgba whisper appears only on hover.
|
|
The schema reserves --elev-ring as a first-class level so
|
|
brands like this one don't have to misuse --elev-raised
|
|
for hairlines.
|
|
</p>
|
|
</article>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ════════════════════════════════════════════════════════════
|
|
QUOTE + CODE — exercises: .quote (left rule in ink-blue),
|
|
.code with .k / .c spans, link.
|
|
═══════════════════════════════════════════════════════════════ -->
|
|
<section data-od-id="quote">
|
|
<p class="section-num">03</p>
|
|
<h2>The token block, in full</h2>
|
|
|
|
<div class="quote-block">
|
|
<blockquote class="quote">
|
|
kami is not a UI framework. It is a constraint system for
|
|
the page, designed to keep deliverables stable, clear, and
|
|
unmistakably printed.
|
|
</blockquote>
|
|
</div>
|
|
|
|
<pre class="code" style="margin-block-start: var(--space-8)"><span class="c">/* Paste this block into the first <style> of every kami artifact. */</span>
|
|
<span class="k">:root</span> {
|
|
<span class="c">/* Surface */</span>
|
|
<span class="k">--bg</span>: <span>#f5f4ed</span>;
|
|
<span class="k">--surface</span>: <span>#faf9f5</span>;
|
|
<span class="k">--accent</span>: <span>#1b365d</span>;
|
|
<span class="c">/* … 38 more, see tokens.css */</span>
|
|
}</pre>
|
|
|
|
<p class="body-meta" style="margin-block-start: var(--space-4)">
|
|
Full reference at
|
|
<a href="./tokens.css">tokens.css</a> ·
|
|
spec at
|
|
<a href="./DESIGN.md">DESIGN.md</a>
|
|
</p>
|
|
</section>
|
|
</main>
|
|
</body>
|
|
</html>
|