Files
lib-theme-lucent/theme.css
jules 4c761b91a4 lib-theme-lucent: initial import (access-first set, 2026-07)
One of three candidates for the access-first / maximum-legibility theme
family (WCAG AAA as identity): 7:1+ contrast on every pair (passes
validate-theme.mjs --aaa), 1.125rem body floor with nothing under 14px,
44px controls for everyone, 3px focus rings, links underlined at rest,
reduced-motion built in. Built for IGH/NDIS-facing surfaces first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 07:20:58 +10:00

474 lines
18 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Lucent — cool daylight clarity. Access-first: WCAG AAA as identity.
*
* One of three candidates in the access-first family (NDIS participants,
* their families, older support coordinators — IGH portals). The thesis:
* maximum legibility is not compliance overhead, it IS the personality.
* Dignity, clarity, zero squinting. GOV.UK / NHS design heritage,
* modernized: a near-white cool field, pure white cards, TRUE near-black
* ink, and one deep accessible blue doing all the pointing. Links are the
* same blue and underlined BY DEFAULT — an affordance, not a decoration.
* Boundaries are drawn with strong ink-alpha borders (0.250.3), not
* shadows: low-vision users need edges, not atmosphere. Radius is a modest
* 0.5rem; elevation is low; motion is minimal and quick with all springs
* aliased to standard eases.
*
* Contrast is verified, not hoped for (Y ≈ L³; C = (Y1+.05)/(Y2+.05)):
* LIGHT body ink 0.15 on bg 0.99 → ~19.1:1 (on white card ~19.7:1)
* muted-fg 0.35 on white → ~11.3:1 (muted is still reading text)
* white label on primary 0.42 → ~8.5:1 (blue's real WCAG Y sits
* below L³, so the true ratio is higher still)
* DARK body ink 0.96 on bg 0.16 → ~17.3:1 (on card 0.22 ~15.4:1)
* muted-fg 0.78 on card 0.22 → ~8.7:1 (on bg ~9.7:1)
* ink 0.15 on primary 0.74 → ~8.5:1
* Every status pair targets ≥7:1 and never dips below 4.5:1.
*
* Type is Atkinson Hyperlegible (designed by the Braille Institute for
* low-vision readers) for BOTH body and headings — hierarchy comes from
* weight 700 and the ramp, not a second family. JetBrains Mono for code.
* The ramp starts larger than the house norm (body 1.125rem/1.65) and the
* caption never drops below 14px. Touch targets are 44px for EVERYONE,
* not just coarse pointers.
*
* Token contract matches lib-theme-caffe-florian/theme.css (Crema v1).
*/
/*
* Fonts: consuming apps load Atkinson Hyperlegible + JetBrains 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: "Atkinson Hyperlegible", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-heading: "Atkinson Hyperlegible", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "JetBrains 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). */
--heading-family: "Atkinson Hyperlegible", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--body-family: "Atkinson Hyperlegible", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
/* ── Colors ── near-white cool daylight field, pure white cards, true
* near-black cool ink. Body ink on bg ≈ 19.1:1; on card ≈ 19.7:1. */
--background: oklch(0.99 0.002 240);
--foreground: oklch(0.15 0.005 250);
--card: oklch(1 0 0);
--card-foreground: oklch(0.15 0.005 250);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.15 0.005 250);
/* Primary — one deep accessible blue (GOV.UK lineage) with a pure
* white label. Approx (1+.05)/(0.42³+.05) ≈ 8.5:1; saturated blue's
* true WCAG luminance is lower than L³, so the real ratio is higher. */
--primary: oklch(0.42 0.15 255);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.93 0.008 245);
--secondary-foreground: oklch(0.22 0.01 250);
/* muted-foreground is still reading text in an access theme — it must
* clear 7:1 on white too. 0.35³=0.0429 → 1.05/0.0929 ≈ 11.3:1. */
--muted: oklch(0.945 0.004 245);
--muted-foreground: oklch(0.35 0.01 250);
/* Accent — a pale blue wash for hover/selection, deep blue ink on top
* (0.854/0.083 ≈ 10.3:1). Never a second competing fill. */
--accent: oklch(0.93 0.03 250);
--accent-foreground: oklch(0.32 0.11 255);
/* ── Status ── deep solid fills with white labels (destructive /
* success / info), dark ink on amber for warning — every pair targets
* ≥7:1. destructive 1.05/(0.45³+.05) ≈ 7.4:1 (red's true Y is lower →
* real ratio higher); success deepened to 0.40 because green's true Y
* runs ABOVE L³ (1.05/0.114 ≈ 9.2:1 approx, ≈7.5:1 real); info
* 1.05/0.124 ≈ 8.5:1; warning ink-on-amber 0.562/0.058 ≈ 9.7:1. */
--destructive: oklch(0.45 0.17 27);
--destructive-foreground: oklch(1 0 0);
--success: oklch(0.40 0.11 155);
--success-foreground: oklch(1 0 0);
--warning: oklch(0.80 0.14 80);
--warning-foreground: oklch(0.20 0.03 80);
--info: oklch(0.42 0.11 230);
--info-foreground: oklch(1 0 0);
/* Borders STRONG — ink at 0.250.3 alpha. Visible boundaries for low
* vision, not decorative hairlines. */
--border: oklch(0.15 0.005 250 / 0.28);
--input: oklch(0.15 0.005 250 / 0.32);
--ring: oklch(0.42 0.15 255);
/* Chart palette — blue lead, adjacent hues ≥60° apart, adjacent-index
* lightness varied so series separate for color-blind readers too. */
--chart-1: oklch(0.45 0.14 255);
--chart-2: oklch(0.58 0.12 150);
--chart-3: oklch(0.70 0.13 80);
--chart-4: oklch(0.55 0.16 20);
--chart-5: oklch(0.48 0.14 310);
--sidebar: oklch(0.965 0.004 245);
--sidebar-foreground: oklch(0.15 0.005 250);
--sidebar-primary: oklch(0.42 0.15 255);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.93 0.03 250);
--sidebar-accent-foreground: oklch(0.32 0.11 255);
--sidebar-border: oklch(0.15 0.005 250 / 0.25);
--sidebar-ring: oklch(0.42 0.15 255);
/* Syntax — every token deep enough to read as body text on white;
* comments are NOT faded into illegibility (0.40 ≈ 9:1). */
--syntax-keyword: oklch(0.42 0.14 310);
--syntax-string: oklch(0.42 0.10 155);
--syntax-number: oklch(0.45 0.12 55);
--syntax-comment: oklch(0.40 0.01 250);
--syntax-function: oklch(0.42 0.13 255);
--syntax-type: oklch(0.42 0.09 210);
/* ── Chat surfaces ──
* User bubble is the deep blue with white text (≈8.5:1); assistant is
* a white card with a strong visible border. */
--chat-user-bg: oklch(0.42 0.15 255);
--chat-user-fg: oklch(1 0 0);
--chat-assistant-bg: oklch(1 0 0);
--chat-assistant-fg: oklch(0.15 0.005 250);
--chat-assistant-border: oklch(0.15 0.005 250 / 0.28);
/* Modest radius — calm, institutional, never pill-happy. */
--radius: 0.5rem;
/* ── Motion ── minimal and quick; nothing decorative. All springs
* alias to the standard eases — personality lives in restraint. */
--duration-fast: 120ms;
--duration-base: 200ms;
--duration-slow: 300ms;
--duration-slower: 400ms;
--duration-spring: 200ms;
--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 ── access ramp: starts larger, generous line-heights,
* caption never below 14px. Fixed rem, product register. */
--text-hero-size: 3rem;
--text-hero-lh: 1.15;
--text-display-size: 2.5rem;
--text-display-lh: 1.2;
--text-headline-size: 1.875rem;
--text-headline-lh: 1.25;
--text-title-size: 1.5rem;
--text-title-lh: 1.3;
--text-subtitle-size: 1.25rem;
--text-subtitle-lh: 1.45;
--text-body-size: 1.125rem;
--text-body-lh: 1.65;
--text-label-size: 1rem;
--text-label-lh: 1.5;
--text-caption-size: 0.875rem;
--text-caption-lh: 1.4;
/* ── Elevation ── low. Separation is the job of borders here; shadows
* only whisper altitude for popovers and dialogs. */
--elevation-0: none;
--elevation-1: 0 1px 2px oklch(0.15 0.005 250 / 0.05);
--elevation-2: 0 1px 3px oklch(0.15 0.005 250 / 0.07), 0 1px 2px oklch(0.15 0.005 250 / 0.04);
--elevation-3: 0 4px 8px -2px oklch(0.15 0.005 250 / 0.08), 0 2px 4px -2px oklch(0.15 0.005 250 / 0.04);
--elevation-4: 0 12px 24px -6px oklch(0.15 0.005 250 / 0.1), 0 4px 8px -4px oklch(0.15 0.005 250 / 0.05);
--elevation-5: 0 20px 40px -8px oklch(0.15 0.005 250 / 0.14);
--spacing: 0.25rem;
/* No sub-pixel hairlines in an access theme — thin is a real 1px. */
--border-width-thin: 1px;
--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 — very dark cool charcoal with very light ink (≈17.3:1), cards
* stepping clearly lighter (bg 0.16 → card 0.22 → popover 0.24 → muted
* 0.26: popover above card, below muted). Primary flips to a lighter blue
* carrying near-black ink (≈8.5:1) — dark-ink-on-bright beats
* white-on-mid-tone.
*/
.dark {
--background: oklch(0.16 0.005 250);
--foreground: oklch(0.96 0.005 250);
--card: oklch(0.22 0.005 250);
--card-foreground: oklch(0.96 0.005 250);
--popover: oklch(0.24 0.005 250);
--popover-foreground: oklch(0.96 0.005 250);
/* Lighter blue, near-black ink: (0.74³+.05)/(0.15³+.05) ≈ 8.5:1. */
--primary: oklch(0.74 0.11 255);
--primary-foreground: oklch(0.15 0.01 255);
--secondary: oklch(0.26 0.006 250);
--secondary-foreground: oklch(0.93 0.005 250);
/* muted-fg 0.78: on card 0.22 ≈ 8.7:1, on bg ≈ 9.7:1, on muted 0.26
* ≈ 7.8:1 — muted text stays AAA reading text in dark mode too. */
--muted: oklch(0.26 0.006 250);
--muted-foreground: oklch(0.78 0.01 250);
--accent: oklch(0.30 0.05 255);
--accent-foreground: oklch(0.85 0.08 255);
/* Status flips to bright swatches with near-black ink so every pair
* clears ≥7:1: destructive 0.423/0.054 ≈ 7.8:1 approx (kept at 0.72
* because red's true Y runs below L³); success ≈ 9.7:1; warning
* ≈ 11.1:1; info ≈ 9.0:1. */
--destructive: oklch(0.72 0.16 25);
--destructive-foreground: oklch(0.16 0.02 25);
--success: oklch(0.78 0.14 155);
--success-foreground: oklch(0.16 0.03 155);
--warning: oklch(0.82 0.14 80);
--warning-foreground: oklch(0.16 0.03 80);
--info: oklch(0.76 0.10 230);
--info-foreground: oklch(0.15 0.03 230);
/* Strong light-ink borders — boundaries stay visible at night too. */
--border: oklch(0.96 0.005 250 / 0.28);
--input: oklch(0.96 0.005 250 / 0.32);
--ring: oklch(0.74 0.11 255);
--chart-1: oklch(0.70 0.12 255);
--chart-2: oklch(0.78 0.13 150);
--chart-3: oklch(0.82 0.13 80);
--chart-4: oklch(0.70 0.15 20);
--chart-5: oklch(0.68 0.13 310);
--sidebar: oklch(0.18 0.005 250);
--sidebar-foreground: oklch(0.96 0.005 250);
--sidebar-primary: oklch(0.74 0.11 255);
--sidebar-primary-foreground: oklch(0.15 0.01 255);
--sidebar-accent: oklch(0.30 0.05 255);
--sidebar-accent-foreground: oklch(0.85 0.08 255);
--sidebar-border: oklch(0.96 0.005 250 / 0.25);
--sidebar-ring: oklch(0.74 0.11 255);
--syntax-keyword: oklch(0.78 0.11 310);
--syntax-string: oklch(0.80 0.11 155);
--syntax-number: oklch(0.82 0.11 55);
--syntax-comment: oklch(0.74 0.01 250);
--syntax-function: oklch(0.78 0.10 255);
--syntax-type: oklch(0.80 0.08 210);
/* ── Chat surfaces (dark) ── user bubble is a deep blue panel with
* very light ink (≈12:1); assistant matches the card. */
--chat-user-bg: oklch(0.30 0.06 255);
--chat-user-fg: oklch(0.96 0.005 250);
--chat-assistant-bg: oklch(0.22 0.005 250);
--chat-assistant-fg: oklch(0.96 0.005 250);
--chat-assistant-border: oklch(0.96 0.005 250 / 0.28);
--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 — Lucent's access-first philosophy.
*
* Atkinson Hyperlegible everywhere; no negative tracking (the font was
* drawn for character disambiguation — don't squeeze it). Links carry a
* real underline at rest. Boundaries are borders, not shadows.
*/
@layer base {
body {
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
letter-spacing: 0;
font-family: var(--font-sans);
font-size: var(--text-body-size);
line-height: var(--text-body-lh);
}
/* Headings — same family, weight 700 does the hierarchy work.
* `color: inherit` (NOT var(--foreground)) so headings sitting on
* colored slabs keep the slab's foreground pairing. */
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: 700;
letter-spacing: 0;
color: inherit;
}
code, kbd, samp, pre,
[data-slot="code"],
[data-slot="code-block"] {
font-family: var(--font-mono);
}
/* Inline code — foreground-tint chip with foreground ink (house
* pattern); slightly stronger tint so the chip boundary is visible. */
:not(pre) > code {
padding: 0.1em 0.35em;
border-radius: calc(var(--radius) * 0.5);
background: color-mix(in oklch, var(--foreground) 8%, transparent);
color: var(--foreground);
font-size: 0.9em;
}
/* Selection — a solid blue tint, ink stays put. */
::selection {
background: color-mix(in oklch, var(--primary) 25%, transparent);
color: var(--foreground);
}
/* Links — the blue, UNDERLINED AT REST (an affordance you can see,
* not one you must discover); the underline thickens on hover. */
a {
color: var(--primary);
text-decoration-line: underline;
text-decoration-thickness: 1px;
text-decoration-color: currentColor;
text-underline-offset: 0.2em;
transition: text-decoration-thickness var(--duration-fast) var(--ease-standard);
}
a:hover {
text-decoration-thickness: 2px;
}
[data-slot="sidebar"] {
background: var(--sidebar);
border-right: var(--border-width-base) solid var(--sidebar-border);
}
}
/*
* Focus-visible — thicker than the house norm: a 3px solid ring, offset
* 2px, on every actionable control. Keyboard users are first-class here.
* Placed outside @layer base so it beats Tailwind's default ring utilities.
*/
[data-slot="button"]:focus-visible,
[data-slot="select-trigger"]:focus-visible,
[data-slot="checkbox"]:focus-visible,
[data-slot="radio-group-item"]:focus-visible,
[data-slot="switch"]:focus-visible,
[data-slot="tabs-trigger"]:focus-visible,
a:focus-visible {
outline: 3px solid var(--ring);
outline-offset: 2px;
box-shadow: none;
}
/* Buttons — bold labels (Atkinson ships 400/700; the 700 IS the button
* voice, GOV.UK style). */
[data-slot="button"] {
font-weight: 700;
letter-spacing: 0;
}
[data-slot="button"]:hover:not(:disabled) {
opacity: 0.9;
}
[data-slot="button"]:active:not(:disabled) {
opacity: 0.82;
}
/* 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 — 44px minimum for EVERYONE, not gated behind a coarse-
* pointer query. Tremor, low vision, and arthritis don't check your
* pointer type. */
[data-slot="button"] {
min-height: 2.75rem;
min-width: 2.75rem;
}
[data-slot="input"],
[data-slot="select-trigger"] {
min-height: 2.75rem;
}
/* Coarse pointers keep the same floor (harmless, kept for parity with the
* house pattern). */
@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;
}
}
/* Inputs — white field, strong visible border; focus is a border-color
* change PLUS a 2px ring so the state change never hides. */
[data-slot="input"],
[data-slot="textarea"],
[data-slot="select-trigger"] {
background: var(--card);
border: var(--border-width-base) solid var(--input);
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: 1;
}
[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) 45%, transparent);
}
[data-slot="input"]:focus,
[data-slot="input"]:focus-visible,
[data-slot="textarea"]:focus,
[data-slot="textarea"]:focus-visible,
[data-slot="select-trigger"]:focus {
border-color: var(--primary);
box-shadow: 0 0 0 2px var(--ring);
outline: none;
}
/* Reduced motion — transitions on interactive slots drop to zero; nothing
* in this theme is motion-dependent. */
@media (prefers-reduced-motion: reduce) {
[data-slot="button"],
[data-slot="input"],
[data-slot="textarea"],
[data-slot="select-trigger"],
[data-slot="tabs-trigger"],
[data-slot="switch"],
[data-slot="checkbox"],
a {
transition: none;
}
}