Round-5 feedback flagged the muddy result of forcing one --color-caution
to be both bright yellow (signal) and accessible text (ochre). The
single-token-per-role model collapsed the visible identity. Caution
notices ended up reading as brown/khaki surfaces because color-mix(
in oklab, var(--color-caution) 8%, transparent) over cream produced
muddy results whatever the source hex.
New architecture: four-tier per-role tokens, hand-tuned -bg surfaces,
plus usage-specific notice tokens on top.
Tier model — each semantic role publishes four tokens:
signal Brand-true colour. Left bars, large icons, dots, focus rings.
Looks like the role at a glance.
ink Accessible text variant. Used ONLY when text itself must
carry the role colour. WCAG AA 4.5:1 on its surface.
border Middle value. Visible chip/notice outline; not text-safe.
bg Hand-tuned pale tint surface. NOT a color-mix derivation.
Example, light-mode caution:
--color-caution-signal #FFCD00 (bright yellow)
--color-caution-ink #7A5D00 (ochre, for small text only)
--color-caution-border #D9A900 (mid gold)
--color-caution-bg #FFF7D6 (clean pale yellow tint)
--color-{role} retained as a backward-compat alias pointing at -signal.
v0.2 callers keep working; new code reaches for the specific tier.
Usage layer — components subscribe to usage-specific tokens, not
role-tier tokens directly:
--notice-{role}-bar left bar, signal-tier
--notice-{role}-icon signal in dark, ink in light, signal in HC
--notice-{role}-border outer border colour
--notice-{role}-bg hand-tuned pale surface
LumotiaNotice reads these via Tailwind arbitrary-value classes
(bg-[var(--notice-info-bg)] etc). v0.2 fallback values defined at
:root (outside quietware blocks) using color-mix from v0.2 --color-
{role} tokens so the Notice keeps rendering sensibly when quietware
is off.
HC contract — every -bg token forces to transparent. HC notices
render text + icon + 2-px border + left bar, no body tint. Brand
atmosphere steps fully aside.
Per Jake's round-5 spec: yellow stays yellow as the visible signal,
ochre only appears as small-text ink, and brown/khaki never
represents the caution role's primary visual identity again. Same
discipline applies to red/blue/green — each role has a clean
hand-tuned -bg pale tint per theme.
Verified — visible result.
Light caution notice: bright #FFCD00 bar + signal icon + ink-darkened
small icon + pale #FFF7D6 surface + #D9A900 border. Clean,
semantically clear, no mud.
Dark caution notice: bright #FFCD00 bar + signal icon + dark warm
#29210A surface + bright yellow border. Reads as yellow at a glance.
HC modes: no body tint, strong bordered notices, semantic colours
preserved as primary identifier alongside icon + label.
npm run check: 0 errors, 0 warnings across 5707 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
470 lines
17 KiB
CSS
470 lines
17 KiB
CSS
/* ============================================================
|
|
Lumotia v0.3 — Tactile Quietware tokens.
|
|
|
|
Phase 1 of the v0.3 release. Tokens only. No component refactors,
|
|
no layout changes, no behavioural shifts. The new palette and
|
|
typography activate only when html[data-design="quietware"] is set.
|
|
|
|
When the attribute is absent the runtime app behaves exactly as it
|
|
did in v0.2.
|
|
|
|
Activation paths:
|
|
1. Manual: add data-design="quietware" to <html> via dev tools.
|
|
2. Build flag (Phase 2): VITE_LUMOTIA_QUIETWARE=1 wires the
|
|
attribute through +layout.svelte automatically.
|
|
|
|
--------------------------------------------------------------
|
|
Phase 4g — four-tier semantic role tokens (2026-05-15 round 5).
|
|
|
|
One semantic role colour cannot do every UI job. Forcing
|
|
--color-caution to be both bright yellow (signal) AND accessible
|
|
text on cream (ochre) collapses the visible identity. Instead,
|
|
each role has four tier-specific tokens:
|
|
|
|
signal the brand-true colour for the role. Used for left
|
|
bars, large icons, dots, status pills, focus rings.
|
|
Looks like the role at a glance.
|
|
|
|
ink accessible text variant. Used ONLY when text itself
|
|
must carry the role colour (small chips, badges).
|
|
WCAG AA 4.5:1 on the surface it sits on.
|
|
|
|
border middle value. Visible as a notice or chip outline
|
|
but does not need to clear AA-text contrast.
|
|
|
|
bg hand-tuned pale tint surface. NOT a generic
|
|
color-mix derivation, because that produces muddy
|
|
results in mixed-luminance themes. Each value is
|
|
picked per theme so the surface stays clean.
|
|
|
|
Usage layer on top: --notice-{role}-{bar,icon,border,bg,title,body}.
|
|
Components reference the usage tokens so future per-usage
|
|
re-tuning does not require touching every site.
|
|
|
|
The source hue tokens stay (encoding role identity from Jake's
|
|
color.adobe.com palette). They are NEVER used in the runtime UI
|
|
directly. Use the tiered tokens.
|
|
|
|
Brand accent (--color-accent, copper-clay) remains distinct from
|
|
semantic yellow. Brand is action / Lumotia identity. Yellow is
|
|
caution / review.
|
|
|
|
Typography stack (V4 pairing, locked 2026-05-15):
|
|
Work Sans -> body, UI, controls, headers
|
|
Young Serif -> brand moments only (wordmark, empty-state hook)
|
|
JetBrains Mono -> logs, paths, model IDs
|
|
|
|
Source brief: docs/release/v0.3-tactile-quietware.md.
|
|
============================================================ */
|
|
|
|
@font-face {
|
|
font-family: 'Work Sans';
|
|
src: url('/fonts/work-sans-variable.woff2') format('woff2-variations'),
|
|
url('/fonts/work-sans-variable.woff2') format('woff2');
|
|
font-weight: 100 900;
|
|
font-style: normal;
|
|
font-display: swap;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: 'Work Sans';
|
|
src: url('/fonts/work-sans-italic-variable.woff2') format('woff2-variations'),
|
|
url('/fonts/work-sans-italic-variable.woff2') format('woff2');
|
|
font-weight: 100 900;
|
|
font-style: italic;
|
|
font-display: swap;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: 'Young Serif';
|
|
src: url('/fonts/young-serif.woff2') format('woff2');
|
|
font-weight: 400;
|
|
font-style: normal;
|
|
font-display: swap;
|
|
}
|
|
|
|
/* ============================================================
|
|
Semantic source hues. Identity-only, never used directly.
|
|
Each --color-{role}-{tier} is a hand-tuned derivation.
|
|
============================================================ */
|
|
:root[data-design="quietware"] {
|
|
--semantic-red-source: #FF0700;
|
|
--semantic-yellow-source: #FFCD00;
|
|
--semantic-green-source: #00FF56;
|
|
--semantic-blue-source: #000AFF;
|
|
}
|
|
|
|
/* ============================================================
|
|
Root-level fallback notice tokens. When quietware is OFF the
|
|
v0.2 surface still gets sensible notice-* values derived from
|
|
the v0.2 --color-{role} tokens via color-mix. Quietware blocks
|
|
below override these with hand-tuned per-mode values.
|
|
============================================================ */
|
|
:root {
|
|
--notice-info-bar: var(--color-info);
|
|
--notice-info-icon: var(--color-info);
|
|
--notice-info-border: color-mix(in oklab, var(--color-info) 40%, transparent);
|
|
--notice-info-bg: color-mix(in oklab, var(--color-info) 8%, transparent);
|
|
|
|
--notice-danger-bar: var(--color-danger);
|
|
--notice-danger-icon: var(--color-danger);
|
|
--notice-danger-border: color-mix(in oklab, var(--color-danger) 40%, transparent);
|
|
--notice-danger-bg: color-mix(in oklab, var(--color-danger) 8%, transparent);
|
|
|
|
--notice-success-bar: var(--color-success);
|
|
--notice-success-icon: var(--color-success);
|
|
--notice-success-border: color-mix(in oklab, var(--color-success) 40%, transparent);
|
|
--notice-success-bg: color-mix(in oklab, var(--color-success) 8%, transparent);
|
|
|
|
--notice-caution-bar: var(--color-caution);
|
|
--notice-caution-icon: var(--color-caution);
|
|
--notice-caution-border: color-mix(in oklab, var(--color-caution) 40%, transparent);
|
|
--notice-caution-bg: color-mix(in oklab, var(--color-caution) 8%, transparent);
|
|
}
|
|
|
|
/* ============================================================
|
|
Quietware — dark mode default. Warm brown-charcoal atmosphere.
|
|
============================================================ */
|
|
:root[data-design="quietware"] {
|
|
--color-bg: #12100E;
|
|
--color-bg-elevated: #1A1713;
|
|
--color-bg-card: #211D18;
|
|
--color-bg-input: #181510;
|
|
--color-border: #4A4035;
|
|
--color-border-subtle: #2C261F;
|
|
|
|
--color-text: #F3EFE7;
|
|
--color-text-secondary: #BDB6A9;
|
|
--color-text-tertiary: #908779;
|
|
|
|
/* Brand accent. Copper-clay, distinct from semantic yellow. */
|
|
--color-accent: #C97845;
|
|
--color-accent-hover: #B96638;
|
|
--color-accent-subtle: #C9784514;
|
|
--color-accent-glow: #C9784525;
|
|
|
|
/* Four-tier role tokens. Signal stays bright (the role's identity);
|
|
ink lightens for text legibility on dark; border is a middle value;
|
|
bg is a hand-tuned dark tint, not a color-mix derivation. */
|
|
--color-info-signal: #6EA8FF;
|
|
--color-info-ink: #9CC6FF;
|
|
--color-info-border: #4D8DFF;
|
|
--color-info-bg: #101A2A;
|
|
|
|
--color-danger-signal: #FF5A52;
|
|
--color-danger-ink: #FF9B96;
|
|
--color-danger-border: #FF5A52;
|
|
--color-danger-bg: #2A1412;
|
|
|
|
--color-success-signal: #00FF56;
|
|
--color-success-ink: #7DFFA8;
|
|
--color-success-border: #00D94A;
|
|
--color-success-bg: #0D2415;
|
|
|
|
--color-caution-signal: #FFCD00;
|
|
--color-caution-ink: #FFE066;
|
|
--color-caution-border: #FFCD00;
|
|
--color-caution-bg: #29210A;
|
|
|
|
/* Backward-compat aliases. v0.2 callers of --color-danger etc.
|
|
get the signal variant by default. New code should reach for the
|
|
specific tier. */
|
|
--color-info: var(--color-info-signal);
|
|
--color-danger: var(--color-danger-signal);
|
|
--color-success: var(--color-success-signal);
|
|
--color-caution: var(--color-caution-signal);
|
|
--color-warning: var(--color-caution-signal);
|
|
|
|
/* Notice usage tokens. Components subscribe to these so future
|
|
per-usage retuning does not require touching every site. In dark
|
|
mode the notice icon uses signal (bright on dark surface);
|
|
title/body always neutral text. */
|
|
--notice-info-bar: var(--color-info-signal);
|
|
--notice-info-icon: var(--color-info-signal);
|
|
--notice-info-border: var(--color-info-border);
|
|
--notice-info-bg: var(--color-info-bg);
|
|
|
|
--notice-danger-bar: var(--color-danger-signal);
|
|
--notice-danger-icon: var(--color-danger-signal);
|
|
--notice-danger-border: var(--color-danger-border);
|
|
--notice-danger-bg: var(--color-danger-bg);
|
|
|
|
--notice-success-bar: var(--color-success-signal);
|
|
--notice-success-icon: var(--color-success-signal);
|
|
--notice-success-border: var(--color-success-border);
|
|
--notice-success-bg: var(--color-success-bg);
|
|
|
|
--notice-caution-bar: var(--color-caution-signal);
|
|
--notice-caution-icon: var(--color-caution-signal);
|
|
--notice-caution-border: var(--color-caution-border);
|
|
--notice-caution-bg: var(--color-caution-bg);
|
|
|
|
--color-sidebar: #0F0E0C;
|
|
--color-nav-active: #211D18;
|
|
--color-hover: #1A1713;
|
|
--color-overlay-dim: rgba(15, 14, 12, 0.7);
|
|
|
|
--font-family-body: 'Work Sans', system-ui, sans-serif;
|
|
--font-family-display: 'Young Serif', serif;
|
|
--font-family-mono: 'JetBrains Mono', monospace;
|
|
|
|
/* Phase 4e behavioural tokens for the HC rendering contract. */
|
|
--grain-opacity: 0.06;
|
|
--shadow-strength: 1;
|
|
--border-width-control: 1px;
|
|
--focus-ring-width: 2px;
|
|
--panel-radius: 8px;
|
|
|
|
--quietware-texture-opacity: var(--grain-opacity);
|
|
--quietware-texture-color: rgba(243, 239, 231, 1);
|
|
}
|
|
|
|
/* ============================================================
|
|
Quietware — light mode. Warm paper.
|
|
Caution signal stays bright #FFCD00; ink #7A5D00 only appears
|
|
when caution text itself must be coloured.
|
|
============================================================ */
|
|
:root[data-design="quietware"][data-theme="light"] {
|
|
--color-bg: #FBF8F2;
|
|
--color-bg-elevated: #F6F1EA;
|
|
--color-bg-card: #FFFDF8;
|
|
--color-bg-input: #F4EFE7;
|
|
--color-border: #897D70;
|
|
--color-border-subtle: #D8D0C4;
|
|
|
|
--color-text: #1D1B18;
|
|
--color-text-secondary: #5E574C;
|
|
--color-text-tertiary: #74685B;
|
|
|
|
--color-accent: #9D5F32;
|
|
--color-accent-hover: #854E29;
|
|
--color-accent-subtle: #9D5F3214;
|
|
--color-accent-glow: #9D5F3225;
|
|
|
|
/* Four-tier role tokens, light mode. Bg values hand-picked as
|
|
clean pale tints, not muddy color-mix products. */
|
|
--color-info-signal: #000AFF;
|
|
--color-info-ink: #0033A0;
|
|
--color-info-border: #3A6BFF;
|
|
--color-info-bg: #EEF3FF;
|
|
|
|
--color-danger-signal: #FF0700;
|
|
--color-danger-ink: #B3261E;
|
|
--color-danger-border: #E13A32;
|
|
--color-danger-bg: #FFF0EF;
|
|
|
|
--color-success-signal: #00D94A;
|
|
--color-success-ink: #006B2D;
|
|
--color-success-border: #159947;
|
|
--color-success-bg: #EAF8EE;
|
|
|
|
--color-caution-signal: #FFCD00;
|
|
--color-caution-ink: #7A5D00;
|
|
--color-caution-border: #D9A900;
|
|
--color-caution-bg: #FFF7D6;
|
|
|
|
--color-info: var(--color-info-signal);
|
|
--color-danger: var(--color-danger-signal);
|
|
--color-success: var(--color-success-signal);
|
|
--color-caution: var(--color-caution-signal);
|
|
--color-warning: var(--color-caution-signal);
|
|
|
|
/* In light mode the notice icon uses ink (the dark variant) so it
|
|
reads clearly against the pale -bg tint. Signal would wash out. */
|
|
--notice-info-bar: var(--color-info-signal);
|
|
--notice-info-icon: var(--color-info-ink);
|
|
--notice-info-border: var(--color-info-border);
|
|
--notice-info-bg: var(--color-info-bg);
|
|
|
|
--notice-danger-bar: var(--color-danger-signal);
|
|
--notice-danger-icon: var(--color-danger-ink);
|
|
--notice-danger-border: var(--color-danger-border);
|
|
--notice-danger-bg: var(--color-danger-bg);
|
|
|
|
--notice-success-bar: var(--color-success-signal);
|
|
--notice-success-icon: var(--color-success-ink);
|
|
--notice-success-border: var(--color-success-border);
|
|
--notice-success-bg: var(--color-success-bg);
|
|
|
|
--notice-caution-bar: var(--color-caution-signal);
|
|
--notice-caution-icon: var(--color-caution-ink);
|
|
--notice-caution-border: var(--color-caution-border);
|
|
--notice-caution-bg: var(--color-caution-bg);
|
|
|
|
--color-sidebar: #F3EEE6;
|
|
--color-nav-active: #E8E2D6;
|
|
--color-hover: #EAE4D8;
|
|
--color-overlay-dim: rgba(29, 27, 24, 0.55);
|
|
|
|
--grain-opacity: 0.07;
|
|
--quietware-texture-opacity: var(--grain-opacity);
|
|
--quietware-texture-color: rgba(29, 27, 24, 1);
|
|
}
|
|
|
|
/* ============================================================
|
|
Quietware — high-contrast. Simple and brutal.
|
|
All notice -bg goes transparent; -bar/-icon/-border collapse
|
|
to a single source colour per role. Semantic recognition by
|
|
colour identity + label + icon shape, not by ambient tinting.
|
|
============================================================ */
|
|
:root[data-design="quietware"][data-contrast="high"],
|
|
:root[data-design="quietware"][data-theme="light"][data-contrast="high"] {
|
|
--color-bg: #FFFFFF;
|
|
--color-bg-elevated: #FFFFFF;
|
|
--color-bg-card: #FFFFFF;
|
|
--color-bg-input: #FFFFFF;
|
|
--color-border: #000000;
|
|
--color-border-subtle: #000000;
|
|
|
|
--color-text: #000000;
|
|
--color-text-secondary: #000000;
|
|
--color-text-tertiary: #000000;
|
|
|
|
--color-accent: #005FCC;
|
|
--color-accent-hover: #004BA0;
|
|
--color-accent-subtle: transparent;
|
|
--color-accent-glow: transparent;
|
|
|
|
--color-info-signal: #000AFF;
|
|
--color-info-ink: #000AFF;
|
|
--color-info-border: #000AFF;
|
|
--color-info-bg: transparent;
|
|
|
|
--color-danger-signal: #FF0700;
|
|
--color-danger-ink: #FF0700;
|
|
--color-danger-border: #FF0700;
|
|
--color-danger-bg: transparent;
|
|
|
|
--color-success-signal: #00B83E;
|
|
--color-success-ink: #007A2B;
|
|
--color-success-border: #00B83E;
|
|
--color-success-bg: transparent;
|
|
|
|
--color-caution-signal: #FFCD00;
|
|
--color-caution-ink: #7A5D00;
|
|
--color-caution-border: #FFCD00;
|
|
--color-caution-bg: transparent;
|
|
|
|
--color-info: var(--color-info-signal);
|
|
--color-danger: var(--color-danger-signal);
|
|
--color-success: var(--color-success-signal);
|
|
--color-caution: var(--color-caution-signal);
|
|
--color-warning: var(--color-caution-signal);
|
|
|
|
--notice-info-bar: var(--color-info-signal);
|
|
--notice-info-icon: var(--color-info-signal);
|
|
--notice-info-border: var(--color-info-border);
|
|
--notice-info-bg: transparent;
|
|
|
|
--notice-danger-bar: var(--color-danger-signal);
|
|
--notice-danger-icon: var(--color-danger-signal);
|
|
--notice-danger-border: var(--color-danger-border);
|
|
--notice-danger-bg: transparent;
|
|
|
|
--notice-success-bar: var(--color-success-signal);
|
|
--notice-success-icon: var(--color-success-ink);
|
|
--notice-success-border: var(--color-success-border);
|
|
--notice-success-bg: transparent;
|
|
|
|
--notice-caution-bar: var(--color-caution-signal);
|
|
--notice-caution-icon: var(--color-caution-ink);
|
|
--notice-caution-border: var(--color-caution-border);
|
|
--notice-caution-bg: transparent;
|
|
|
|
--color-sidebar: #FFFFFF;
|
|
--color-nav-active: #000000;
|
|
--color-hover: #F0F0F0;
|
|
--color-overlay-dim: rgba(0, 0, 0, 0.85);
|
|
|
|
--grain-opacity: 0;
|
|
--shadow-strength: 0;
|
|
--border-width-control: 2px;
|
|
--focus-ring-width: 3px;
|
|
--panel-radius: 8px;
|
|
--quietware-texture-opacity: 0;
|
|
--quietware-texture-color: rgba(0, 0, 0, 0);
|
|
}
|
|
|
|
/* Dark-base high-contrast — black background, white borders. Used
|
|
when the user explicitly wants HC inside the dark theme. */
|
|
:root[data-design="quietware"][data-contrast="high"]:not([data-theme="light"]) {
|
|
--color-bg: #000000;
|
|
--color-bg-elevated: #000000;
|
|
--color-bg-card: #000000;
|
|
--color-bg-input: #000000;
|
|
--color-border: #FFFFFF;
|
|
--color-border-subtle: #FFFFFF;
|
|
|
|
--color-text: #FFFFFF;
|
|
--color-text-secondary: #FFFFFF;
|
|
--color-text-tertiary: #FFFFFF;
|
|
|
|
--color-accent: #75A8F0;
|
|
--color-accent-hover: #5C92E0;
|
|
|
|
/* HC dark inverts the contrast direction: signals are the lighter
|
|
end of each hue, not the saturated source. -bg stays transparent
|
|
so the black surface shows through directly. */
|
|
--color-info-signal: #8AB4F8;
|
|
--color-info-ink: #8AB4F8;
|
|
--color-info-border: #8AB4F8;
|
|
--color-info-bg: transparent;
|
|
|
|
--color-danger-signal: #FF8A8A;
|
|
--color-danger-ink: #FF8A8A;
|
|
--color-danger-border: #FF8A8A;
|
|
--color-danger-bg: transparent;
|
|
|
|
--color-success-signal: #79D59B;
|
|
--color-success-ink: #79D59B;
|
|
--color-success-border: #79D59B;
|
|
--color-success-bg: transparent;
|
|
|
|
--color-caution-signal: #F2C94C;
|
|
--color-caution-ink: #FFE066;
|
|
--color-caution-border: #F2C94C;
|
|
--color-caution-bg: transparent;
|
|
|
|
--color-info: var(--color-info-signal);
|
|
--color-danger: var(--color-danger-signal);
|
|
--color-success: var(--color-success-signal);
|
|
--color-caution: var(--color-caution-signal);
|
|
--color-warning: var(--color-caution-signal);
|
|
|
|
--notice-info-icon: var(--color-info-signal);
|
|
--notice-danger-icon: var(--color-danger-signal);
|
|
--notice-success-icon: var(--color-success-signal);
|
|
--notice-caution-icon: var(--color-caution-signal);
|
|
|
|
--color-sidebar: #000000;
|
|
--color-nav-active: #FFFFFF;
|
|
--color-hover: #1A1A1A;
|
|
}
|
|
|
|
/* ============================================================
|
|
Static grain overlay. Reads --grain-opacity. Zero in HC mode by
|
|
contract. Fixed-position so it does not scroll.
|
|
============================================================ */
|
|
:root[data-design="quietware"] body::after {
|
|
content: "";
|
|
position: fixed;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
z-index: 9999;
|
|
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
|
|
opacity: var(--grain-opacity);
|
|
mix-blend-mode: overlay;
|
|
}
|
|
|
|
/* ============================================================
|
|
Reduced motion. Honour OS preference; quietware rules require it.
|
|
============================================================ */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
:root[data-design="quietware"] * {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
scroll-behavior: auto !important;
|
|
}
|
|
}
|