Align app.css with Calm Archive tokens; drop Utopia generator script
Rework public/app.css to use the design-system tokens from
docs/design/domus-tokens.css: the Utopia fluid type/space scale
(320→1240px, body 18→20px, ratio 1.20→1.25), the warm paper/ink palette
with the Clay accent, --radius 14px and the single --shadow-float, and the
web-safe serif/ui/mono font stacks. Token names now use the canonical
--w-* scheme, the wordmark and card title are serif, controls are flat with
concentric radii, catalog labels are mono uppercase, and the preview
placeholder picks up the 45° grain.
Make the utopia skill docs-only: remove the Python generator script and
point at the documented clamp() math and utopia.fyi instead.
diff --git a/.claude/skills/utopia/SKILL.md b/.claude/skills/utopia/SKILL.md
index 6c4209f..920621c 100644
--- a/.claude/skills/utopia/SKILL.md
+++ b/.claude/skills/utopia/SKILL.md
@@ -65,23 +65,16 @@ multiples above.
## Generating tokens
-Prefer the helper script (no network, deterministic):
+Use the math above to compute a step by hand, or feed the project config
+into the interactive generators:
-```bash
-# Print the full :root token block (type + space) for the project config:
-python3 .claude/skills/utopia/scripts/utopia.py
+- Type — <https://utopia.fyi/type/calculator>
+- Space — <https://utopia.fyi/space/calculator>
-# Compute a single clamp() for an arbitrary min→max px pair:
-python3 .claude/skills/utopia/scripts/utopia.py 13.5 15
-# → clamp(0.8438rem, 0.8111rem + 0.163vw, 0.9375rem)
-
-# Override the viewport range or rem base:
-python3 .claude/skills/utopia/scripts/utopia.py --min-vw 320 --max-vw 1240 25.92 31.25
-```
-
-The interactive generator at <https://utopia.fyi/type/calculator> and
-<https://utopia.fyi/space/calculator> produces the same values if you'd
-rather use the UI — feed it the config above.
+Set viewport 320 → 1240, body 18 → 20, ratio 1.20 → 1.25, then copy the
+emitted `--step-*` / `--space-*` clamps. Verify a new value against an
+existing one in `docs/design/domus-tokens.css` (e.g. step 0 must stay
+`clamp(1.125rem, 1.0815rem + 0.2174vw, 1.25rem)`) so the scale matches.
## Using the tokens
diff --git a/.claude/skills/utopia/scripts/utopia.py b/.claude/skills/utopia/scripts/utopia.py
deleted file mode 100755
index c9b3a39..0000000
--- a/.claude/skills/utopia/scripts/utopia.py
+++ /dev/null
@@ -1,98 +0,0 @@
-#!/usr/bin/env python3
-"""Generate Utopia fluid clamp() tokens for Domus.
-
-Utopia (utopia.fyi) interpolates a value between a min size (at the min
-viewport) and a max size (at the max viewport) with a single clamp(). This
-script reproduces that math so type/space tokens can be regenerated without
-the website.
-
-Usage
------
- utopia.py # print the full :root token block
- utopia.py 13.5 15 # one clamp() for a min->max px pair
- utopia.py --min-vw 320 --max-vw 1240 18 20
-"""
-import argparse
-from decimal import Decimal, ROUND_HALF_UP
-
-REM = 16.0 # 1rem assumed = 16px (browser default)
-
-
-def round_(x, n=4):
- # Half-up rounding (matches Utopia), then trim trailing zeros.
- q = Decimal(10) ** -n
- d = Decimal(repr(x)).quantize(q, rounding=ROUND_HALF_UP)
- return format(d.normalize(), "f")
-
-
-def clamp(min_px, max_px, min_vw, max_vw):
- """Return a Utopia-style clamp() string for min_px -> max_px."""
- slope = (max_px - min_px) / (max_vw - min_vw)
- vw = slope * 100
- intercept_rem = (min_px - slope * min_vw) / REM
- return (
- f"clamp({round_(min_px / REM)}rem, "
- f"{round_(intercept_rem)}rem + {round_(vw)}vw, "
- f"{round_(max_px / REM)}rem)"
- )
-
-
-def type_scale(body_min, body_max, ratio_min, ratio_max):
- """Steps -2..5: body * ratio**n (min uses min ratio, max uses max ratio)."""
- steps = {}
- for n in range(-2, 6):
- name = f"--step-{n}" if n >= 0 else f"--step-{n}"
- steps[name] = (body_min * ratio_min**n, body_max * ratio_max**n)
- return steps
-
-
-# Space multiples off the body base, in Utopia's naming.
-SPACE_MULTIPLES = {
- "--space-3xs": 0.25,
- "--space-2xs": 0.5,
- "--space-xs": 0.75,
- "--space-s": 1.0,
- "--space-m": 1.5,
- "--space-l": 2.0,
- "--space-xl": 3.0,
- "--space-2xl": 4.0,
- "--space-3xl": 6.0,
-}
-
-
-def main():
- p = argparse.ArgumentParser(description=__doc__,
- formatter_class=argparse.RawDescriptionHelpFormatter)
- p.add_argument("pair", nargs="*", type=float,
- help="min_px max_px for a single clamp()")
- p.add_argument("--min-vw", type=float, default=320)
- p.add_argument("--max-vw", type=float, default=1240)
- p.add_argument("--body-min", type=float, default=18)
- p.add_argument("--body-max", type=float, default=20)
- p.add_argument("--ratio-min", type=float, default=1.20)
- p.add_argument("--ratio-max", type=float, default=1.25)
- args = p.parse_args()
-
- if args.pair:
- if len(args.pair) != 2:
- p.error("provide exactly two numbers: min_px max_px")
- print(clamp(args.pair[0], args.pair[1], args.min_vw, args.max_vw))
- return
-
- print(":root {")
- print(" /* fluid type scale */")
- for name, (mn, mx) in type_scale(args.body_min, args.body_max,
- args.ratio_min, args.ratio_max).items():
- print(f" {name}: {clamp(mn, mx, args.min_vw, args.max_vw)}; "
- f"/* {round_(mn, 2)} -> {round_(mx, 2)} */")
- print()
- print(" /* fluid space scale */")
- for name, mult in SPACE_MULTIPLES.items():
- mn, mx = args.body_min * mult, args.body_max * mult
- print(f" {name}: {clamp(mn, mx, args.min_vw, args.max_vw)}; "
- f"/* {round_(mn, 2)} -> {round_(mx, 2)} */")
- print("}")
-
-
-if __name__ == "__main__":
- main()
diff --git a/public/app.css b/public/app.css
index 95731cc..aa2c431 100644
--- a/public/app.css
+++ b/public/app.css
@@ -1,48 +1,71 @@
-/* Utopia fluid type scale - min 320px/1rem, max 1280px/1.25rem, ratio 1.25 */
+/* ════════════════════════════════════════════════════════════════════════
+ Domus — Calm Archive
+ Tokens mirror docs/design/domus-tokens.css. Fluid type & space generated
+ with the Utopia scale (utopia.fyi): viewport 320→1240px, body 18→20px,
+ type ratio 1.20→1.25. See docs/design/design-system.md.
+ ════════════════════════════════════════════════════════════════════════ */
:root {
- --step--2: clamp(0.64rem, calc(0.62rem + 0.11vw), 0.72rem);
- --step--1: clamp(0.80rem, calc(0.76rem + 0.20vw), 0.94rem);
- --step-0: clamp(1.00rem, calc(0.93rem + 0.33vw), 1.25rem);
- --step-1: clamp(1.25rem, calc(1.14rem + 0.54vw), 1.56rem);
- --step-2: clamp(1.56rem, calc(1.40rem + 0.82vw), 1.95rem);
- --step-3: clamp(1.95rem, calc(1.72rem + 1.18vw), 2.44rem);
-
- /* Utopia fluid space scale */
- --space-3xs: clamp(0.25rem, calc(0.23rem + 0.11vw), 0.31rem);
- --space-2xs: clamp(0.50rem, calc(0.46rem + 0.22vw), 0.63rem);
- --space-xs: clamp(0.75rem, calc(0.70rem + 0.27vw), 0.94rem);
- --space-s: clamp(1.00rem, calc(0.93rem + 0.33vw), 1.25rem);
- --space-m: clamp(1.50rem, calc(1.40rem + 0.54vw), 1.88rem);
- --space-l: clamp(2.00rem, calc(1.85rem + 0.76vw), 2.50rem);
- --space-xl: clamp(3.00rem, calc(2.78rem + 1.09vw), 3.75rem);
- --space-2xl: clamp(4.00rem, calc(3.70rem + 1.52vw), 5.00rem);
- --space-3xl: clamp(6.00rem, calc(5.56rem + 2.17vw), 7.50rem);
-
- /* Design tokens */
- --bg: oklch(0.985 0.002 255);
- --surface: #ffffff;
- --ink: oklch(0.24 0.012 262);
- --ink-2: oklch(0.50 0.010 262);
- --ink-3: oklch(0.68 0.008 262);
- --line: oklch(0.905 0.004 262);
- --line-2: oklch(0.82 0.006 262);
- --fill: oklch(0.967 0.003 262);
- --fill-2: oklch(0.945 0.004 262);
- --accent: oklch(0.50 0.17 290);
- --accent-ink: oklch(0.42 0.17 290);
- --accent-soft:oklch(0.96 0.03 290);
- --radius: 10px;
- --font-ui: system-ui, -apple-system, sans-serif;
- --font-mono: ui-monospace, "SF Mono", monospace;
+ /* ── fluid type scale ───────────────────────────────────────────────── */
+ --step--2: clamp(0.7813rem, 0.7747rem + 0.0326vw, 0.8rem); /* 12.50 → 12.80 */
+ --step--1: clamp(0.9375rem, 0.9158rem + 0.1087vw, 1rem); /* 15.00 → 16.00 */
+ --step-0: clamp(1.125rem, 1.0815rem + 0.2174vw, 1.25rem); /* 18.00 → 20.00 */
+ --step-1: clamp(1.35rem, 1.2761rem + 0.3696vw, 1.5625rem); /* 21.60 → 25.00 */
+ --step-2: clamp(1.62rem, 1.5041rem + 0.5793vw, 1.9531rem); /* 25.92 → 31.25 */
+ --step-3: clamp(1.944rem, 1.771rem + 0.8651vw, 2.4414rem); /* 31.10 → 39.06 */
+ --step-4: clamp(2.3328rem, 2.0827rem + 1.2504vw, 3.0518rem); /* 37.32 → 48.83 */
+ --step-5: clamp(2.7994rem, 2.4462rem + 1.7658vw, 3.8147rem); /* 44.79 → 61.04 */
+
+ /* ── fluid space scale ──────────────────────────────────────────────── */
+ --space-3xs: clamp(0.2813rem, 0.2704rem + 0.0543vw, 0.3125rem); /* 4.5 → 5 */
+ --space-2xs: clamp(0.5625rem, 0.5408rem + 0.1087vw, 0.625rem); /* 9 → 10 */
+ --space-xs: clamp(0.8438rem, 0.8111rem + 0.163vw, 0.9375rem); /* 13.5 → 15 */
+ --space-s: clamp(1.125rem, 1.0815rem + 0.2174vw, 1.25rem); /* 18 → 20 */
+ --space-m: clamp(1.6875rem, 1.6223rem + 0.3261vw, 1.875rem); /* 27 → 30 */
+ --space-l: clamp(2.25rem, 2.163rem + 0.4348vw, 2.5rem); /* 36 → 40 */
+ --space-xl: clamp(3.375rem, 3.2446rem + 0.6522vw, 3.75rem); /* 54 → 60 */
+ --space-2xl: clamp(4.5rem, 4.3261rem + 0.8696vw, 5rem); /* 72 → 80 */
+ --space-3xl: clamp(6.75rem, 6.4891rem + 1.3043vw, 7.5rem); /* 108 → 120 */
+
+ /* ── paper & ink (warm, low chroma) ─────────────────────────────────── */
+ --w-desk: #e9e3d6; /* page background / desk */
+ --w-bg: #f3efe6; /* warm paper */
+ --w-surface: #fdfbf6; /* card stock */
+ --w-fill: #efe9dc; /* recessed paper */
+ --w-fill-2: #e9e2d2;
+ --w-line: #e4ddcf; /* hairline */
+ --w-line-2: #d4cbb8; /* rule */
+ --w-ink-3: #9b9384; /* faint catalog-label */
+ --w-ink-2: #6b6458; /* muted */
+ --w-ink: #2a261f; /* warm near-black */
+
+ /* ── accent (Clay — swappable at the brand level) ───────────────────── */
+ --w-accent: #9a5a3c;
+ --w-accent-ink: color-mix(in oklch, var(--w-accent) 80%, black);
+ --w-accent-soft: color-mix(in oklch, var(--w-accent) 12%, white);
+
+ /* ── radius & elevation ─────────────────────────────────────────────── */
+ --radius: 14px; /* cards; controls subtract 5–6px */
+ --shadow-float:
+ 0 1px 2px rgba(60,52,36,.04),
+ 0 4px 10px -4px rgba(60,52,36,.10),
+ 0 24px 48px -28px rgba(60,52,36,.30);
+
+ /* ── families (web-safe, no custom fonts) ───────────────────────────── */
+ --font-serif: Georgia, "Times New Roman", "Iowan Old Style", serif;
+ --font-ui: "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
+ --font-mono: ui-monospace, Menlo, Consolas, "Courier New", monospace;
}
*, *::before, *::after { box-sizing: border-box; }
-html, body {
+html { font-size: 100%; }
+body {
margin: 0; padding: 0;
- background: var(--bg);
+ background: var(--w-bg);
font-family: var(--font-ui);
- color: var(--ink);
+ font-size: var(--step-0);
+ line-height: 1.55;
+ color: var(--w-ink);
-webkit-font-smoothing: antialiased;
}
@@ -65,28 +88,28 @@ html, body {
align-items: center;
padding: 0 var(--space-m);
height: 58px;
- border-bottom: 1px solid var(--line);
- background: color-mix(in oklch, var(--surface) 70%, var(--bg));
- backdrop-filter: blur(4px);
+ border-bottom: 1px solid var(--w-line);
+ background: var(--w-bg);
position: sticky;
top: 0;
z-index: 10;
}
.logo {
+ font-family: var(--font-serif);
font-size: var(--step-1);
- font-weight: 700;
- letter-spacing: -0.03em;
+ font-weight: 600;
+ letter-spacing: -0.012em;
display: flex;
align-items: center;
gap: var(--space-2xs);
- color: var(--ink);
+ color: var(--w-ink);
text-decoration: none;
}
.logo-mark {
width: 22px; height: 22px;
- border: 1.5px solid var(--ink);
+ border: 1.5px solid var(--w-ink);
border-radius: 6px;
position: relative;
flex: none;
@@ -96,8 +119,8 @@ html, body {
position: absolute;
inset: 4px 4px auto 4px;
height: 1.5px;
- background: var(--ink);
- box-shadow: 0 4px 0 var(--ink), 0 8px 0 var(--accent);
+ background: var(--w-ink);
+ box-shadow: 0 4px 0 var(--w-ink), 0 8px 0 var(--w-accent);
}
/* ---- main content ---- */
@@ -111,29 +134,30 @@ html, body {
/* ---- capture card ---- */
.card {
- background: var(--surface);
- border: 1px solid var(--line);
+ background: var(--w-surface);
+ border: 1px solid var(--w-line);
border-radius: var(--radius);
- box-shadow: 0 18px 50px -20px rgba(20,22,30,0.28), 0 2px 8px -4px rgba(20,22,30,0.12);
+ box-shadow: var(--shadow-float);
width: min(420px, 100%);
overflow: hidden;
}
.card-body {
- padding: var(--space-l);
+ padding: var(--space-m);
}
.card-title {
+ font-family: var(--font-serif);
font-size: var(--step-1);
- font-weight: 650;
- letter-spacing: -0.02em;
+ font-weight: 600;
+ letter-spacing: -0.012em;
line-height: 1.12;
margin: 0 0 var(--space-3xs) 0;
}
.card-lead {
font-size: var(--step--1);
- color: var(--ink-2);
+ color: var(--w-ink-2);
margin: 0 0 var(--space-m) 0;
line-height: 1.45;
}
@@ -150,42 +174,45 @@ html, body {
align-items: center;
justify-content: center;
gap: var(--space-2xs);
- padding: var(--space-xs) var(--space-m);
- border: 1px solid var(--line-2);
- border-radius: calc(var(--radius) - 2px);
- background: var(--surface);
+ padding: var(--space-xs) var(--space-s);
+ border: 1px solid var(--w-line-2);
+ border-radius: calc(var(--radius) - 5px);
+ background: var(--w-surface);
font-family: var(--font-ui);
- font-size: var(--step-0);
+ font-size: var(--step--1);
font-weight: 550;
letter-spacing: -0.01em;
- color: var(--ink);
+ color: var(--w-ink);
cursor: pointer;
white-space: nowrap;
width: 100%;
- transition: background .12s ease, border-color .12s ease;
+ transition: background .14s ease, border-color .14s ease;
text-align: center;
}
-.btn:hover { background: var(--fill); }
+.btn:hover { background: var(--w-fill); }
.btn-primary {
- background: var(--accent);
- border-color: var(--accent);
+ background: var(--w-accent);
+ border-color: var(--w-accent);
color: #fff;
}
-.btn-primary:hover { background: var(--accent-ink); }
+.btn-primary:hover {
+ background: var(--w-accent-ink);
+ border-color: var(--w-accent-ink);
+}
.btn-ghost {
background: transparent;
border-color: transparent;
- color: var(--ink-2);
+ color: var(--w-ink-2);
}
-.btn-ghost:hover { background: var(--fill); }
+.btn-ghost:hover { background: var(--w-fill); }
.drop-hint {
font-family: var(--font-mono);
font-size: var(--step--2);
- color: var(--ink-3);
+ color: var(--w-ink-3);
text-align: center;
margin-top: var(--space-s);
}
@@ -193,8 +220,11 @@ html, body {
/* ---- saved state ---- */
.preview-zone {
height: 210px;
- border-bottom: 1px solid var(--line);
- background: var(--fill-2);
+ border-bottom: 1px solid var(--w-line);
+ background:
+ repeating-linear-gradient(45deg, transparent 0 8px,
+ rgba(60,52,36,.045) 8px 9px),
+ var(--w-fill);
display: flex;
align-items: center;
justify-content: center;
@@ -212,9 +242,11 @@ html, body {
flex-direction: column;
align-items: center;
gap: var(--space-2xs);
- color: var(--ink-3);
+ color: var(--w-ink-3);
font-family: var(--font-mono);
font-size: var(--step--2);
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
}
.save-form {
@@ -241,9 +273,12 @@ html, body {
.asset-inputs-label {
margin: 0;
- font-size: var(--step--1);
- color: var(--ink-3);
+ font-family: var(--font-mono);
+ font-size: var(--step--2);
font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--w-ink-2);
}
.asset-input-row {
@@ -255,38 +290,38 @@ html, body {
.asset-input-row input[type="text"] {
flex: 1;
min-width: 0;
- padding: var(--space-2xs) var(--space-xs);
- border: 1px solid var(--line-2);
- border-radius: calc(var(--radius) - 2px);
+ padding: var(--space-xs) var(--space-s);
+ border: 1px solid var(--w-line-2);
+ border-radius: calc(var(--radius) - 6px);
font-family: var(--font-ui);
- font-size: var(--step-0);
- color: var(--ink);
- background: var(--surface);
+ font-size: var(--step--1);
+ color: var(--w-ink);
+ background: var(--w-surface);
outline: none;
}
.asset-input-row input[type="text"]:focus {
- border-color: var(--accent);
- box-shadow: 0 0 0 2px var(--accent-soft);
+ border-color: var(--w-accent);
+ box-shadow: 0 0 0 2px var(--w-accent-soft);
}
.btn-remove-asset {
flex: none;
padding: var(--space-3xs);
border: none;
- border-radius: calc(var(--radius) - 2px);
+ border-radius: calc(var(--radius) - 6px);
background: transparent;
- color: var(--ink-3);
+ color: var(--w-ink-3);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
- transition: background .12s ease, color .12s ease;
+ transition: background .14s ease, color .14s ease;
}
.btn-remove-asset:hover {
- background: var(--fill-2);
- color: var(--ink);
+ background: var(--w-fill-2);
+ color: var(--w-ink);
}
.asset-add-btn {
@@ -297,7 +332,7 @@ html, body {
font-family: var(--font-ui);
font-size: var(--step--1);
font-weight: 550;
- color: var(--accent-ink);
+ color: var(--w-accent-ink);
cursor: pointer;
}
@@ -308,8 +343,8 @@ html, body {
/* ---- dropzone drag feedback ---- */
.card[data-drag="over"] {
- border-color: var(--accent);
- box-shadow: 0 0 0 3px var(--accent-soft), 0 18px 50px -20px rgba(20,22,30,0.28);
+ border-color: var(--w-accent);
+ box-shadow: 0 0 0 3px var(--w-accent-soft), var(--shadow-float);
}
@media (max-width: 480px) {