/* * 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 55–60) 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 80–95 (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; } }