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:
57
README.md
Normal file
57
README.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# lib-theme-lucent
|
||||||
|
|
||||||
|
**Lucent — cool daylight clarity.** An access-first Crema theme where WCAG AAA is the
|
||||||
|
identity, not the compliance line. One of three candidates in the access family, built
|
||||||
|
for NDIS participants, their families, and older support coordinators (IGH / IveGotHome
|
||||||
|
portals): dignity, clarity, zero squinting. The discipline IS the personality.
|
||||||
|
|
||||||
|
## Thesis
|
||||||
|
|
||||||
|
GOV.UK / NHS design heritage, modernized. Light mode is a near-white cool field with
|
||||||
|
pure white cards and TRUE near-black ink — body text lands at ~19:1, and even
|
||||||
|
`muted-foreground` clears 7:1 on white, because in an access theme muted text is still
|
||||||
|
reading text. One deep accessible blue does all the pointing (white label ≈ 8.5:1), and
|
||||||
|
links are that same blue, **underlined at rest** — the underline thickens on hover.
|
||||||
|
Dark mode is a very dark cool charcoal with very light ink (~17:1); cards step clearly
|
||||||
|
lighter, and primary flips to a lighter blue carrying near-black ink (≈ 8.5:1).
|
||||||
|
|
||||||
|
The access-family DNA:
|
||||||
|
|
||||||
|
- **Type ramp starts larger** — body 1.125rem/1.65, label 1rem/1.5, caption 0.875rem
|
||||||
|
(never below 14px). Generous line-heights throughout.
|
||||||
|
- **Contrast verified, not hoped for** — body ink and muted-foreground ≥7:1 on
|
||||||
|
background AND card in both modes; primary label ≥7:1; status pairs target ≥7:1 and
|
||||||
|
never dip below 4.5:1. Computed ratios are stated in `theme.css` comments.
|
||||||
|
- **Focus is loud** — 3px solid ring + 2px offset on buttons/controls; inputs get a
|
||||||
|
border-color change plus a 2px ring. Always visible.
|
||||||
|
- **Touch targets 44px for everyone** — not gated behind a coarse-pointer query.
|
||||||
|
- **Borders are strong** (ink at 0.25–0.32 alpha) — visible boundaries for low vision;
|
||||||
|
elevation stays low; separation comes from borders, not shadows.
|
||||||
|
- **Motion minimal and quick** (120/200/300/400ms), all springs aliased to standard
|
||||||
|
eases; a `prefers-reduced-motion` block zeroes transitions on interactive slots.
|
||||||
|
- Radius a modest 0.5rem. No glass (inert stubs).
|
||||||
|
|
||||||
|
## Fonts
|
||||||
|
|
||||||
|
**Atkinson Hyperlegible** (Braille Institute; drawn for character disambiguation at low
|
||||||
|
vision) for both body and headings — hierarchy comes from weight 700 and the ramp, not a
|
||||||
|
second family. **JetBrains Mono** for code. No negative letter-spacing anywhere: the
|
||||||
|
face was designed for legibility; don't squeeze it.
|
||||||
|
|
||||||
|
Consuming apps load fonts at the very top of their entry CSS (never inside `theme.css`):
|
||||||
|
|
||||||
|
```css
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:wght@400;700&family=JetBrains+Mono:wght@400;500;600&display=swap");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Atkinson Hyperlegible ships 400/700 only — buttons and headings use 700 (the GOV.UK
|
||||||
|
button voice); never request intermediate weights or they'll synthesize.
|
||||||
|
- Dark surfaces: background 0.16 → card 0.22 → popover 0.24 → muted 0.26 (popover above
|
||||||
|
card, below muted).
|
||||||
|
- Status pattern: light mode uses deep solid fills with white labels (dark ink on amber
|
||||||
|
for warning); dark mode flips to bright swatches with near-black ink.
|
||||||
|
- Headings use `color: inherit` so titles on colored slabs keep the slab's pairing;
|
||||||
|
`[data-slot="card-title"]` is pinned to the subtitle role.
|
||||||
|
- Validate: `node create-crema-app/scripts/validate-theme.mjs lib-theme-lucent/theme.css`
|
||||||
473
theme.css
Normal file
473
theme.css
Normal 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.25–0.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.25–0.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user