From d1e37275871fe28dbdddef42b98a09c2f7d23c8a Mon Sep 17 00:00:00 2001 From: jules Date: Tue, 7 Jul 2026 22:35:56 +1000 Subject: [PATCH] lib-theme-harbor: initial import (warm-consumer set, 2026-07) Includes the 2026-07-07 UX sweep: neutral inline-code chips (accent reserved for actions/links/focus), dark-mode card/popover lightness lift for visible surface separation, 44px touch-target floor on coarse pointers, card-title pinned to a named ramp role. Co-Authored-By: Claude Fable 5 --- README.md | 44 ++++++ theme.css | 404 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 448 insertions(+) create mode 100644 README.md create mode 100644 theme.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..f5f235c --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# lib-theme-harbor + +Cool neutral, warm spark. The most neutral of the warm-consumer set: the entire surface is a +calm, faintly-**cool** light grey (morning harbor light on soft slate — a whisper of blue-grey +that reads as clean neutral, never cream, never enterprise blue), and all the warmth is carried +by a single **honey/amber** accent spent surgically on the primary action, selection, and focus. +Grey stone warmed by lamplight. Light + dark. Built for me.sky-ai.com back-of-house. + +- **Surface:** faintly-cool light grey `--background` (`oklch(0.975 0.005 248)`); clean white + cards, hairline-separated. Ink is a deep, very-slightly-cool charcoal. +- **Accent:** one honey/amber primary (`oklch(0.8 0.14 78)`) with **dark ink on top** (the + bright-swatch / dark-ink pairing) — the sole warmth, ≤10% of the surface. +- **Status:** `warning` is pulled toward amber-**orange** (hue 52) and set darker than the honey + primary (hue 78) so the two never read as the same swatch. +- **Dark:** harbor at night — deep cool-neutral charcoal (`oklch(0.2 0.008 252)`, not pure black, + never warm-brown), amber glowing warm against it. +- **Shape:** crisp `--radius: 0.75rem`; low, neutral cool shadows; product-fast motion (base + 280ms), springs aliased to the standard eases. +- **Type:** crisp modern-neutral — Inter Tight (text) + Geist (headings) + Geist Mono (code). + +## Fonts + +Themes must not `@import` fonts (breaks PostCSS ordering — see the Crema token contract). The +**consuming app** loads them at the top of its entry CSS via this Google Fonts URL: + +``` +https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&family=Inter+Tight:wght@400;500;600;700&display=swap +``` + +## Usage + +Consumed by the **Studio** lab as a sibling. The consuming app imports `theme.css` (or, in +Studio, `scripts/build-themes.mjs` copies it to `public/themes/harbor.css`). Dark mode is +class-based (`.dark`); the app owns the toggle and should respect `prefers-color-scheme` on first +load. + +## Validation + +```bash +node ../create-crema-app/scripts/validate-theme.mjs theme.css +``` + +Passes the Crema v1 token contract: every Tier-1 token present in both modes, all contrast floors +met, no font `@import`, no root `font-size`. diff --git a/theme.css b/theme.css new file mode 100644 index 0000000..a2cab1c --- /dev/null +++ b/theme.css @@ -0,0 +1,404 @@ +/* + * Harbor — cool neutral, warm spark. + * + * The most neutral of the warm-consumer set. The whole surface is a calm, + * faintly-COOL light grey — morning harbor light on soft slate, a whisper of + * blue-grey that reads as clean neutral, never cream, never SaaS blue. All the + * warmth in the theme is delivered by ONE honey/amber accent (hue ~78), spent + * surgically on the primary action, selection, and focus ring — grey stone + * warmed by lamplight. Ink is a deep, very-slightly-cool charcoal; the amber + * carries dark ink on top (the bright-swatch/dark-ink pairing) so it lands high + * above the contrast floor while staying quiet everywhere else. + * + * Dark mode is the harbor at night: a deep cool-neutral charcoal (low chroma, + * faint cool hue ~250, not pure black and never warm-brown) with the same amber + * glowing warm against it. Crisp, quiet, trustworthy — a calm utility that is + * warm only at the touchpoints. Built for me.sky-ai.com back-of-house. + * + * Type is crisp modern-neutral: Inter Tight for text, Geist for headings, Geist + * Mono for code. Product register — fixed rem sizes, no serif. Radius is a + * crisp 0.75rem; shadows are low and neutral; motion is product-fast. + * + * Token contract matches lib-theme-caffe-florian/theme.css (Crema v1). + */ + +/* + * Fonts: consuming apps load Inter Tight + Geist + Geist Mono at the top of + * their entry CSS (where @import statements are allowed), via the Google Fonts + * URL documented in this theme's README. Embedding the @import here — or + * smuggling the URL in a comment for a build script to lift — breaks PostCSS's + * ordering rules, so neither is done in this file. + */ + +:root { + --font-sans: "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-heading: "Geist", "Inter Tight", -apple-system, BlinkMacSystemFont, sans-serif; + --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace; + + /* Tailwind utility families. Distinct names so a consuming app's + * `@theme inline` can indirect to these (--font-heading/--font-sans collide + * with Tailwind's own font-* namespace). Mirror --font-heading/--font-sans. */ + --heading-family: "Geist", "Inter Tight", -apple-system, BlinkMacSystemFont, sans-serif; + --body-family: "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + + /* ── Colors ── soft faintly-cool light-grey surface (harbor light), clean + * white cards, deep cool-neutral charcoal ink, one honey/amber accent. */ + --background: oklch(0.975 0.005 248); + --foreground: oklch(0.22 0.008 255); + + --card: oklch(1 0 0); + --card-foreground: oklch(0.22 0.008 255); + + --popover: oklch(0.995 0.003 248); + --popover-foreground: oklch(0.22 0.008 255); + + /* Primary — the sole warmth. Honey/amber deepened a touch toward orange + * (74°, L0.77) so it's rich rather than blinding, with a near-black warm + * ink (L0.17, low chroma) on top — crisp, not amber-brown-on-amber. ~9:1. */ + --primary: oklch(0.77 0.145 74); + --primary-foreground: oklch(0.17 0.02 74); + + --secondary: oklch(0.94 0.006 250); + --secondary-foreground: oklch(0.3 0.008 255); + + --muted: oklch(0.94 0.006 250); + --muted-foreground: oklch(0.46 0.011 253); + + /* Accent — the warm spark as a hover/selection tint: a soft amber wash with + * deep amber ink, not a second competing fill. */ + --accent: oklch(0.93 0.03 80); + --accent-foreground: oklch(0.38 0.09 72); + + /* ── Status ── dark ink on the bright status hues (harbor's bright-swatch + * pattern), except destructive/info which carry light labels on deeper + * fills for alarm legibility. Warning is pulled toward amber-ORANGE + * (hue 52) and set darker than the honey primary (hue 78) so the two never + * read as the same swatch. */ + --destructive: oklch(0.55 0.21 27); + --destructive-foreground: oklch(0.98 0.01 27); + --success: oklch(0.6 0.14 150); + --success-foreground: oklch(0.17 0.03 150); + --warning: oklch(0.68 0.17 52); + --warning-foreground: oklch(0.23 0.05 52); + --info: oklch(0.54 0.15 248); + --info-foreground: oklch(0.98 0.01 248); + + --border: oklch(0.22 0.008 255 / 0.1); + --input: oklch(0.22 0.008 255 / 0.12); + --ring: oklch(0.77 0.145 74 / 0.55); + + /* Chart palette — amber lead, then cool blue / teal / violet / rose. + * Warm spark against a cool spectrum; hues spaced, adjacent lightness + * varied. */ + --chart-1: oklch(0.76 0.15 78); + --chart-2: oklch(0.58 0.14 248); + --chart-3: oklch(0.68 0.1 190); + --chart-4: oklch(0.55 0.15 300); + --chart-5: oklch(0.68 0.15 25); + + --sidebar: oklch(0.955 0.006 250); + --sidebar-foreground: oklch(0.22 0.008 255); + --sidebar-primary: oklch(0.8 0.14 78); + --sidebar-primary-foreground: oklch(0.26 0.05 78); + --sidebar-accent: oklch(0.93 0.03 80); + --sidebar-accent-foreground: oklch(0.38 0.09 72); + --sidebar-border: oklch(0.22 0.008 255 / 0.08); + --sidebar-ring: oklch(0.8 0.14 78 / 0.55); + + /* Syntax — cool base with a warm keyword, matching the harbor palette. */ + --syntax-keyword: oklch(0.55 0.15 300); + --syntax-string: oklch(0.5 0.12 150); + --syntax-number: oklch(0.55 0.14 52); + --syntax-comment: oklch(0.55 0.01 253); + --syntax-function: oklch(0.5 0.15 248); + --syntax-type: oklch(0.52 0.1 190); + + /* ── Chat surfaces ── + * Contract for lib-agent-chat-ui / lib-agent-dock-ui. User bubble is a deep + * cool-charcoal panel with light text (neutral, so amber stays scarce); + * assistant bubble is a clean card with a hairline border. */ + --chat-user-bg: oklch(0.28 0.012 253); + --chat-user-fg: oklch(0.97 0.005 250); + --chat-assistant-bg: oklch(1 0 0); + --chat-assistant-fg: oklch(0.22 0.008 255); + --chat-assistant-border: oklch(0.22 0.008 255 / 0.1); + + /* Crisp-modern radius. */ + --radius: 0.75rem; + + /* ── Motion ── crisp, product-fast. Springs alias to the standard eases so + * the theme stays quiet — personality lives in the speed, not in bounce. */ + --duration-fast: 160ms; + --duration-base: 280ms; + --duration-slow: 400ms; + --duration-slower: 560ms; + --duration-spring: 280ms; + + --ease-standard: cubic-bezier(0.2, 0, 0, 1); + --ease-decelerate: cubic-bezier(0, 0, 0, 1); + --ease-accelerate: cubic-bezier(0.3, 0, 1, 1); + --ease-emphasized: cubic-bezier(0.2, 0, 0, 1); + + --ease-spring-gentle: var(--ease-standard); + --ease-spring-snappy: var(--ease-decelerate); + --ease-spring-bouncy: var(--ease-standard); + + /* ── Typography ── crisp product register, fixed rem, full role ramp. */ + --text-hero-size: 3.5rem; + --text-hero-lh: 1.05; + --text-display-size: 3rem; + --text-display-lh: 1.1; + --text-headline-size: 2rem; + --text-headline-lh: 1.2; + --text-title-size: 1.5rem; + --text-title-lh: 1.3; + --text-subtitle-size: 1.25rem; + --text-subtitle-lh: 1.5; + --text-body-size: 1.0625rem; + --text-body-lh: 1.6; + --text-label-size: 0.9375rem; + --text-label-lh: 1.25rem; + --text-caption-size: 0.8125rem; + --text-caption-lh: 1.125rem; + + /* ── Elevation ── crisp, low, neutral cool shadows. */ + --elevation-0: none; + --elevation-1: 0 1px 2px oklch(0.22 0.008 255 / 0.06); + --elevation-2: 0 1px 3px oklch(0.22 0.008 255 / 0.08), 0 1px 2px oklch(0.22 0.008 255 / 0.05); + --elevation-3: 0 4px 8px -2px oklch(0.22 0.008 255 / 0.08), 0 2px 4px -2px oklch(0.22 0.008 255 / 0.05); + --elevation-4: 0 12px 24px -6px oklch(0.22 0.008 255 / 0.1), 0 4px 8px -4px oklch(0.22 0.008 255 / 0.05); + --elevation-5: 0 20px 40px -8px oklch(0.22 0.008 255 / 0.16); + + --spacing: 0.25rem; + + --border-width-thin: 0.5px; + --border-width-base: 1px; + --border-width-thick: 2px; + --border-width-heavy: 4px; + + /* No glass — inert values so consuming apps referencing these get no-ops. */ + --glass-blur: 0px; + --glass-saturate: 100%; + --glass-brightness: 1; + --glass-contrast: 1; +} + +/* + * Dark mode — harbor at night. Deep cool-neutral charcoal (faint cool hue ~250, + * low chroma, not pure black, never warm-brown); the same honey/amber glows + * warm against it with dark ink on top. Crisp and calm. + */ +.dark { + --background: oklch(0.2 0.008 252); + --foreground: oklch(0.94 0.006 250); + + --card: oklch(0.26 0.008 252); + --card-foreground: oklch(0.94 0.006 250); + + --popover: oklch(0.28 0.008 252); + --popover-foreground: oklch(0.94 0.006 250); + + --primary: oklch(0.80 0.145 74); + --primary-foreground: oklch(0.17 0.02 74); + + --secondary: oklch(0.28 0.008 252); + --secondary-foreground: oklch(0.9 0.006 250); + + --muted: oklch(0.28 0.008 252); + --muted-foreground: oklch(0.72 0.011 250); + + --accent: oklch(0.33 0.05 78); + --accent-foreground: oklch(0.86 0.1 78); + + --destructive: oklch(0.56 0.21 27); + --destructive-foreground: oklch(0.98 0.01 27); + --success: oklch(0.7 0.15 150); + --success-foreground: oklch(0.17 0.03 150); + --warning: oklch(0.74 0.16 52); + --warning-foreground: oklch(0.23 0.05 52); + --info: oklch(0.66 0.15 248); + --info-foreground: oklch(0.16 0.03 248); + + --border: oklch(0.95 0.006 250 / 0.12); + --input: oklch(0.95 0.006 250 / 0.14); + --ring: oklch(0.80 0.145 74 / 0.55); + + --chart-1: oklch(0.8 0.15 78); + --chart-2: oklch(0.68 0.14 248); + --chart-3: oklch(0.74 0.1 190); + --chart-4: oklch(0.66 0.15 300); + --chart-5: oklch(0.72 0.15 25); + + --sidebar: oklch(0.22 0.008 252); + --sidebar-foreground: oklch(0.94 0.006 250); + --sidebar-primary: oklch(0.81 0.14 78); + --sidebar-primary-foreground: oklch(0.24 0.05 78); + --sidebar-accent: oklch(0.33 0.05 78); + --sidebar-accent-foreground: oklch(0.86 0.1 78); + --sidebar-border: oklch(0.95 0.006 250 / 0.09); + --sidebar-ring: oklch(0.81 0.14 78 / 0.55); + + --syntax-keyword: oklch(0.72 0.13 300); + --syntax-string: oklch(0.74 0.12 150); + --syntax-number: oklch(0.78 0.14 52); + --syntax-comment: oklch(0.6 0.01 250); + --syntax-function: oklch(0.72 0.14 248); + --syntax-type: oklch(0.72 0.1 190); + + /* ── Chat surfaces (dark) ── */ + --chat-user-bg: oklch(0.34 0.012 252); + --chat-user-fg: oklch(0.96 0.006 250); + --chat-assistant-bg: oklch(0.26 0.008 252); + --chat-assistant-fg: oklch(0.94 0.006 250); + --chat-assistant-border: oklch(0.95 0.006 250 / 0.12); + + --elevation-1: 0 1px 2px oklch(0 0 0 / 0.3); + --elevation-2: 0 1px 3px oklch(0 0 0 / 0.4), 0 1px 2px oklch(0 0 0 / 0.25); + --elevation-3: 0 4px 8px -2px oklch(0 0 0 / 0.4), 0 2px 4px -2px oklch(0 0 0 / 0.25); + --elevation-4: 0 12px 24px -6px oklch(0 0 0 / 0.45), 0 4px 8px -4px oklch(0 0 0 / 0.25); + --elevation-5: 0 20px 40px -8px oklch(0 0 0 / 0.55); +} + +/* + * @layer base — Harbor's crisp, quiet product philosophy. + * + * No glass, no editorial flourish. Clean grotesque type, calm neutral surfaces + * hairline-separated, and the amber spark reserved for focus, selection, and the + * primary action. Everything else stays cool and out of the way. + */ +@layer base { + body { + min-height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + letter-spacing: -0.006em; + font-family: var(--font-sans); + font-size: var(--text-body-size); + line-height: var(--text-body-lh); + } + + /* Headings — Geist, crisp and tight. */ + h1, h2, h3, h4, + [data-slot="card-title"], + [data-slot="dialog-title"], + [data-slot="sheet-title"], + [data-slot="alert-dialog-title"] { + font-family: var(--font-heading); + font-weight: 600; + letter-spacing: -0.02em; + color: var(--foreground); + } + + code, kbd, samp, pre, + [data-slot="code"], + [data-slot="code-block"] { + font-family: var(--font-mono); + } + + /* Inline code — cool-neutral chip. */ + :not(pre) > code { + padding: 0.1em 0.35em; + border-radius: calc(var(--radius) * 0.5); + background: color-mix(in oklch, var(--foreground) 6%, transparent); + color: var(--foreground); + font-size: 0.9em; + } + + /* Selection — the amber spark. */ + ::selection { + background: color-mix(in oklch, var(--primary) 30%, transparent); + color: var(--foreground); + } + + /* Links — amber, underline-on-hover. */ + a { + color: color-mix(in oklch, var(--primary) 70%, var(--foreground)); + text-decoration-color: color-mix(in oklch, var(--primary) 40%, transparent); + text-underline-offset: 0.2em; + transition: text-decoration-color var(--duration-fast) var(--ease-standard); + } + a:hover { + text-decoration-color: var(--primary); + } + + [data-slot="sidebar"] { + background: var(--sidebar); + border-right: var(--border-width-base) solid var(--sidebar-border); + } +} + +/* + * Focus-visible — the amber ring, offset (not inset), for keyboard users. + * Placed outside @layer base so it beats Tailwind's default ring utilities. + */ +[data-slot="button"]:focus-visible, +[data-slot="input"]:focus-visible, +[data-slot="textarea"]:focus-visible, +[data-slot="select-trigger"]:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; + box-shadow: none; +} + +/* Buttons — quiet weight; opacity absorb on press, product-fast. */ +[data-slot="button"] { + font-weight: 500; + letter-spacing: -0.006em; +} + +/* Card titles — pinned to the subtitle role so the scale is the theme's + * decision, not the card primitive's default. */ +[data-slot="card-title"] { + font-size: var(--text-subtitle-size); + line-height: var(--text-subtitle-lh); +} + +/* Touch targets — desktop keeps the crisp density ladder; coarse pointers + * get the 44px floor on every control. */ +@media (pointer: coarse) { + [data-slot="button"] { + min-height: 2.75rem; + min-width: 2.75rem; + } + [data-slot="input"], + [data-slot="select-trigger"] { + min-height: 2.75rem; + } +} +[data-slot="button"]:hover:not(:disabled) { + opacity: 0.9; +} +[data-slot="button"]:active:not(:disabled) { + opacity: 0.82; + transform: scale(0.99); +} + +/* Inputs — clean card surface, hairline border, amber focus as a flush ring. */ +[data-slot="input"], +[data-slot="textarea"], +[data-slot="select-trigger"] { + background: var(--card); + border: var(--border-width-base) solid var(--border); + border-radius: var(--radius); + color: var(--foreground); + transition: + border-color var(--duration-fast) var(--ease-standard), + box-shadow var(--duration-fast) var(--ease-standard); +} +[data-slot="input"]::placeholder, +[data-slot="textarea"]::placeholder { + color: var(--muted-foreground); + opacity: 0.8; +} +[data-slot="input"]:hover:not(:disabled), +[data-slot="textarea"]:hover:not(:disabled), +[data-slot="select-trigger"]:hover:not(:disabled) { + border-color: color-mix(in oklch, var(--foreground) 22%, transparent); +} +[data-slot="input"]:focus, +[data-slot="textarea"]:focus, +[data-slot="select-trigger"]:focus { + border-color: var(--primary); + box-shadow: 0 0 0 1px var(--primary); + outline: none; +}