@yuzu-jobs/ui-components 0.0.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -6,19 +6,37 @@ import InputSelect from "./src/InputSelect.astro";
6
6
  import InputSwitch from "./src/InputSwitch.astro";
7
7
  import InputContainer from "./src/InputContainer.astro";
8
8
  import InputLabel from "./src/InputLabel.astro";
9
+ import InputChips from "./src/InputChips.astro";
10
+ import InputRadio from "./src/InputRadio.astro";
11
+ import InputTextarea from "./src/InputTextarea.astro";
9
12
  import Alert from "./src/Alert.astro";
10
13
  import ProgressBar from "./src/ProgressBar.astro";
11
14
  import Button from "./src/Button.astro";
12
15
  import ProfileSetupFlowHeader from "./src/ProfileSetupFlowHeader.astro";
16
+ import StatusDot from "./src/StatusDot.astro";
17
+ import DashboardSidebarNavItem from "./src/DashboardSidebarNavItem.astro";
18
+ import PageTabs from "./src/PageTabs.astro";
19
+ import Pagination from "./src/Pagination.astro";
20
+ import BackLink from "./src/BackLink.astro";
21
+ import JobSalary from "./src/JobSalary.astro";
13
22
 
14
23
  export {
15
24
  Button,
25
+ StatusDot,
16
26
  InputText,
17
27
  InputSelect,
18
28
  InputSwitch,
19
29
  InputContainer,
20
30
  InputLabel,
31
+ InputChips,
32
+ InputRadio,
33
+ InputTextarea,
21
34
  Alert,
22
35
  ProgressBar,
23
36
  ProfileSetupFlowHeader,
37
+ DashboardSidebarNavItem,
38
+ PageTabs,
39
+ Pagination,
40
+ BackLink,
41
+ JobSalary,
24
42
  };
package/package.json CHANGED
@@ -3,14 +3,16 @@
3
3
  "engines": {
4
4
  "node": ">=22.12.0"
5
5
  },
6
- "version": "0.0.8",
6
+ "version": "0.2.0",
7
7
  "type": "module",
8
8
  "exports": {
9
- ".": "./index.ts"
9
+ ".": "./index.ts",
10
+ "./theme.css": "./theme.css"
10
11
  },
11
12
  "files": [
12
13
  "src",
13
- "index.ts"
14
+ "index.ts",
15
+ "theme.css"
14
16
  ],
15
17
  "keywords": [
16
18
  "astro-component"
package/src/Alert.astro CHANGED
@@ -15,14 +15,14 @@ const variantClasses: Record<Variant, string> = {
15
15
  success: "bg-emerald-50 border-emerald-500 text-emerald-800",
16
16
  error: "bg-red-50 border-red-500 text-red-800",
17
17
  warning: "bg-amber-50 border-amber-500 text-amber-800",
18
- info: "bg-indigo-50 border-indigo-500 text-indigo-800",
18
+ info: "bg-brand-50 border-brand-500 text-brand-800",
19
19
  };
20
20
 
21
21
  const iconClasses: Record<Variant, string> = {
22
22
  success: "text-emerald-500",
23
23
  error: "text-red-500",
24
24
  warning: "text-amber-500",
25
- info: "text-indigo-500",
25
+ info: "text-brand-500",
26
26
  };
27
27
 
28
28
  const icons: Record<Variant, typeof CircleCheck> = {
@@ -38,7 +38,7 @@ const Icon = icons[variant];
38
38
  <div
39
39
  id={id}
40
40
  class:list={[
41
- "flex items-start gap-3 border border-transparent border-l-4 px-4 py-3 rounded-lg text-sm font-sans shadow-sm",
41
+ "flex items-start gap-3 border border-transparent border-l-4 px-4 py-3 rounded-lg text-sm font-sans",
42
42
  variantClasses[variant],
43
43
  className,
44
44
  ]}
@@ -0,0 +1,18 @@
1
+ ---
2
+ import { ArrowLeft } from "@lucide/astro";
3
+
4
+ interface Props {
5
+ href: string;
6
+ label: string;
7
+ }
8
+
9
+ const { href, label } = Astro.props;
10
+ ---
11
+
12
+ <a
13
+ href={href}
14
+ class="inline-flex items-center gap-1.5 mb-2 text-sm text-slate-500 hover:text-brand-600 transition-colors group"
15
+ >
16
+ <ArrowLeft class="w-3.5 h-3.5 transition-transform group-hover:-translate-x-0.5" />
17
+ {label}
18
+ </a>
package/src/Button.astro CHANGED
@@ -3,7 +3,7 @@ interface Props {
3
3
  id?: string;
4
4
  href?: string;
5
5
  type?: "button" | "submit" | "reset";
6
- variant?: "primary" | "secondary" | "outline" | "ghost" | "danger";
6
+ variant?: "primary" | "accent" | "secondary" | "outline" | "ghost" | "danger";
7
7
  size?: "sm" | "md" | "lg";
8
8
  disabled?: boolean;
9
9
  fullWidth?: boolean;
@@ -28,20 +28,23 @@ const {
28
28
  } = Astro.props;
29
29
 
30
30
  const variantClasses = {
31
- // Filled — solid background, white text. Use for primary CTAs on any background.
31
+ // Filled ink — solid background, white text. Use for primary CTAs on light backgrounds.
32
32
  primary:
33
- "bg-indigo-600 hover:bg-indigo-700 text-white border border-transparent",
33
+ "bg-primary hover:bg-primary-hover text-white border border-transparent",
34
+ // Filled teal — the brand accent. Use sparingly (e.g. spoke "Join free" CTA).
35
+ accent:
36
+ "bg-accent hover:bg-accent-hover text-white border border-transparent",
34
37
  // Danger — filled red, white text. Use for destructive actions.
35
38
  danger: "bg-red-600 hover:bg-red-700 text-white border border-transparent",
36
- // Outline — dark border and text on transparent background. Use for secondary actions on light backgrounds.
39
+ // Outline — ink text, hairline border. Use for secondary actions on light backgrounds.
37
40
  secondary:
38
- "text-gray-700 border border-gray-300 hover:bg-gray-100 hover:border-gray-400 hover:text-gray-900",
41
+ "text-ink-900 border border-slate-300 hover:bg-slate-50 hover:border-slate-400",
39
42
  // Alias for secondary
40
43
  outline:
41
- "text-gray-700 border border-gray-300 hover:bg-gray-100 hover:border-gray-400 hover:text-gray-900",
42
- // Ghost — subtle indigo outline on transparent background. Use for actions on dark/indigo backgrounds.
44
+ "text-ink-900 border border-slate-300 hover:bg-slate-50 hover:border-slate-400",
45
+ // Ghost — subtle teal outline on transparent background. Use for actions on dark (ink) surfaces.
43
46
  ghost:
44
- "text-indigo-200 border border-indigo-500/50 hover:bg-indigo-500/20 hover:text-white hover:border-indigo-400",
47
+ "text-brand-200 border border-brand-500/50 hover:bg-brand-500/20 hover:text-white hover:border-brand-400",
45
48
  };
46
49
 
47
50
  const sizeClasses = {
@@ -51,7 +54,7 @@ const sizeClasses = {
51
54
  };
52
55
 
53
56
  const classes = [
54
- "inline-flex items-center gap-1.5 rounded-lg font-sans font-medium cursor-pointer transition-colors",
57
+ "inline-flex items-center gap-1.5 rounded-md font-sans font-medium cursor-pointer transition-colors",
55
58
  "disabled:opacity-40 disabled:cursor-not-allowed",
56
59
  variantClasses[variant],
57
60
  sizeClasses[size],
@@ -0,0 +1,33 @@
1
+ ---
2
+ import type { AstroComponentFactory } from "astro/runtime/server/index.js";
3
+
4
+ interface Props {
5
+ name: string;
6
+ href: string;
7
+ icon: AstroComponentFactory;
8
+ isActive: boolean;
9
+ }
10
+
11
+ const { name, href, icon: Icon, isActive } = Astro.props;
12
+ ---
13
+
14
+ <li class="flex">
15
+ <a
16
+ class={`relative w-full px-3 py-2.5 flex items-center gap-3 rounded-md text-sm font-sans transition-colors ${
17
+ isActive
18
+ ? "bg-brand-500/15 text-white"
19
+ : "text-ink-300 hover:bg-white/5 hover:text-white"
20
+ }`}
21
+ href={href}
22
+ >
23
+ {
24
+ isActive && (
25
+ <span class="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-5 bg-brand-500" />
26
+ )
27
+ }
28
+ <span class={isActive ? "text-brand-400" : "text-ink-400"}>
29
+ <Icon size={16} />
30
+ </span>
31
+ {name}
32
+ </a>
33
+ </li>
@@ -0,0 +1,73 @@
1
+ ---
2
+ interface Option {
3
+ value: string;
4
+ label: string;
5
+ }
6
+
7
+ interface Props {
8
+ name: string;
9
+ options: Option[];
10
+ values?: string[];
11
+ required?: boolean;
12
+ locked?: boolean;
13
+ lockedValues?: string[];
14
+ }
15
+
16
+ const {
17
+ name,
18
+ options,
19
+ values = [],
20
+ required,
21
+ locked = false,
22
+ lockedValues = [],
23
+ } = Astro.props;
24
+
25
+ const activeValues = locked ? lockedValues : values;
26
+ ---
27
+
28
+ <div class="flex flex-wrap gap-2" data-chips-name={name}>
29
+ {
30
+ locked &&
31
+ lockedValues.map((value) => (
32
+ <input type="hidden" name={name} value={value} />
33
+ ))
34
+ }
35
+ {
36
+ options.map((option) => {
37
+ const inputId = `${name}-${option.value}`;
38
+ const isChecked = activeValues.includes(option.value);
39
+ return (
40
+ <label
41
+ class:list={[
42
+ "group",
43
+ locked ? "cursor-not-allowed" : "cursor-pointer",
44
+ ]}
45
+ for={inputId}
46
+ >
47
+ <input
48
+ class="sr-only peer"
49
+ type="checkbox"
50
+ id={inputId}
51
+ name={locked ? undefined : name}
52
+ value={option.value}
53
+ checked={isChecked}
54
+ disabled={locked}
55
+ required={!locked && required && option.value === activeValues[0]}
56
+ />
57
+ <span
58
+ class:list={[
59
+ "inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium border transition-colors",
60
+ locked
61
+ ? isChecked
62
+ ? "bg-primary border-primary text-white opacity-50 cursor-not-allowed"
63
+ : "bg-white border-slate-200 text-slate-400 opacity-50 cursor-not-allowed"
64
+ : "bg-white border-slate-200 text-slate-600 cursor-pointer peer-checked:bg-primary peer-checked:border-primary peer-checked:text-white group-hover:border-primary group-hover:text-primary peer-checked:group-hover:text-white peer-focus-visible:ring-2 peer-focus-visible:ring-primary peer-focus-visible:ring-offset-2",
65
+ ]}
66
+ >
67
+ {option.label}
68
+ </span>
69
+ </label>
70
+ );
71
+ })
72
+ }
73
+ </div>
@@ -0,0 +1,40 @@
1
+ ---
2
+ interface Option {
3
+ id: string;
4
+ value: string;
5
+ label: string;
6
+ }
7
+
8
+ interface Props {
9
+ name: string;
10
+ options: Option[];
11
+ required?: boolean;
12
+ defaultValue?: string;
13
+ }
14
+
15
+ const { name, options, required, defaultValue } = Astro.props;
16
+ ---
17
+
18
+ <div class="flex flex-col gap-2">
19
+ {
20
+ options.map((option) => (
21
+ <label
22
+ class="flex items-center gap-3 cursor-pointer group"
23
+ for={option.id}
24
+ >
25
+ <input
26
+ class="w-4 h-4 border-slate-200 accent-primary focus:ring-2 focus:ring-primary focus:ring-offset-1 cursor-pointer"
27
+ type="radio"
28
+ id={option.id}
29
+ name={name}
30
+ value={option.value}
31
+ required={required ?? false}
32
+ checked={option.value === defaultValue}
33
+ />
34
+ <span class="text-sm group-hover:text-primary transition-colors">
35
+ {option.label}
36
+ </span>
37
+ </label>
38
+ ))
39
+ }
40
+ </div>
@@ -0,0 +1,29 @@
1
+ ---
2
+ interface Props {
3
+ id: string;
4
+ name: string;
5
+ placeholder?: string;
6
+ required?: boolean;
7
+ value?: string;
8
+ rows?: number;
9
+ maxLength?: number;
10
+ minLength?: number;
11
+ }
12
+
13
+ const { id, name, placeholder, required, value, rows, maxLength, minLength } =
14
+ Astro.props;
15
+
16
+ const className =
17
+ "p-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent resize-y";
18
+ ---
19
+
20
+ <textarea
21
+ class={className}
22
+ id={id}
23
+ name={name}
24
+ placeholder={placeholder ?? ""}
25
+ required={required ?? false}
26
+ rows={rows ?? 4}
27
+ maxlength={maxLength ?? 2000}
28
+ minlength={minLength ?? 0}
29
+ >{value ?? ""}</textarea>
@@ -0,0 +1,47 @@
1
+ ---
2
+ type SalaryUnitType = "HOUR" | "DAY" | "WEEK" | "MONTH" | "YEAR";
3
+
4
+ interface Props {
5
+ min?: number;
6
+ max?: number;
7
+ /** Accepts plain strings from loosely-typed API payloads; unknown values
8
+ render without a unit suffix. */
9
+ unitType?: SalaryUnitType | (string & {});
10
+ class?: string;
11
+ }
12
+
13
+ const { min, max, unitType, class: className } = Astro.props;
14
+
15
+ const formatSalary = (amount: number) => {
16
+ if (amount >= 100_000) {
17
+ return `$${amount / 1000}K`;
18
+ }
19
+ return new Intl.NumberFormat("en-US", {
20
+ style: "currency",
21
+ currency: "USD",
22
+ minimumFractionDigits: amount % 1 === 0 ? 0 : 2,
23
+ maximumFractionDigits: amount % 1 === 0 ? 0 : 2,
24
+ }).format(amount);
25
+ };
26
+
27
+ const salaryUnitLabel: Record<SalaryUnitType, string> = {
28
+ HOUR: "/hr",
29
+ DAY: "/day",
30
+ WEEK: "/wk",
31
+ MONTH: "/mo",
32
+ YEAR: "/yr",
33
+ };
34
+
35
+ const range = (() => {
36
+ const unit =
37
+ unitType && unitType in salaryUnitLabel
38
+ ? salaryUnitLabel[unitType as SalaryUnitType]
39
+ : "";
40
+ if (min && max) return `${formatSalary(min)} – ${formatSalary(max)}${unit}`;
41
+ if (min) return `From ${formatSalary(min)}${unit}`;
42
+ if (max) return `Up to ${formatSalary(max)}${unit}`;
43
+ return null;
44
+ })();
45
+ ---
46
+
47
+ {range && <span class={className}>{range}</span>}
@@ -0,0 +1,43 @@
1
+ ---
2
+ interface Tab {
3
+ label: string;
4
+ href: string;
5
+ /** Optional notification badge count — only shown when > 0 */
6
+ badge?: number;
7
+ }
8
+
9
+ interface Props {
10
+ tabs: Tab[];
11
+ /** The href of the currently active tab (exact match) */
12
+ activeHref: string;
13
+ }
14
+
15
+ const { tabs, activeHref } = Astro.props;
16
+ ---
17
+
18
+ <div class="bg-white border-b border-slate-200 px-4 md:px-8">
19
+ <nav class="flex -mb-px">
20
+ {
21
+ tabs.map((tab) => {
22
+ const isActive = tab.href === activeHref;
23
+ return (
24
+ <a
25
+ href={tab.href}
26
+ class={`inline-flex items-center gap-2 px-4 py-3 text-sm font-medium font-sans border-b-2 transition-colors ${
27
+ isActive
28
+ ? "border-brand-600 text-brand-700"
29
+ : "border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300"
30
+ }`}
31
+ >
32
+ {tab.label}
33
+ {tab.badge !== undefined && tab.badge > 0 && (
34
+ <span class="inline-flex items-center justify-center min-w-[1.1rem] h-[1.1rem] px-1 rounded-full text-xs font-bold bg-amber-400 text-amber-900 leading-none">
35
+ {tab.badge}
36
+ </span>
37
+ )}
38
+ </a>
39
+ );
40
+ })
41
+ }
42
+ </nav>
43
+ </div>
@@ -0,0 +1,106 @@
1
+ ---
2
+ import { ChevronLeft, ChevronRight } from "@lucide/astro";
3
+
4
+ interface Props {
5
+ currentPage: number;
6
+ totalPages: number;
7
+ }
8
+
9
+ const { currentPage, totalPages } = Astro.props;
10
+
11
+ function pageHref(page: number): string {
12
+ const url = new URL(Astro.url);
13
+ url.searchParams.set("page", String(page));
14
+ return url.pathname + url.search;
15
+ }
16
+
17
+ function getPageNumbers(current: number, total: number): (number | "...")[] {
18
+ if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
19
+
20
+ const set = new Set(
21
+ [1, total, current - 1, current, current + 1].filter(
22
+ (p) => p >= 1 && p <= total,
23
+ ),
24
+ );
25
+ const sorted = Array.from(set).sort((a, b) => a - b);
26
+
27
+ const result: (number | "...")[] = [];
28
+ for (let i = 0; i < sorted.length; i++) {
29
+ if (i > 0 && sorted[i] - sorted[i - 1] > 1) result.push("...");
30
+ result.push(sorted[i]);
31
+ }
32
+ return result;
33
+ }
34
+
35
+ const pages = getPageNumbers(currentPage, totalPages);
36
+ const hasPrev = currentPage > 1;
37
+ const hasNext = currentPage < totalPages;
38
+ ---
39
+
40
+ <nav aria-label="Pagination" class="flex items-center justify-center gap-1">
41
+ <!-- Previous -->
42
+ {
43
+ hasPrev ? (
44
+ <a
45
+ href={pageHref(currentPage - 1)}
46
+ aria-label="Previous page"
47
+ class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-slate-500 hover:bg-brand-50 hover:text-brand-700 transition-colors"
48
+ >
49
+ <ChevronLeft class="w-4 h-4" />
50
+ </a>
51
+ ) : (
52
+ <span
53
+ aria-disabled="true"
54
+ class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-slate-300 cursor-not-allowed"
55
+ >
56
+ <ChevronLeft class="w-4 h-4" />
57
+ </span>
58
+ )
59
+ }
60
+
61
+ <!-- Page numbers -->
62
+ {
63
+ pages.map((page) =>
64
+ page === "..." ? (
65
+ <span class="inline-flex items-center justify-center w-9 h-9 text-sm text-slate-400 select-none">
66
+ &hellip;
67
+ </span>
68
+ ) : page === currentPage ? (
69
+ <span
70
+ aria-current="page"
71
+ class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-sm font-semibold bg-brand-600 text-white"
72
+ >
73
+ {page}
74
+ </span>
75
+ ) : (
76
+ <a
77
+ href={pageHref(page)}
78
+ aria-label={`Page ${page}`}
79
+ class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-sm text-slate-600 hover:bg-brand-50 hover:text-brand-700 transition-colors"
80
+ >
81
+ {page}
82
+ </a>
83
+ ),
84
+ )
85
+ }
86
+
87
+ <!-- Next -->
88
+ {
89
+ hasNext ? (
90
+ <a
91
+ href={pageHref(currentPage + 1)}
92
+ aria-label="Next page"
93
+ class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-slate-500 hover:bg-brand-50 hover:text-brand-700 transition-colors"
94
+ >
95
+ <ChevronRight class="w-4 h-4" />
96
+ </a>
97
+ ) : (
98
+ <span
99
+ aria-disabled="true"
100
+ class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-slate-300 cursor-not-allowed"
101
+ >
102
+ <ChevronRight class="w-4 h-4" />
103
+ </span>
104
+ )
105
+ }
106
+ </nav>
@@ -11,19 +11,19 @@ interface Props {
11
11
  const { step, totalSteps, title, userRole } = Astro.props;
12
12
  ---
13
13
 
14
- <div class="bg-indigo-900 border-b border-indigo-800/60">
14
+ <div class="bg-ink-900 border-b border-ink-800/60">
15
15
  <div class="container mx-auto max-w-3xl px-8 py-6">
16
16
  <div class="flex items-center gap-3 mb-4">
17
- <span class="text-indigo-400 text-sm font-sans">Account setup</span>
18
- <span class="text-indigo-700">/</span>
19
- <span class="text-indigo-200 text-sm font-sans"
17
+ <span class="text-ink-300 text-sm font-sans">Account setup</span>
18
+ <span class="text-ink-500">/</span>
19
+ <span class="text-ink-200 text-sm font-sans"
20
20
  >{userRole == "recruiter" ? "Recruiter" : "Candidate"} profile</span
21
21
  >
22
22
  </div>
23
23
 
24
24
  <div class="mb-5">
25
25
  <p
26
- class="text-indigo-400 text-xs font-sans uppercase tracking-widest font-semibold mb-1"
26
+ class="text-brand-400 text-[11px] font-mono uppercase tracking-[0.08em] font-semibold mb-1"
27
27
  >
28
28
  Step {step} of {totalSteps}
29
29
  </p>
@@ -9,9 +9,9 @@ const { totalSteps, currentStep, class: className } = Astro.props;
9
9
  ---
10
10
 
11
11
  <div class:list={[className]}>
12
- <div class="w-full bg-gray-200 rounded-full h-2">
12
+ <div class="w-full bg-slate-200/40 rounded-full h-2">
13
13
  <div
14
- class="bg-primary h-2 rounded-full"
14
+ class="bg-brand-500 h-2 rounded-full"
15
15
  style={`width: ${Math.max(8, ((currentStep - 1) / totalSteps) * 100)}%`}
16
16
  >
17
17
  </div>
@@ -0,0 +1,33 @@
1
+ ---
2
+ // Status indicator: colored dot + plain text (replaces pill badges).
3
+ // Semantic colors per the Precision theme: emerald=active/success,
4
+ // slate=draft/neutral, amber=closed/attention, teal=pending, red=error.
5
+ type Color = "emerald" | "slate" | "amber" | "teal" | "red";
6
+
7
+ interface Props {
8
+ color: Color;
9
+ label: string;
10
+ class?: string;
11
+ }
12
+
13
+ const { color, label, class: className } = Astro.props;
14
+
15
+ const dotClasses: Record<Color, string> = {
16
+ emerald: "bg-emerald-500",
17
+ slate: "bg-slate-400",
18
+ amber: "bg-amber-500",
19
+ teal: "bg-brand-500",
20
+ red: "bg-red-500",
21
+ };
22
+ ---
23
+
24
+ <span
25
+ class:list={[
26
+ "inline-flex items-center gap-1.5 text-[13px] font-sans text-slate-600 whitespace-nowrap",
27
+ className,
28
+ ]}
29
+ >
30
+ <span class:list={["w-1.5 h-1.5 rounded-full shrink-0", dotClasses[color]]}
31
+ ></span>
32
+ {label}
33
+ </span>
package/theme.css ADDED
@@ -0,0 +1,61 @@
1
+ /*
2
+ * Shared "Precision" design tokens for mizu.health (hub) and the spoke sites.
3
+ * Consumers @import this after tailwindcss in their global.css:
4
+ *
5
+ * @import "tailwindcss";
6
+ * @import "@yuzu-jobs/ui-components/theme.css";
7
+ *
8
+ * The --font-schibsted-grotesk / --font-spline-sans-mono variables are
9
+ * provided by each app's astro.config.mjs fonts entry (astro:assets <Font>).
10
+ */
11
+ @theme {
12
+ --font-sans: var(--font-schibsted-grotesk), ui-sans-serif, system-ui,
13
+ sans-serif;
14
+ --font-mono: var(--font-spline-sans-mono), ui-monospace, monospace;
15
+
16
+ /* Brand = teal (accent) scale, built from the logo node #0E8FA6 */
17
+ --color-brand-50: #eff6f8;
18
+ --color-brand-100: #ddf3f6;
19
+ --color-brand-200: #b6e0e7;
20
+ --color-brand-300: #7cc6d2;
21
+ --color-brand-400: #3faabc;
22
+ --color-brand-500: #14b0c6; /* bright accent — dark surfaces */
23
+ --color-brand-600: #0e8fa6; /* the logo node — primary accent */
24
+ --color-brand-700: #0b6e80;
25
+ --color-brand-800: #0a5866;
26
+ --color-brand-900: #0a4854;
27
+ --color-brand-950: #06222e;
28
+
29
+ /* Ink = structural navy (nav, sidebar, headings, primary buttons) */
30
+ --color-ink-50: #f4f7fa;
31
+ --color-ink-100: #e7edf3;
32
+ --color-ink-200: #c4d2df;
33
+ --color-ink-300: #93a9bd;
34
+ --color-ink-400: #5f7b94;
35
+ --color-ink-500: #3d5a75;
36
+ --color-ink-600: #28425c;
37
+ --color-ink-700: #1a344c;
38
+ --color-ink-800: #142c42;
39
+ --color-ink-900: #0f2e4c; /* the logo ink */
40
+ --color-ink-950: #0a1f33;
41
+
42
+ /* Semantic */
43
+ --color-primary: var(--color-ink-900); /* primary buttons are INK, not teal */
44
+ --color-primary-hover: var(--color-ink-700);
45
+ --color-accent: var(--color-brand-600); /* links, data, highlights */
46
+ --color-accent-hover: var(--color-brand-700);
47
+
48
+ /* Neutrals: the theme standardizes on slate. gray-* utilities are aliased
49
+ to slate so existing markup renders correctly; prefer slate-* in new code. */
50
+ --color-gray-50: var(--color-slate-50);
51
+ --color-gray-100: var(--color-slate-100);
52
+ --color-gray-200: var(--color-slate-200);
53
+ --color-gray-300: var(--color-slate-300);
54
+ --color-gray-400: var(--color-slate-400);
55
+ --color-gray-500: var(--color-slate-500);
56
+ --color-gray-600: var(--color-slate-600);
57
+ --color-gray-700: var(--color-slate-700);
58
+ --color-gray-800: var(--color-slate-800);
59
+ --color-gray-900: var(--color-slate-900);
60
+ --color-gray-950: var(--color-slate-950);
61
+ }