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 <noreply@anthropic.com>
This commit is contained in:
jules
2026-07-07 22:35:56 +10:00
commit d1e3727587
2 changed files with 448 additions and 0 deletions

404
theme.css Normal file
View File

@@ -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;
}