Files
lib-theme-beacon/theme.css
jules 1c57027888 lib-theme-beacon: 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

458 lines
17 KiB
CSS
Raw Permalink 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.

/*
* Beacon — dark-native AAA. The lighthouse theme.
*
* Access-first / maximum-legibility family, candidate 1 of 3. WCAG AAA is the
* identity, not the compliance checkbox: built for NDIS participants, their
* families, and older support coordinators — including photophobic and
* low-vision users who need DARK high contrast. The discipline IS the
* personality.
*
* The dark mode is the theme. A near-black cool-neutral field (L 0.13 — dark
* but never pure #000, which causes halation for astigmatic users) carries
* very light ink at ~18:1. Cards step clearly LIGHTER (L 0.19) so surface
* boundaries are legible without squinting, and borders are strong light-alpha
* strokes (0.3) — on near-black, borders do all the separation; elevation
* stays low. One accent: a bright accessible yellow-gold (hue 95) with
* near-black ink on top — the classic high-contrast-dark action idiom, a
* lighthouse beam. Links are the yellow, underlined by default. Focus is a
* 3px yellow outline — unmistakable.
*
* Light mode is the competent daylight variant: white field, near-black ink
* ~19:1, primary flips to a deep amber-brown with a white label, and the
* yellow survives as the selection highlight.
*
* Status hues are tuned so every label pair computes ≥7:1 (AAA at body size),
* and warning sits at orange (hue 5560) well away from the yellow primary
* (hue 95) so the two never read as the same swatch.
*
* Type: Public Sans (USWDS accessibility heritage) for body AND headings
* (weight 600); IBM Plex Mono for code. The ramp starts larger than product
* default — body 1.125rem/1.65, nothing below 14px — with generous
* line-heights throughout. Radius modest (0.5rem). Motion minimal and quick;
* all springs alias to standard; reduced-motion zeroes transitions.
*
* Contrast ratios in comments are computed with the validator's own OKLCH →
* sRGB → WCAG-luminance math (quick check: Y ≈ L³; ratio = (Y1+.05)/(Y2+.05)).
*
* Token contract: Crema v1 (matches lib-theme-harbor/theme.css structure).
*/
/*
* Fonts: consuming apps load Public Sans + IBM Plex Mono at the top of their
* entry CSS via the Google Fonts URL documented in this theme's README.
* Embedding the @import here breaks PostCSS ordering, so it is not done.
*/
:root {
--font-sans: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-heading: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
/* Tailwind utility families — mirror --font-heading/--font-sans. */
--heading-family: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--body-family: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
/* ── Colors (light) ── white field, near-black ink, deep amber-brown
* action. foreground/background 18.58:1; foreground/card 19.12:1. */
--background: oklch(0.99 0.002 250);
--foreground: oklch(0.17 0.004 250);
--card: oklch(1 0 0);
--card-foreground: oklch(0.17 0.004 250);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.17 0.004 250);
/* Primary — deep amber-brown (the beam by day), white label 8.30:1 (AAA).
* The bright yellow returns as the selection highlight (--accent). */
--primary: oklch(0.42 0.11 80);
--primary-foreground: oklch(0.99 0.005 80);
--secondary: oklch(0.94 0.004 250);
--secondary-foreground: oklch(0.25 0.005 250); /* 13.42:1 */
--muted: oklch(0.94 0.004 250);
/* muted-foreground: 8.22:1 on background, 8.46:1 on card, 7.09:1 on muted
* — AAA everywhere it can land. */
--muted-foreground: oklch(0.42 0.006 250);
/* Accent — the yellow kept for selection/hover wash, deep amber ink
* 10.22:1. */
--accent: oklch(0.9 0.1 95);
--accent-foreground: oklch(0.3 0.06 80);
/* ── Status ── deep fills, white labels, every pair ≥7:1 (AAA).
* Warning at hue 55 (orange), far from primary's 8095 (yellow). */
--destructive: oklch(0.45 0.18 27);
--destructive-foreground: oklch(0.99 0.005 27); /* 7.93:1 */
--success: oklch(0.42 0.12 150);
--success-foreground: oklch(0.99 0.005 150); /* 7.76:1 */
--warning: oklch(0.45 0.13 55);
--warning-foreground: oklch(0.99 0.005 55); /* 7.54:1 */
--info: oklch(0.44 0.11 245);
--info-foreground: oklch(0.99 0.005 245); /* 7.50:1 */
/* Strong visible boundaries — 0.25 alpha ink, not a whisper. */
--border: oklch(0.17 0.004 250 / 0.25);
--input: oklch(0.17 0.004 250 / 0.3);
--ring: oklch(0.42 0.11 80);
/* Charts — amber lead, hues ≥60° apart (20/85/160/245/305), adjacent
* lightness varied, all dark enough to read on white. */
--chart-1: oklch(0.55 0.12 85);
--chart-2: oklch(0.42 0.13 245);
--chart-3: oklch(0.52 0.12 160);
--chart-4: oklch(0.6 0.16 20);
--chart-5: oklch(0.45 0.15 305);
--sidebar: oklch(0.96 0.003 250);
--sidebar-foreground: oklch(0.17 0.004 250); /* 17.02:1 */
--sidebar-primary: oklch(0.42 0.11 80);
--sidebar-primary-foreground: oklch(0.99 0.005 80);
--sidebar-accent: oklch(0.9 0.1 95);
--sidebar-accent-foreground: oklch(0.3 0.06 80);
--sidebar-border: oklch(0.17 0.004 250 / 0.2);
--sidebar-ring: oklch(0.42 0.11 80);
/* Syntax — deep, high-contrast hues on white; comment stays ≥7:1. */
--syntax-keyword: oklch(0.45 0.15 305);
--syntax-string: oklch(0.42 0.11 150);
--syntax-number: oklch(0.45 0.12 55);
--syntax-comment: oklch(0.45 0.005 250);
--syntax-function: oklch(0.44 0.12 245);
--syntax-type: oklch(0.42 0.08 200);
/* ── Chat surfaces ── user bubble is a deep charcoal panel with light
* text (15.10:1); assistant is a white card with a strong border. */
--chat-user-bg: oklch(0.25 0.005 250);
--chat-user-fg: oklch(0.98 0.002 250);
--chat-assistant-bg: oklch(1 0 0);
--chat-assistant-fg: oklch(0.17 0.004 250);
--chat-assistant-border: oklch(0.17 0.004 250 / 0.25);
/* Modest radius — calm, legible geometry. */
--radius: 0.5rem;
/* ── Motion ── minimal and quick; springs alias to standard — the
* personality is steadiness, never bounce. */
--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-standard);
--ease-spring-bouncy: var(--ease-standard);
/* ── Typography ── access ramp: starts larger, generous line-heights,
* caption never below 14px. */
--text-hero-size: 3rem;
--text-hero-lh: 1.1;
--text-display-size: 2.5rem;
--text-display-lh: 1.15;
--text-headline-size: 1.875rem;
--text-headline-lh: 1.25;
--text-title-size: 1.5rem;
--text-title-lh: 1.35;
--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; /* 14px — the floor, never below */
--text-caption-lh: 1.4;
/* ── Elevation ── low; borders carry separation. */
--elevation-0: none;
--elevation-1: 0 1px 2px oklch(0.17 0.004 250 / 0.06);
--elevation-2: 0 1px 3px oklch(0.17 0.004 250 / 0.08), 0 1px 2px oklch(0.17 0.004 250 / 0.05);
--elevation-3: 0 4px 8px -2px oklch(0.17 0.004 250 / 0.08), 0 2px 4px -2px oklch(0.17 0.004 250 / 0.05);
--elevation-4: 0 12px 24px -6px oklch(0.17 0.004 250 / 0.1), 0 4px 8px -4px oklch(0.17 0.004 250 / 0.05);
--elevation-5: 0 20px 40px -8px oklch(0.17 0.004 250 / 0.14);
--spacing: 0.25rem;
--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 — the identity. Near-black cool-neutral field (not #000 — avoids
* halation), very light ink, cards stepping clearly lighter, strong
* light-alpha borders, and the yellow-gold beam with near-black ink on top.
* Popover sits above card, below muted (0.19 < 0.21 < 0.24).
*/
.dark {
/* foreground/background 18.44:1. */
--background: oklch(0.13 0.004 250);
--foreground: oklch(0.97 0.002 250);
/* Cards step clearly LIGHTER — visible surface, not a guess. 16.93:1. */
--card: oklch(0.19 0.005 250);
--card-foreground: oklch(0.97 0.002 250);
--popover: oklch(0.21 0.005 250);
--popover-foreground: oklch(0.97 0.002 250); /* 16.24:1 */
/* Primary — the beam. Bright yellow-gold (hue 95, clear of warning's 60)
* with near-black ink: 12.96:1. Beam vs field itself: 12.75:1. */
--primary: oklch(0.85 0.16 95);
--primary-foreground: oklch(0.11 0.01 95);
--secondary: oklch(0.24 0.006 250);
--secondary-foreground: oklch(0.93 0.003 250); /* 13.38:1 */
--muted: oklch(0.24 0.006 250);
/* muted-foreground: 10.05:1 on background, 9.23:1 on card, 8.22:1 on
* muted — AAA on every field it can sit on. */
--muted-foreground: oklch(0.78 0.005 250);
/* Accent — dim gold wash with bright gold ink, 10.14:1. */
--accent: oklch(0.3 0.05 95);
--accent-foreground: oklch(0.9 0.12 95);
/* ── Status ── bright fills, near-black ink (the beacon idiom), every
* pair ≥7:1. Warning at orange 60 — primary holds yellow 95. */
--destructive: oklch(0.7 0.19 30);
--destructive-foreground: oklch(0.11 0.02 30); /* 7.06:1 */
--success: oklch(0.8 0.19 145);
--success-foreground: oklch(0.12 0.03 145); /* 11.57:1 */
--warning: oklch(0.78 0.16 60);
--warning-foreground: oklch(0.12 0.03 60); /* 9.72:1 */
--info: oklch(0.78 0.1 230);
--info-foreground: oklch(0.11 0.02 230); /* 10.43:1 */
/* STRONG light-alpha borders — on near-black these do all separation. */
--border: oklch(0.97 0.002 250 / 0.3);
--input: oklch(0.97 0.002 250 / 0.35);
--ring: oklch(0.85 0.16 95);
/* Charts — gold lead, hues 25/95/160/240/305 (≥65° apart), adjacent
* lightness varied, all bright enough to read on near-black. */
--chart-1: oklch(0.85 0.16 95);
--chart-2: oklch(0.68 0.13 240);
--chart-3: oklch(0.78 0.14 160);
--chart-4: oklch(0.7 0.17 25);
--chart-5: oklch(0.6 0.15 305);
--sidebar: oklch(0.16 0.004 250);
--sidebar-foreground: oklch(0.97 0.002 250); /* 17.80:1 */
--sidebar-primary: oklch(0.85 0.16 95);
--sidebar-primary-foreground: oklch(0.11 0.01 95);
--sidebar-accent: oklch(0.3 0.05 95);
--sidebar-accent-foreground: oklch(0.9 0.12 95);
--sidebar-border: oklch(0.97 0.002 250 / 0.25);
--sidebar-ring: oklch(0.85 0.16 95);
/* Syntax — bright, well-spaced hues on near-black. */
--syntax-keyword: oklch(0.8 0.1 305);
--syntax-string: oklch(0.82 0.12 150);
--syntax-number: oklch(0.84 0.12 60);
--syntax-comment: oklch(0.72 0.005 250);
--syntax-function: oklch(0.8 0.09 230);
--syntax-type: oklch(0.82 0.08 190);
/* ── Chat surfaces (dark) ── user panel a step above muted; assistant
* on the card surface with the strong border. User pair 14.24:1. */
--chat-user-bg: oklch(0.26 0.008 250);
--chat-user-fg: oklch(0.97 0.002 250);
--chat-assistant-bg: oklch(0.19 0.005 250);
--chat-assistant-fg: oklch(0.97 0.002 250);
--chat-assistant-border: oklch(0.97 0.002 250 / 0.3);
/* Elevation near-zero — borders are the separation system on near-black. */
--elevation-1: 0 1px 2px oklch(0 0 0 / 0.4);
--elevation-2: 0 1px 3px oklch(0 0 0 / 0.45), 0 1px 2px oklch(0 0 0 / 0.3);
--elevation-3: 0 4px 8px -2px oklch(0 0 0 / 0.45), 0 2px 4px -2px oklch(0 0 0 / 0.3);
--elevation-4: 0 12px 24px -6px oklch(0 0 0 / 0.5), 0 4px 8px -4px oklch(0 0 0 / 0.3);
--elevation-5: 0 20px 40px -8px oklch(0 0 0 / 0.6);
}
/*
* @layer base — Beacon's legibility-first philosophy.
*
* Larger type, generous leading, underlined links, strong boundaries, one
* unmistakable accent. Nothing decorative; everything readable.
*/
@layer base {
body {
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: var(--font-sans);
font-size: var(--text-body-size);
line-height: var(--text-body-lh);
}
/* Headings — Public Sans 600. `color: inherit` (NOT var(--foreground))
* so headings recolor correctly inside tinted surfaces (alerts, chips,
* status banners). */
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: 600;
letter-spacing: -0.01em;
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; never a
* low-contrast pastel. */
: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.925em;
}
/* Selection — the gold wash by day. */
::selection {
background: var(--accent);
color: var(--accent-foreground);
}
/* Selection in the dark — the beam itself. */
.dark ::selection {
background: var(--primary);
color: var(--primary-foreground);
}
/* Links — always underlined (never color-only affordance); underline
* thickens on hover. Amber-brown by day, the yellow beam at night. */
a {
color: var(--primary);
text-decoration-line: underline;
text-decoration-thickness: 1px;
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 — Beacon's signature: a 3px solid ring, offset 2px, in the
* theme's accent (deep amber by day, the yellow beam in the dark). Placed
* outside @layer base so it beats Tailwind's default ring utilities.
* Always visible; never outline: none without replacement.
*/
[data-slot="button"]:focus-visible,
[data-slot="select-trigger"]:focus-visible {
outline: 3px solid var(--ring);
outline-offset: 2px;
box-shadow: none;
}
/* Inputs focus — border-color change + 2px ring (flush, no offset gap that
* could read as a broken border). */
[data-slot="input"]:focus-visible,
[data-slot="textarea"]:focus-visible,
[data-slot="input"]:focus,
[data-slot="textarea"]:focus,
[data-slot="select-trigger"]:focus {
outline: none;
border-color: var(--ring);
box-shadow: 0 0 0 2px var(--ring);
}
/* Buttons — steady weight, no transform theatrics; state reads through
* opacity only, product-fast. */
[data-slot="button"] {
font-weight: 600;
min-height: 2.75rem;
}
[data-slot="button"]:hover:not(:disabled) {
opacity: 0.92;
}
[data-slot="button"]:active:not(:disabled) {
opacity: 0.84;
}
/* 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 — the 44px floor is GLOBAL, not behind a media query.
* Motor-impairment doesn't correlate with pointer type. */
[data-slot="input"],
[data-slot="select-trigger"] {
min-height: 2.75rem;
}
@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 — card surface, strong border, generous hit area. */
[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);
}
[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);
}
/* Reduced motion — zero transitions on every interactive slot; nothing
* essential is motion-only. */
@media (prefers-reduced-motion: reduce) {
a,
[data-slot="button"],
[data-slot="input"],
[data-slot="textarea"],
[data-slot="select-trigger"] {
transition: none;
}
}