/* * 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; } }