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>
This commit is contained in:
jules
2026-07-08 07:20:58 +10:00
commit 4c761b91a4
2 changed files with 530 additions and 0 deletions

473
theme.css Normal file
View File

@@ -0,0 +1,473 @@
/*
* 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;
}
}