@outs-tand-ing/ui 0.5.7 → 0.6.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/dist/index.js CHANGED
@@ -29,7 +29,7 @@ function C(e, t, n, r, a, o) {
29
29
  "text-5xl": n.size === "lg"
30
30
  }]) }, "OUTSTANDING", 2);
31
31
  }
32
- var w = /*#__PURE__*/ S(x, [["render", C], ["__file", "/Users/kemal/Developer/outstanding/main/outs/ui/src/components/BrandLogo.vue"]]), T = {
32
+ var w = /*#__PURE__*/ S(x, [["render", C], ["__file", "/Users/kemal/Developer/outstanding/ws/design-tokens/outs/ui/src/components/BrandLogo.vue"]]), T = {
33
33
  squircle: {
34
34
  width: 100,
35
35
  height: 100,
@@ -269,7 +269,7 @@ function L(t, n, o, s, l, f) {
269
269
  var R = /*#__PURE__*/ S(O, [
270
270
  ["render", L],
271
271
  ["__scopeId", "data-v-00a84a8e"],
272
- ["__file", "/Users/kemal/Developer/outstanding/main/outs/ui/src/components/SVGShape.vue"]
272
+ ["__file", "/Users/kemal/Developer/outstanding/ws/design-tokens/outs/ui/src/components/SVGShape.vue"]
273
273
  ]), z = /*@__PURE__*/ s({
274
274
  __name: "AccessWrapper",
275
275
  props: {
@@ -325,7 +325,7 @@ function K(e, t, s, c, l, u) {
325
325
  }, null, -1), a("div", G, [_(e.$slots, "footer")])], 512)
326
326
  ]);
327
327
  }
328
- var q = /*#__PURE__*/ S(z, [["render", K], ["__file", "/Users/kemal/Developer/outstanding/main/outs/ui/src/components/AccessWrapper.vue"]]), J = /*@__PURE__*/ s({
328
+ var q = /*#__PURE__*/ S(z, [["render", K], ["__file", "/Users/kemal/Developer/outstanding/ws/design-tokens/outs/ui/src/components/AccessWrapper.vue"]]), J = /*@__PURE__*/ s({
329
329
  __name: "ScrollableArea",
330
330
  props: {
331
331
  axis: {
@@ -407,7 +407,7 @@ function Y(e, t, n, o, s, c) {
407
407
  }, null, 6))
408
408
  ], 2);
409
409
  }
410
- var X = /*#__PURE__*/ S(J, [["render", Y], ["__file", "/Users/kemal/Developer/outstanding/main/outs/ui/src/components/ScrollableArea.vue"]]);
410
+ var X = /*#__PURE__*/ S(J, [["render", Y], ["__file", "/Users/kemal/Developer/outstanding/ws/design-tokens/outs/ui/src/components/ScrollableArea.vue"]]);
411
411
  //#endregion
412
412
  //#region src/utils/svg-parse.ts
413
413
  function Z(e) {
@@ -0,0 +1,169 @@
1
+ /** A 4pt scale. Native names spell the numeric prefixes out (`3xs` → `xxxs`) to stay valid identifiers. */
2
+ export declare const space: {
3
+ readonly '3xs': 2;
4
+ readonly '2xs': 4;
5
+ readonly xs: 8;
6
+ readonly sm: 12;
7
+ readonly md: 16;
8
+ readonly lg: 24;
9
+ readonly xl: 32;
10
+ readonly '2xl': 48;
11
+ readonly '3xl': 64;
12
+ };
13
+ export declare const radius: {
14
+ readonly sm: 4;
15
+ readonly md: 8;
16
+ readonly lg: 16;
17
+ readonly sheet: 28;
18
+ };
19
+ export declare const motion: {
20
+ readonly duration: {
21
+ readonly fast: 150;
22
+ readonly base: 250;
23
+ readonly slow: 400;
24
+ };
25
+ /** M3's standard easing; reduced motion swaps every transition for an instant or opacity-only change. */
26
+ readonly easing: readonly [0.2, 0, 0, 1];
27
+ };
28
+ /** `Livvic-SemiBold.ttf` → `livvic_semibold`: an Android resource name is lowercase, digits and underscores. */
29
+ export declare const androidFont: (file: string) => string;
30
+ /** Livvic ships as static weights (no variable file, no 800), one file per weight the roles use. */
31
+ export declare const typeface: {
32
+ readonly family: "Livvic";
33
+ readonly dir: "livvic";
34
+ readonly files: {
35
+ readonly 400: "Livvic-Regular.ttf";
36
+ readonly 500: "Livvic-Medium.ttf";
37
+ readonly 600: "Livvic-SemiBold.ttf";
38
+ readonly 700: "Livvic-Bold.ttf";
39
+ readonly 900: "Livvic-Black.ttf";
40
+ };
41
+ };
42
+ export type IosTextStyle = 'largeTitle' | 'title' | 'title2' | 'title3' | 'headline' | 'body' | 'callout' | 'subheadline' | 'footnote' | 'caption' | 'caption2';
43
+ export type M3Slot = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall' | 'labelLarge' | 'labelMedium' | 'labelSmall';
44
+ export interface TypeRole {
45
+ /** pt / sp / px at the default text size; native scales it through `ios` and `m3`. */
46
+ size: number;
47
+ weight: number;
48
+ /** A multiple of size. */
49
+ lineHeight: number;
50
+ /** em. */
51
+ tracking: number;
52
+ tabular?: boolean;
53
+ /** Web only: the responsive step `typography.css` already used, where the role had one. */
54
+ clamp?: string;
55
+ ios: IosTextStyle;
56
+ m3: M3Slot;
57
+ }
58
+ export declare const typeRoles: {
59
+ readonly display: {
60
+ readonly size: 40;
61
+ readonly weight: 900;
62
+ readonly lineHeight: 1.1;
63
+ readonly tracking: -0.02;
64
+ readonly clamp: "clamp(2rem, 5vw, 3.5rem)";
65
+ readonly ios: "largeTitle";
66
+ readonly m3: "displaySmall";
67
+ };
68
+ readonly title: {
69
+ readonly size: 28;
70
+ readonly weight: 700;
71
+ readonly lineHeight: 1.15;
72
+ readonly tracking: -0.01;
73
+ readonly clamp: "clamp(1.5rem, 3.5vw, 2.25rem)";
74
+ readonly ios: "title";
75
+ readonly m3: "headlineMedium";
76
+ };
77
+ readonly headline: {
78
+ readonly size: 20;
79
+ readonly weight: 600;
80
+ readonly lineHeight: 1.25;
81
+ readonly tracking: 0;
82
+ readonly clamp: "clamp(1.25rem, 2.5vw, 1.75rem)";
83
+ readonly ios: "title3";
84
+ readonly m3: "titleLarge";
85
+ };
86
+ readonly body: {
87
+ readonly size: 16;
88
+ readonly weight: 400;
89
+ readonly lineHeight: 1.5;
90
+ readonly tracking: 0;
91
+ readonly ios: "body";
92
+ readonly m3: "bodyLarge";
93
+ };
94
+ readonly label: {
95
+ readonly size: 14;
96
+ readonly weight: 500;
97
+ readonly lineHeight: 1.3;
98
+ readonly tracking: 0.01;
99
+ readonly ios: "subheadline";
100
+ readonly m3: "labelLarge";
101
+ };
102
+ readonly caption: {
103
+ readonly size: 12;
104
+ readonly weight: 400;
105
+ readonly lineHeight: 1.35;
106
+ readonly tracking: 0.01;
107
+ readonly ios: "caption";
108
+ readonly m3: "bodySmall";
109
+ };
110
+ readonly numeral: {
111
+ readonly size: 16;
112
+ readonly weight: 500;
113
+ readonly lineHeight: 1.3;
114
+ readonly tracking: 0;
115
+ readonly tabular: true;
116
+ readonly ios: "body";
117
+ readonly m3: "titleMedium";
118
+ };
119
+ };
120
+ export type SeasonVar = '--color-season' | '--color-season-text' | '--color-accent' | '--color-surface' | '--color-border';
121
+ export type Mode = 'light' | 'dark';
122
+ /**
123
+ * Two tones, one set of names. `season` is the season's own colour at full strength, for the
124
+ * season track (season, round, question). `neutral` is the platform's own light/dark greys
125
+ * carrying a faint wash of the season's accent, for every page outside it.
126
+ */
127
+ export declare const semantic: {
128
+ readonly season: {
129
+ readonly surface: "--color-season";
130
+ readonly surfaceRaised: "--color-surface";
131
+ readonly separator: "--color-border";
132
+ readonly textPrimary: "--color-season-text";
133
+ readonly accent: "--color-accent";
134
+ };
135
+ readonly neutral: {
136
+ readonly base: {
137
+ readonly light: {
138
+ readonly surface: "#f2f2f7";
139
+ readonly surfaceRaised: "#ffffff";
140
+ readonly separator: "#d1d1d6";
141
+ readonly textPrimary: "#111111";
142
+ };
143
+ readonly dark: {
144
+ readonly surface: "#000000";
145
+ readonly surfaceRaised: "#1c1c1e";
146
+ readonly separator: "#38383a";
147
+ readonly textPrimary: "#f2f2f2";
148
+ };
149
+ };
150
+ /** Share of the season accent mixed into each neutral base. */
151
+ readonly tint: {
152
+ readonly surface: 0.04;
153
+ readonly surfaceRaised: 0.03;
154
+ readonly separator: 0.08;
155
+ readonly textPrimary: 0;
156
+ };
157
+ };
158
+ /** text-secondary is text-primary mixed into the surface: this share at the faintest, more where a season needs it to clear 4.5:1. */
159
+ readonly textSecondaryShare: 0.7;
160
+ /** Not a season colour: red-700 / red-400, the reds that clear 4.5:1 on every season surface. */
161
+ readonly danger: {
162
+ readonly light: "#b91c1c";
163
+ readonly dark: "#f87171";
164
+ };
165
+ };
166
+ export declare const semanticRoles: readonly ["textPrimary", "textSecondary", "surface", "surfaceRaised", "separator", "accent", "danger", "onAccent"];
167
+ export type SemanticRole = (typeof semanticRoles)[number];
168
+ export type Tone = 'season' | 'neutral';
169
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/tokens/tokens.ts"],"names":[],"mappings":"AAIA,2GAA2G;AAC3G,eAAO,MAAM,KAAK;;;;;;;;;;CAA+F,CAAA;AAEjH,eAAO,MAAM,MAAM;;;;;CAA+C,CAAA;AAElE,eAAO,MAAM,MAAM;;;;;;IAEjB,yGAAyG;;CAEjG,CAAA;AAEV,gHAAgH;AAChH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,WAAgE,CAAA;AAExG,oGAAoG;AACpG,eAAO,MAAM,QAAQ;;;;;;;;;;CAIX,CAAA;AAEV,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,CAAA;AAC/J,MAAM,MAAM,MAAM,GAAG,cAAc,GAAG,eAAe,GAAG,cAAc,GAAG,eAAe,GAAG,gBAAgB,GAAG,eAAe,GAAG,YAAY,GAAG,aAAa,GAAG,YAAY,GAAG,WAAW,GAAG,YAAY,GAAG,WAAW,GAAG,YAAY,GAAG,aAAa,GAAG,YAAY,CAAA;AAEpQ,MAAM,WAAW,QAAQ;IACvB,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,0BAA0B;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,2FAA2F;IAC3F,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,YAAY,CAAA;IACjB,EAAE,EAAE,MAAM,CAAA;CACX;AAED,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAQuB,CAAA;AAG7C,MAAM,MAAM,SAAS,GAAG,gBAAgB,GAAG,qBAAqB,GAAG,gBAAgB,GAAG,iBAAiB,GAAG,gBAAgB,CAAA;AAE1H,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,MAAM,CAAA;AAEnC;;;;GAIG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;QAajB,+DAA+D;;;;;;;;IAGjE,sIAAsI;;IAEtI,iGAAiG;;;;;CAOlG,CAAA;AAED,eAAO,MAAM,aAAa,oHAAqH,CAAA;AAC/I,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAA;AACzD,MAAM,MAAM,IAAI,GAAG,QAAQ,GAAG,SAAS,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outs-tand-ing/ui",
3
- "version": "0.5.7",
3
+ "version": "0.6.0",
4
4
  "description": "Shared Vue components, icons, and design tokens for the outstanding platform",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "./style.css": "./dist/ui.css",
17
17
  "./typography.css": "./src/assets/typography.css",
18
+ "./tokens.css": "./src/tokens/generated/tokens.css",
18
19
  "./components.css": "./src/assets/components.css",
19
20
  "./seasons/24": {
20
21
  "types": "./dist/seasons/season24/index.d.ts",
@@ -40,7 +41,10 @@
40
41
  "./seasons/25.css": "./src/seasons/season25/style.css",
41
42
  "./seasons/26.css": "./src/seasons/season26/style.css",
42
43
  "./seasons/wc26.css": "./src/seasons/fifawc26/style.css",
43
- "./seasons/27.css": "./src/seasons/season27/style.css"
44
+ "./seasons/27.css": "./src/seasons/season27/style.css",
45
+ "./seasons/-1.css": "./src/seasons/seasonminus1/style.css",
46
+ "./seasons/0.css": "./src/seasons/season0/style.css",
47
+ "./seasons/1.css": "./src/seasons/season1/style.css"
44
48
  },
45
49
  "files": [
46
50
  "dist",
@@ -62,7 +66,8 @@
62
66
  "generate:ios:icon": "bun scripts/generate-ios-icon.ts",
63
67
  "generate:android:wordmark": "bun scripts/generate-android-wordmark.ts",
64
68
  "generate:ios:wordmark": "bun scripts/generate-ios-wordmark.ts",
65
- "generate:favicon": "bun scripts/generate-favicon.ts"
69
+ "generate:favicon": "bun scripts/generate-favicon.ts",
70
+ "generate:tokens": "bun scripts/generate-tokens.ts"
66
71
  },
67
72
  "peerDependencies": {
68
73
  "vue": "^3.5.0"
@@ -0,0 +1,93 @@
1
+ Copyright 2019 The Livvic Project Authors (https://github.com/Fonthausen/Livvic)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -1,10 +1,12 @@
1
- @font-face {
2
- font-family: 'Posterama';
3
- src: url('./fonts/Posterama_1984_W04_Black.ttf') format('truetype');
4
- font-weight: 900;
5
- font-style: normal;
6
- font-display: swap;
7
- }
1
+ /* The type roles for web. Sizes, weights and line heights are the tokens (tokens.css, generated from
2
+ src/tokens/tokens.ts), so this file only says which element takes which role. Import tokens.css
3
+ first. Posterama is the logo's alone and ships here no longer. */
4
+
5
+ @font-face { font-family: 'Livvic'; src: url('./fonts/livvic/Livvic-Regular.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
6
+ @font-face { font-family: 'Livvic'; src: url('./fonts/livvic/Livvic-Medium.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; }
7
+ @font-face { font-family: 'Livvic'; src: url('./fonts/livvic/Livvic-SemiBold.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; }
8
+ @font-face { font-family: 'Livvic'; src: url('./fonts/livvic/Livvic-Bold.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
9
+ @font-face { font-family: 'Livvic'; src: url('./fonts/livvic/Livvic-Black.woff2') format('woff2'); font-weight: 900; font-style: normal; font-display: swap; }
8
10
 
9
11
  @utility text-trim {
10
12
  text-box-trim: trim-both;
@@ -21,30 +23,39 @@
21
23
  text-box-edge: cap alphabetic;
22
24
  }
23
25
 
26
+ @utility type-display { font-size: var(--type-display-size); font-weight: var(--type-display-weight); line-height: var(--type-display-line-height); letter-spacing: var(--type-display-tracking); }
27
+ @utility type-title { font-size: var(--type-title-size); font-weight: var(--type-title-weight); line-height: var(--type-title-line-height); letter-spacing: var(--type-title-tracking); }
28
+ @utility type-headline { font-size: var(--type-headline-size); font-weight: var(--type-headline-weight); line-height: var(--type-headline-line-height); letter-spacing: var(--type-headline-tracking); }
29
+ @utility type-body { font-size: var(--type-body-size); font-weight: var(--type-body-weight); line-height: var(--type-body-line-height); letter-spacing: var(--type-body-tracking); }
30
+ @utility type-label { font-size: var(--type-label-size); font-weight: var(--type-label-weight); line-height: var(--type-label-line-height); letter-spacing: var(--type-label-tracking); }
31
+ @utility type-caption { font-size: var(--type-caption-size); font-weight: var(--type-caption-weight); line-height: var(--type-caption-line-height); letter-spacing: var(--type-caption-tracking); }
32
+ @utility type-numeral { font-size: var(--type-numeral-size); font-weight: var(--type-numeral-weight); line-height: var(--type-numeral-line-height); letter-spacing: var(--type-numeral-tracking); font-variant-numeric: tabular-nums; }
33
+
24
34
  @layer base {
35
+ body {
36
+ @apply type-body;
37
+ font-family: var(--font-family-base);
38
+ color: var(--ui-text-primary);
39
+ }
40
+
25
41
  h1,
26
42
  h2,
27
43
  h3,
28
44
  h4,
29
45
  h5,
30
46
  h6 {
31
- font-family: var(--font-heading);
32
- color: var(--color-text-primary);
33
- font-weight: 900;
34
- line-height: 1.1;
35
- letter-spacing: -0.02em;
36
- transition: color 200ms ease, font-family 200ms ease;
47
+ color: var(--ui-text-primary);
48
+ transition: color var(--motion-base) var(--ease-standard);
37
49
  }
38
50
 
39
- h1 { font-size: clamp(2rem, 5vw, 3.5rem); }
40
-
41
- h2 { font-size: clamp(1.5rem, 3.5vw, 2.25rem); }
51
+ h1 { @apply type-display; }
42
52
 
43
- h3 { font-size: clamp(1.25rem, 2.5vw, 1.75rem); letter-spacing: -0.01em; }
53
+ h2 { @apply type-title; }
44
54
 
45
- h4 { font-size: 1.25rem; letter-spacing: 0; font-weight: 700; }
55
+ h3,
56
+ h4 { @apply type-headline; }
46
57
 
47
- h5 { font-size: 1rem; letter-spacing: 0; font-weight: 700; }
58
+ h5 { @apply type-label; }
48
59
 
49
- h6 { font-size: 0.875rem; letter-spacing: 0; font-weight: 600; color: var(--color-text-secondary); }
60
+ h6 { @apply type-caption; color: var(--ui-text-secondary); }
50
61
  }
@@ -0,0 +1,136 @@
1
+ // AUTO-GENERATED — do not edit manually
2
+ // Run: bun run generate:tokens (from ui/)
3
+ // Source: ui/src/tokens/tokens.ts and each ui/src/seasons/<season>/style.css
4
+ package outs.tand.ing.ui.theme
5
+
6
+ import androidx.compose.animation.core.CubicBezierEasing
7
+ import androidx.compose.material3.Typography
8
+ import androidx.compose.runtime.Immutable
9
+ import androidx.compose.ui.graphics.Color
10
+ import androidx.compose.ui.text.TextStyle
11
+ import androidx.compose.ui.text.font.Font
12
+ import androidx.compose.ui.text.font.FontFamily
13
+ import androidx.compose.ui.text.font.FontWeight
14
+ import androidx.compose.ui.unit.dp
15
+ import androidx.compose.ui.unit.em
16
+ import androidx.compose.ui.unit.sp
17
+ import outs.tand.ing.R
18
+
19
+ object Space {
20
+ val xxxs = 2.dp
21
+ val xxs = 4.dp
22
+ val xs = 8.dp
23
+ val sm = 12.dp
24
+ val md = 16.dp
25
+ val lg = 24.dp
26
+ val xl = 32.dp
27
+ val xxl = 48.dp
28
+ val xxxl = 64.dp
29
+ }
30
+
31
+ object Radius {
32
+ val sm = 4.dp
33
+ val md = 8.dp
34
+ val lg = 16.dp
35
+ val sheet = 28.dp
36
+ }
37
+
38
+ object Motion {
39
+ const val FAST_MS = 150
40
+ const val BASE_MS = 250
41
+ const val SLOW_MS = 400
42
+ val standardEasing = CubicBezierEasing(0.2f, 0f, 0f, 1f)
43
+
44
+ /** Reduced motion (the system's "Remove animations") collapses every transition to an instant change. */
45
+ fun duration(ms: Int, reduceMotion: Boolean): Int = if (reduceMotion) 0 else ms
46
+ }
47
+
48
+ /** Livvic, one static file per weight the type roles use. */
49
+ val Livvic = FontFamily(
50
+ Font(R.font.livvic_regular, FontWeight(400)),
51
+ Font(R.font.livvic_medium, FontWeight(500)),
52
+ Font(R.font.livvic_semibold, FontWeight(600)),
53
+ Font(R.font.livvic_bold, FontWeight(700)),
54
+ Font(R.font.livvic_black, FontWeight(900)),
55
+ )
56
+
57
+ object TypeRole {
58
+ val display = TextStyle(fontFamily = Livvic, fontWeight = FontWeight(900), fontSize = 40.sp, lineHeight = 44.sp, letterSpacing = (-0.02).em)
59
+ val title = TextStyle(fontFamily = Livvic, fontWeight = FontWeight(700), fontSize = 28.sp, lineHeight = 32.2.sp, letterSpacing = (-0.01).em)
60
+ val headline = TextStyle(fontFamily = Livvic, fontWeight = FontWeight(600), fontSize = 20.sp, lineHeight = 25.sp, letterSpacing = (0).em)
61
+ val body = TextStyle(fontFamily = Livvic, fontWeight = FontWeight(400), fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = (0).em)
62
+ val label = TextStyle(fontFamily = Livvic, fontWeight = FontWeight(500), fontSize = 14.sp, lineHeight = 18.2.sp, letterSpacing = (0.01).em)
63
+ val caption = TextStyle(fontFamily = Livvic, fontWeight = FontWeight(400), fontSize = 12.sp, lineHeight = 16.2.sp, letterSpacing = (0.01).em)
64
+ val numeral = TextStyle(fontFamily = Livvic, fontWeight = FontWeight(500), fontSize = 16.sp, lineHeight = 20.8.sp, letterSpacing = (0).em, fontFeatureSettings = "tnum")
65
+ }
66
+
67
+ /** The M3 type scale on the roles. A slot no role claims keeps M3's metrics in Livvic. */
68
+ val OutsTypography: Typography = Typography().let { base ->
69
+ Typography(
70
+ displayLarge = base.displayLarge.copy(fontFamily = Livvic),
71
+ displayMedium = base.displayMedium.copy(fontFamily = Livvic),
72
+ displaySmall = TypeRole.display,
73
+ headlineLarge = base.headlineLarge.copy(fontFamily = Livvic),
74
+ headlineMedium = TypeRole.title,
75
+ headlineSmall = base.headlineSmall.copy(fontFamily = Livvic),
76
+ titleLarge = TypeRole.headline,
77
+ titleMedium = TypeRole.numeral,
78
+ titleSmall = base.titleSmall.copy(fontFamily = Livvic),
79
+ bodyLarge = TypeRole.body,
80
+ bodyMedium = base.bodyMedium.copy(fontFamily = Livvic),
81
+ bodySmall = TypeRole.caption,
82
+ labelLarge = TypeRole.label,
83
+ labelMedium = base.labelMedium.copy(fontFamily = Livvic),
84
+ labelSmall = base.labelSmall.copy(fontFamily = Livvic),
85
+ )
86
+ }
87
+
88
+ enum class Tone { SEASON, NEUTRAL }
89
+
90
+ @Immutable
91
+ data class SemanticColor(
92
+ val textPrimary: Color,
93
+ val textSecondary: Color,
94
+ val surface: Color,
95
+ val surfaceRaised: Color,
96
+ val separator: Color,
97
+ val accent: Color,
98
+ val danger: Color,
99
+ val onAccent: Color,
100
+ )
101
+
102
+ fun semanticColorFor(shortName: String, tone: Tone, isDark: Boolean = false): SemanticColor = when (Triple(shortName, tone, isDark)) {
103
+ Triple("24", Tone.SEASON, false) -> SemanticColor(textPrimary = Color(0xFF5B21B6), textSecondary = Color(0xFF7E51C4), surface = Color(0xFFEDEAEF), surfaceRaised = Color(0xFFF9F5FF), separator = Color(0xFFDDD0F5), accent = Color(0xFF7C3AED), danger = Color(0xFFB91C1C), onAccent = Color(0xFFFFFFFF))
104
+ Triple("24", Tone.SEASON, true) -> SemanticColor(textPrimary = Color(0xFFD0CCD6), textSecondary = Color(0xFF9B97A1), surface = Color(0xFF1E1A24), surfaceRaised = Color(0xFF130A24), separator = Color(0xFF2E1A52), accent = Color(0xFFA78BFA), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
105
+ Triple("24", Tone.NEUTRAL, false) -> SemanticColor(textPrimary = Color(0xFF111111), textSecondary = Color(0xFF535256), surface = Color(0xFFEDEBF7), surfaceRaised = Color(0xFFFBF9FE), separator = Color(0xFFCAC5D8), accent = Color(0xFF7C3AED), danger = Color(0xFFB91C1C), onAccent = Color(0xFFFFFFFF))
106
+ Triple("24", Tone.NEUTRAL, true) -> SemanticColor(textPrimary = Color(0xFFF2F2F2), textSecondary = Color(0xFFABABAC), surface = Color(0xFF07060A), surfaceRaised = Color(0xFF201F25), separator = Color(0xFF413F49), accent = Color(0xFFA78BFA), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
107
+ Triple("25", Tone.SEASON, false) -> SemanticColor(textPrimary = Color(0xFF9A3412), textSecondary = Color(0xFFA85134), surface = Color(0xFFF0EAE7), surfaceRaised = Color(0xFFFFF8F5), separator = Color(0xFFF5D5C8), accent = Color(0xFFEC8266), danger = Color(0xFFB91C1C), onAccent = Color(0xFF000000))
108
+ Triple("25", Tone.SEASON, true) -> SemanticColor(textPrimary = Color(0xFFD4CDC8), textSecondary = Color(0xFF9F9894), surface = Color(0xFF231C19), surfaceRaised = Color(0xFF1A0F0A), separator = Color(0xFF3D2318), accent = Color(0xFFF0A08A), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
109
+ Triple("25", Tone.NEUTRAL, false) -> SemanticColor(textPrimary = Color(0xFF111111), textSecondary = Color(0xFF555354), surface = Color(0xFFF2EEF1), surfaceRaised = Color(0xFFFEFBFA), separator = Color(0xFFD3CBCD), accent = Color(0xFFEC8266), danger = Color(0xFFB91C1C), onAccent = Color(0xFF000000))
110
+ Triple("25", Tone.NEUTRAL, true) -> SemanticColor(textPrimary = Color(0xFFF2F2F2), textSecondary = Color(0xFFACABAB), surface = Color(0xFF0A0606), surfaceRaised = Color(0xFF222021), separator = Color(0xFF474040), accent = Color(0xFFF0A08A), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
111
+ Triple("26", Tone.SEASON, false) -> SemanticColor(textPrimary = Color(0xFF152038), textSecondary = Color(0xFF545C6E), surface = Color(0xFFE7E9EC), surfaceRaised = Color(0xFFF5F7FA), separator = Color(0xFFC8CEDA), accent = Color(0xFF152038), danger = Color(0xFFB91C1C), onAccent = Color(0xFFFFFFFF))
112
+ Triple("26", Tone.SEASON, true) -> SemanticColor(textPrimary = Color(0xFFCACDD4), textSecondary = Color(0xFF94979F), surface = Color(0xFF171A22), surfaceRaised = Color(0xFF0A0E18), separator = Color(0xFF1E2D48), accent = Color(0xFF6B8FD4), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
113
+ Triple("26", Tone.NEUTRAL, false) -> SemanticColor(textPrimary = Color(0xFF111111), textSecondary = Color(0xFF525254), surface = Color(0xFFE9EAEF), surfaceRaised = Color(0xFFF8F8F9), separator = Color(0xFFC2C3C9), accent = Color(0xFF152038), danger = Color(0xFFB91C1C), onAccent = Color(0xFFFFFFFF))
114
+ Triple("26", Tone.NEUTRAL, true) -> SemanticColor(textPrimary = Color(0xFFF2F2F2), textSecondary = Color(0xFFABABAC), surface = Color(0xFF040608), surfaceRaised = Color(0xFF1E1F23), separator = Color(0xFF3C3F46), accent = Color(0xFF6B8FD4), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
115
+ Triple("wc26", Tone.SEASON, false) -> SemanticColor(textPrimary = Color(0xFF152038), textSecondary = Color(0xFF545C6E), surface = Color(0xFFE7E9EC), surfaceRaised = Color(0xFFF5F7FA), separator = Color(0xFFC8CEDA), accent = Color(0xFF152038), danger = Color(0xFFB91C1C), onAccent = Color(0xFFFFFFFF))
116
+ Triple("wc26", Tone.SEASON, true) -> SemanticColor(textPrimary = Color(0xFFCACDD4), textSecondary = Color(0xFF94979F), surface = Color(0xFF171A22), surfaceRaised = Color(0xFF0A0E18), separator = Color(0xFF1E2D48), accent = Color(0xFF6B8FD4), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
117
+ Triple("wc26", Tone.NEUTRAL, false) -> SemanticColor(textPrimary = Color(0xFF111111), textSecondary = Color(0xFF525254), surface = Color(0xFFE9EAEF), surfaceRaised = Color(0xFFF8F8F9), separator = Color(0xFFC2C3C9), accent = Color(0xFF152038), danger = Color(0xFFB91C1C), onAccent = Color(0xFFFFFFFF))
118
+ Triple("wc26", Tone.NEUTRAL, true) -> SemanticColor(textPrimary = Color(0xFFF2F2F2), textSecondary = Color(0xFFABABAC), surface = Color(0xFF040608), surfaceRaised = Color(0xFF1E1F23), separator = Color(0xFF3C3F46), accent = Color(0xFF6B8FD4), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
119
+ Triple("27", Tone.SEASON, false) -> SemanticColor(textPrimary = Color(0xFF422006), textSecondary = Color(0xFF785E47), surface = Color(0xFFF5F0E0), surfaceRaised = Color(0xFFFEFCE8), separator = Color(0xFFE2D6A4), accent = Color(0xFFCA8A04), danger = Color(0xFFB91C1C), onAccent = Color(0xFF000000))
120
+ Triple("27", Tone.SEASON, true) -> SemanticColor(textPrimary = Color(0xFFFDE047), textSecondary = Color(0xFFB6A136), surface = Color(0xFF0F0F0F), surfaceRaised = Color(0xFF1C1917), separator = Color(0xFF44403C), accent = Color(0xFFFACC15), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
121
+ Triple("27", Tone.NEUTRAL, false) -> SemanticColor(textPrimary = Color(0xFF111111), textSecondary = Color(0xFF545353), surface = Color(0xFFF0EEED), surfaceRaised = Color(0xFFFDFBF7), separator = Color(0xFFD0CBC5), accent = Color(0xFFCA8A04), danger = Color(0xFFB91C1C), onAccent = Color(0xFF000000))
122
+ Triple("27", Tone.NEUTRAL, true) -> SemanticColor(textPrimary = Color(0xFFF2F2F2), textSecondary = Color(0xFFACACAA), surface = Color(0xFF0A0801), surfaceRaised = Color(0xFF23211E), separator = Color(0xFF484437), accent = Color(0xFFFACC15), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
123
+ Triple("-1", Tone.SEASON, false) -> SemanticColor(textPrimary = Color(0xFF152038), textSecondary = Color(0xFF545C6E), surface = Color(0xFFE7E9EC), surfaceRaised = Color(0xFFF5F7FA), separator = Color(0xFFC8CEDA), accent = Color(0xFF152038), danger = Color(0xFFB91C1C), onAccent = Color(0xFFFFFFFF))
124
+ Triple("-1", Tone.SEASON, true) -> SemanticColor(textPrimary = Color(0xFFCACDD4), textSecondary = Color(0xFF94979F), surface = Color(0xFF171A22), surfaceRaised = Color(0xFF0A0E18), separator = Color(0xFF1E2D48), accent = Color(0xFF6B8FD4), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
125
+ Triple("-1", Tone.NEUTRAL, false) -> SemanticColor(textPrimary = Color(0xFF111111), textSecondary = Color(0xFF525254), surface = Color(0xFFE9EAEF), surfaceRaised = Color(0xFFF8F8F9), separator = Color(0xFFC2C3C9), accent = Color(0xFF152038), danger = Color(0xFFB91C1C), onAccent = Color(0xFFFFFFFF))
126
+ Triple("-1", Tone.NEUTRAL, true) -> SemanticColor(textPrimary = Color(0xFFF2F2F2), textSecondary = Color(0xFFABABAC), surface = Color(0xFF040608), surfaceRaised = Color(0xFF1E1F23), separator = Color(0xFF3C3F46), accent = Color(0xFF6B8FD4), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
127
+ Triple("0", Tone.SEASON, false) -> SemanticColor(textPrimary = Color(0xFF422006), textSecondary = Color(0xFF785E47), surface = Color(0xFFF5F0E0), surfaceRaised = Color(0xFFFEFCE8), separator = Color(0xFFE2D6A4), accent = Color(0xFFCA8A04), danger = Color(0xFFB91C1C), onAccent = Color(0xFF000000))
128
+ Triple("0", Tone.SEASON, true) -> SemanticColor(textPrimary = Color(0xFFFDE047), textSecondary = Color(0xFFB6A136), surface = Color(0xFF0F0F0F), surfaceRaised = Color(0xFF1C1917), separator = Color(0xFF44403C), accent = Color(0xFFFACC15), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
129
+ Triple("0", Tone.NEUTRAL, false) -> SemanticColor(textPrimary = Color(0xFF111111), textSecondary = Color(0xFF545353), surface = Color(0xFFF0EEED), surfaceRaised = Color(0xFFFDFBF7), separator = Color(0xFFD0CBC5), accent = Color(0xFFCA8A04), danger = Color(0xFFB91C1C), onAccent = Color(0xFF000000))
130
+ Triple("0", Tone.NEUTRAL, true) -> SemanticColor(textPrimary = Color(0xFFF2F2F2), textSecondary = Color(0xFFACACAA), surface = Color(0xFF0A0801), surfaceRaised = Color(0xFF23211E), separator = Color(0xFF484437), accent = Color(0xFFFACC15), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
131
+ Triple("1", Tone.SEASON, false) -> SemanticColor(textPrimary = Color(0xFF06423E), textSecondary = Color(0xFF437371), surface = Color(0xFFE0F0F5), surfaceRaised = Color(0xFFE8F5FE), separator = Color(0xFFA4CFE2), accent = Color(0xFF04A7CA), danger = Color(0xFFB91C1C), onAccent = Color(0xFF000000))
132
+ Triple("1", Tone.SEASON, true) -> SemanticColor(textPrimary = Color(0xFF47BFFD), textSecondary = Color(0xFF378EBA), surface = Color(0xFF0F0F0F), surfaceRaised = Color(0xFF171C1C), separator = Color(0xFF3C4444), accent = Color(0xFF15B5FA), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
133
+ Triple("1", Tone.NEUTRAL, false) -> SemanticColor(textPrimary = Color(0xFF111111), textSecondary = Color(0xFF525455), surface = Color(0xFFE8EFF5), surfaceRaised = Color(0xFFF7FCFD), separator = Color(0xFFC1CED5), accent = Color(0xFF04A7CA), danger = Color(0xFFB91C1C), onAccent = Color(0xFF000000))
134
+ Triple("1", Tone.NEUTRAL, true) -> SemanticColor(textPrimary = Color(0xFFF2F2F2), textSecondary = Color(0xFFAAABAC), surface = Color(0xFF01070A), surfaceRaised = Color(0xFF1C2125), separator = Color(0xFF354249), accent = Color(0xFF15B5FA), danger = Color(0xFFF87171), onAccent = Color(0xFF000000))
135
+ else -> semanticColorFor("0", tone, isDark)
136
+ }
@@ -0,0 +1,120 @@
1
+ // AUTO-GENERATED — do not edit manually
2
+ // Run: bun run generate:tokens (from ui/)
3
+ // Source: ui/src/tokens/tokens.ts and each ui/src/seasons/<season>/style.css
4
+ import SwiftUI
5
+
6
+ enum Space {
7
+ static let xxxs: CGFloat = 2
8
+ static let xxs: CGFloat = 4
9
+ static let xs: CGFloat = 8
10
+ static let sm: CGFloat = 12
11
+ static let md: CGFloat = 16
12
+ static let lg: CGFloat = 24
13
+ static let xl: CGFloat = 32
14
+ static let xxl: CGFloat = 48
15
+ static let xxxl: CGFloat = 64
16
+ }
17
+
18
+ enum Radius {
19
+ static let sm: CGFloat = 4
20
+ static let md: CGFloat = 8
21
+ static let lg: CGFloat = 16
22
+ static let sheet: CGFloat = 28
23
+ }
24
+
25
+ enum Motion {
26
+ static let fast: Double = 0.15
27
+ static let base: Double = 0.25
28
+ static let slow: Double = 0.4
29
+
30
+ /// The one standard easing. Reduced motion gets no animation at all: the change lands instantly.
31
+ static func animation(_ duration: Double, reduceMotion: Bool) -> Animation? {
32
+ reduceMotion ? nil : .timingCurve(0.2, 0, 0, 1, duration: duration)
33
+ }
34
+ }
35
+
36
+ /// A type role: one Livvic face at a fixed size that scales with its Dynamic Type text style.
37
+ struct TypeRole: Sendable {
38
+ /// The face's PostScript name. A static family is picked by face, not by `.weight()` on the family.
39
+ var face: String
40
+ var size: CGFloat
41
+ var lineHeight: CGFloat
42
+ var tracking: CGFloat
43
+ var textStyle: Font.TextStyle
44
+ var tabular: Bool
45
+
46
+ var font: Font {
47
+ let font = Font.custom(face, size: size, relativeTo: textStyle)
48
+ return tabular ? font.monospacedDigit() : font
49
+ }
50
+
51
+ static let display = TypeRole(face: "Livvic-Black", size: 40, lineHeight: 1.1, tracking: -0.02, textStyle: .largeTitle, tabular: false)
52
+ static let title = TypeRole(face: "Livvic-Bold", size: 28, lineHeight: 1.15, tracking: -0.01, textStyle: .title, tabular: false)
53
+ static let headline = TypeRole(face: "Livvic-SemiBold", size: 20, lineHeight: 1.25, tracking: 0, textStyle: .title3, tabular: false)
54
+ static let body = TypeRole(face: "Livvic-Regular", size: 16, lineHeight: 1.5, tracking: 0, textStyle: .body, tabular: false)
55
+ static let label = TypeRole(face: "Livvic-Medium", size: 14, lineHeight: 1.3, tracking: 0.01, textStyle: .subheadline, tabular: false)
56
+ static let caption = TypeRole(face: "Livvic-Regular", size: 12, lineHeight: 1.35, tracking: 0.01, textStyle: .caption, tabular: false)
57
+ static let numeral = TypeRole(face: "Livvic-Medium", size: 16, lineHeight: 1.3, tracking: 0, textStyle: .body, tabular: true)
58
+ }
59
+
60
+ extension View {
61
+ func typeRole(_ role: TypeRole) -> some View {
62
+ font(role.font)
63
+ .tracking(role.size * role.tracking)
64
+ .lineSpacing(role.size * (role.lineHeight - 1))
65
+ }
66
+ }
67
+
68
+ nonisolated enum Tone: Sendable {
69
+ case season
70
+ case neutral
71
+ }
72
+
73
+ struct SemanticColor: Sendable {
74
+ var textPrimary: Color
75
+ var textSecondary: Color
76
+ var surface: Color
77
+ var surfaceRaised: Color
78
+ var separator: Color
79
+ var accent: Color
80
+ var danger: Color
81
+ var onAccent: Color
82
+ }
83
+
84
+ func semanticColorFor(_ shortName: String, tone: Tone, isDark: Bool = false) -> SemanticColor {
85
+ switch (shortName, tone, isDark) {
86
+ case ("24", .season, false): return SemanticColor(textPrimary: Color(hex: 0x5B21B6), textSecondary: Color(hex: 0x7E51C4), surface: Color(hex: 0xEDEAEF), surfaceRaised: Color(hex: 0xF9F5FF), separator: Color(hex: 0xDDD0F5), accent: Color(hex: 0x7C3AED), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0xFFFFFF))
87
+ case ("24", .season, true): return SemanticColor(textPrimary: Color(hex: 0xD0CCD6), textSecondary: Color(hex: 0x9B97A1), surface: Color(hex: 0x1E1A24), surfaceRaised: Color(hex: 0x130A24), separator: Color(hex: 0x2E1A52), accent: Color(hex: 0xA78BFA), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
88
+ case ("24", .neutral, false): return SemanticColor(textPrimary: Color(hex: 0x111111), textSecondary: Color(hex: 0x535256), surface: Color(hex: 0xEDEBF7), surfaceRaised: Color(hex: 0xFBF9FE), separator: Color(hex: 0xCAC5D8), accent: Color(hex: 0x7C3AED), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0xFFFFFF))
89
+ case ("24", .neutral, true): return SemanticColor(textPrimary: Color(hex: 0xF2F2F2), textSecondary: Color(hex: 0xABABAC), surface: Color(hex: 0x07060A), surfaceRaised: Color(hex: 0x201F25), separator: Color(hex: 0x413F49), accent: Color(hex: 0xA78BFA), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
90
+ case ("25", .season, false): return SemanticColor(textPrimary: Color(hex: 0x9A3412), textSecondary: Color(hex: 0xA85134), surface: Color(hex: 0xF0EAE7), surfaceRaised: Color(hex: 0xFFF8F5), separator: Color(hex: 0xF5D5C8), accent: Color(hex: 0xEC8266), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0x000000))
91
+ case ("25", .season, true): return SemanticColor(textPrimary: Color(hex: 0xD4CDC8), textSecondary: Color(hex: 0x9F9894), surface: Color(hex: 0x231C19), surfaceRaised: Color(hex: 0x1A0F0A), separator: Color(hex: 0x3D2318), accent: Color(hex: 0xF0A08A), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
92
+ case ("25", .neutral, false): return SemanticColor(textPrimary: Color(hex: 0x111111), textSecondary: Color(hex: 0x555354), surface: Color(hex: 0xF2EEF1), surfaceRaised: Color(hex: 0xFEFBFA), separator: Color(hex: 0xD3CBCD), accent: Color(hex: 0xEC8266), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0x000000))
93
+ case ("25", .neutral, true): return SemanticColor(textPrimary: Color(hex: 0xF2F2F2), textSecondary: Color(hex: 0xACABAB), surface: Color(hex: 0x0A0606), surfaceRaised: Color(hex: 0x222021), separator: Color(hex: 0x474040), accent: Color(hex: 0xF0A08A), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
94
+ case ("26", .season, false): return SemanticColor(textPrimary: Color(hex: 0x152038), textSecondary: Color(hex: 0x545C6E), surface: Color(hex: 0xE7E9EC), surfaceRaised: Color(hex: 0xF5F7FA), separator: Color(hex: 0xC8CEDA), accent: Color(hex: 0x152038), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0xFFFFFF))
95
+ case ("26", .season, true): return SemanticColor(textPrimary: Color(hex: 0xCACDD4), textSecondary: Color(hex: 0x94979F), surface: Color(hex: 0x171A22), surfaceRaised: Color(hex: 0x0A0E18), separator: Color(hex: 0x1E2D48), accent: Color(hex: 0x6B8FD4), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
96
+ case ("26", .neutral, false): return SemanticColor(textPrimary: Color(hex: 0x111111), textSecondary: Color(hex: 0x525254), surface: Color(hex: 0xE9EAEF), surfaceRaised: Color(hex: 0xF8F8F9), separator: Color(hex: 0xC2C3C9), accent: Color(hex: 0x152038), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0xFFFFFF))
97
+ case ("26", .neutral, true): return SemanticColor(textPrimary: Color(hex: 0xF2F2F2), textSecondary: Color(hex: 0xABABAC), surface: Color(hex: 0x040608), surfaceRaised: Color(hex: 0x1E1F23), separator: Color(hex: 0x3C3F46), accent: Color(hex: 0x6B8FD4), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
98
+ case ("wc26", .season, false): return SemanticColor(textPrimary: Color(hex: 0x152038), textSecondary: Color(hex: 0x545C6E), surface: Color(hex: 0xE7E9EC), surfaceRaised: Color(hex: 0xF5F7FA), separator: Color(hex: 0xC8CEDA), accent: Color(hex: 0x152038), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0xFFFFFF))
99
+ case ("wc26", .season, true): return SemanticColor(textPrimary: Color(hex: 0xCACDD4), textSecondary: Color(hex: 0x94979F), surface: Color(hex: 0x171A22), surfaceRaised: Color(hex: 0x0A0E18), separator: Color(hex: 0x1E2D48), accent: Color(hex: 0x6B8FD4), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
100
+ case ("wc26", .neutral, false): return SemanticColor(textPrimary: Color(hex: 0x111111), textSecondary: Color(hex: 0x525254), surface: Color(hex: 0xE9EAEF), surfaceRaised: Color(hex: 0xF8F8F9), separator: Color(hex: 0xC2C3C9), accent: Color(hex: 0x152038), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0xFFFFFF))
101
+ case ("wc26", .neutral, true): return SemanticColor(textPrimary: Color(hex: 0xF2F2F2), textSecondary: Color(hex: 0xABABAC), surface: Color(hex: 0x040608), surfaceRaised: Color(hex: 0x1E1F23), separator: Color(hex: 0x3C3F46), accent: Color(hex: 0x6B8FD4), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
102
+ case ("27", .season, false): return SemanticColor(textPrimary: Color(hex: 0x422006), textSecondary: Color(hex: 0x785E47), surface: Color(hex: 0xF5F0E0), surfaceRaised: Color(hex: 0xFEFCE8), separator: Color(hex: 0xE2D6A4), accent: Color(hex: 0xCA8A04), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0x000000))
103
+ case ("27", .season, true): return SemanticColor(textPrimary: Color(hex: 0xFDE047), textSecondary: Color(hex: 0xB6A136), surface: Color(hex: 0x0F0F0F), surfaceRaised: Color(hex: 0x1C1917), separator: Color(hex: 0x44403C), accent: Color(hex: 0xFACC15), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
104
+ case ("27", .neutral, false): return SemanticColor(textPrimary: Color(hex: 0x111111), textSecondary: Color(hex: 0x545353), surface: Color(hex: 0xF0EEED), surfaceRaised: Color(hex: 0xFDFBF7), separator: Color(hex: 0xD0CBC5), accent: Color(hex: 0xCA8A04), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0x000000))
105
+ case ("27", .neutral, true): return SemanticColor(textPrimary: Color(hex: 0xF2F2F2), textSecondary: Color(hex: 0xACACAA), surface: Color(hex: 0x0A0801), surfaceRaised: Color(hex: 0x23211E), separator: Color(hex: 0x484437), accent: Color(hex: 0xFACC15), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
106
+ case ("-1", .season, false): return SemanticColor(textPrimary: Color(hex: 0x152038), textSecondary: Color(hex: 0x545C6E), surface: Color(hex: 0xE7E9EC), surfaceRaised: Color(hex: 0xF5F7FA), separator: Color(hex: 0xC8CEDA), accent: Color(hex: 0x152038), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0xFFFFFF))
107
+ case ("-1", .season, true): return SemanticColor(textPrimary: Color(hex: 0xCACDD4), textSecondary: Color(hex: 0x94979F), surface: Color(hex: 0x171A22), surfaceRaised: Color(hex: 0x0A0E18), separator: Color(hex: 0x1E2D48), accent: Color(hex: 0x6B8FD4), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
108
+ case ("-1", .neutral, false): return SemanticColor(textPrimary: Color(hex: 0x111111), textSecondary: Color(hex: 0x525254), surface: Color(hex: 0xE9EAEF), surfaceRaised: Color(hex: 0xF8F8F9), separator: Color(hex: 0xC2C3C9), accent: Color(hex: 0x152038), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0xFFFFFF))
109
+ case ("-1", .neutral, true): return SemanticColor(textPrimary: Color(hex: 0xF2F2F2), textSecondary: Color(hex: 0xABABAC), surface: Color(hex: 0x040608), surfaceRaised: Color(hex: 0x1E1F23), separator: Color(hex: 0x3C3F46), accent: Color(hex: 0x6B8FD4), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
110
+ case ("0", .season, false): return SemanticColor(textPrimary: Color(hex: 0x422006), textSecondary: Color(hex: 0x785E47), surface: Color(hex: 0xF5F0E0), surfaceRaised: Color(hex: 0xFEFCE8), separator: Color(hex: 0xE2D6A4), accent: Color(hex: 0xCA8A04), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0x000000))
111
+ case ("0", .season, true): return SemanticColor(textPrimary: Color(hex: 0xFDE047), textSecondary: Color(hex: 0xB6A136), surface: Color(hex: 0x0F0F0F), surfaceRaised: Color(hex: 0x1C1917), separator: Color(hex: 0x44403C), accent: Color(hex: 0xFACC15), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
112
+ case ("0", .neutral, false): return SemanticColor(textPrimary: Color(hex: 0x111111), textSecondary: Color(hex: 0x545353), surface: Color(hex: 0xF0EEED), surfaceRaised: Color(hex: 0xFDFBF7), separator: Color(hex: 0xD0CBC5), accent: Color(hex: 0xCA8A04), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0x000000))
113
+ case ("0", .neutral, true): return SemanticColor(textPrimary: Color(hex: 0xF2F2F2), textSecondary: Color(hex: 0xACACAA), surface: Color(hex: 0x0A0801), surfaceRaised: Color(hex: 0x23211E), separator: Color(hex: 0x484437), accent: Color(hex: 0xFACC15), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
114
+ case ("1", .season, false): return SemanticColor(textPrimary: Color(hex: 0x06423E), textSecondary: Color(hex: 0x437371), surface: Color(hex: 0xE0F0F5), surfaceRaised: Color(hex: 0xE8F5FE), separator: Color(hex: 0xA4CFE2), accent: Color(hex: 0x04A7CA), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0x000000))
115
+ case ("1", .season, true): return SemanticColor(textPrimary: Color(hex: 0x47BFFD), textSecondary: Color(hex: 0x378EBA), surface: Color(hex: 0x0F0F0F), surfaceRaised: Color(hex: 0x171C1C), separator: Color(hex: 0x3C4444), accent: Color(hex: 0x15B5FA), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
116
+ case ("1", .neutral, false): return SemanticColor(textPrimary: Color(hex: 0x111111), textSecondary: Color(hex: 0x525455), surface: Color(hex: 0xE8EFF5), surfaceRaised: Color(hex: 0xF7FCFD), separator: Color(hex: 0xC1CED5), accent: Color(hex: 0x04A7CA), danger: Color(hex: 0xB91C1C), onAccent: Color(hex: 0x000000))
117
+ case ("1", .neutral, true): return SemanticColor(textPrimary: Color(hex: 0xF2F2F2), textSecondary: Color(hex: 0xAAABAC), surface: Color(hex: 0x01070A), surfaceRaised: Color(hex: 0x1C2125), separator: Color(hex: 0x354249), accent: Color(hex: 0x15B5FA), danger: Color(hex: 0xF87171), onAccent: Color(hex: 0x000000))
118
+ default: return semanticColorFor("0", tone: tone, isDark: isDark)
119
+ }
120
+ }
@@ -0,0 +1,383 @@
1
+ /* AUTO-GENERATED — do not edit manually */
2
+ /* Run: bun run generate:tokens (from ui/) */
3
+ /* Source: ui/src/tokens/tokens.ts and each ui/src/seasons/<season>/style.css */
4
+
5
+ :root {
6
+ --space-3xs: 2px;
7
+ --space-2xs: 4px;
8
+ --space-xs: 8px;
9
+ --space-sm: 12px;
10
+ --space-md: 16px;
11
+ --space-lg: 24px;
12
+ --space-xl: 32px;
13
+ --space-2xl: 48px;
14
+ --space-3xl: 64px;
15
+ --radius-sm: 4px;
16
+ --radius-md: 8px;
17
+ --radius-lg: 16px;
18
+ --radius-sheet: 28px;
19
+ --motion-fast: 150ms;
20
+ --motion-base: 250ms;
21
+ --motion-slow: 400ms;
22
+ --ease-standard: cubic-bezier(0.2, 0, 0, 1);
23
+ --font-family-base: 'Livvic', system-ui, sans-serif;
24
+ --type-display-size: clamp(2rem, 5vw, 3.5rem);
25
+ --type-display-weight: 900;
26
+ --type-display-line-height: 1.1;
27
+ --type-display-tracking: -0.02em;
28
+ --type-title-size: clamp(1.5rem, 3.5vw, 2.25rem);
29
+ --type-title-weight: 700;
30
+ --type-title-line-height: 1.15;
31
+ --type-title-tracking: -0.01em;
32
+ --type-headline-size: clamp(1.25rem, 2.5vw, 1.75rem);
33
+ --type-headline-weight: 600;
34
+ --type-headline-line-height: 1.25;
35
+ --type-headline-tracking: 0em;
36
+ --type-body-size: 1rem;
37
+ --type-body-weight: 400;
38
+ --type-body-line-height: 1.5;
39
+ --type-body-tracking: 0em;
40
+ --type-label-size: 0.875rem;
41
+ --type-label-weight: 500;
42
+ --type-label-line-height: 1.3;
43
+ --type-label-tracking: 0.01em;
44
+ --type-caption-size: 0.75rem;
45
+ --type-caption-weight: 400;
46
+ --type-caption-line-height: 1.35;
47
+ --type-caption-tracking: 0.01em;
48
+ --type-numeral-size: 1rem;
49
+ --type-numeral-weight: 500;
50
+ --type-numeral-line-height: 1.3;
51
+ --type-numeral-tracking: 0em;
52
+ --ui-text-primary: var(--ui-neutral-text-primary);
53
+ --ui-text-secondary: var(--ui-neutral-text-secondary);
54
+ --ui-surface: var(--ui-neutral-surface);
55
+ --ui-surface-raised: var(--ui-neutral-surface-raised);
56
+ --ui-separator: var(--ui-neutral-separator);
57
+ --ui-accent: var(--ui-neutral-accent);
58
+ --ui-danger: var(--ui-neutral-danger);
59
+ --ui-on-accent: var(--ui-neutral-on-accent);
60
+ }
61
+
62
+ [data-tone='season'] {
63
+ --ui-text-primary: var(--ui-season-text-primary);
64
+ --ui-text-secondary: var(--ui-season-text-secondary);
65
+ --ui-surface: var(--ui-season-surface);
66
+ --ui-surface-raised: var(--ui-season-surface-raised);
67
+ --ui-separator: var(--ui-season-separator);
68
+ --ui-accent: var(--ui-season-accent);
69
+ --ui-danger: var(--ui-season-danger);
70
+ --ui-on-accent: var(--ui-season-on-accent);
71
+ }
72
+
73
+ @media (prefers-reduced-motion: reduce) {
74
+ :root {
75
+ --motion-fast: 0ms;
76
+ --motion-base: 0ms;
77
+ --motion-slow: 0ms;
78
+ }
79
+ }
80
+
81
+ :root[data-season='24'] {
82
+ --ui-season-text-primary: #5b21b6;
83
+ --ui-season-text-secondary: #7e51c4;
84
+ --ui-season-surface: #edeaef;
85
+ --ui-season-surface-raised: #f9f5ff;
86
+ --ui-season-separator: #ddd0f5;
87
+ --ui-season-accent: #7c3aed;
88
+ --ui-season-danger: #b91c1c;
89
+ --ui-season-on-accent: #ffffff;
90
+ --ui-neutral-text-primary: #111111;
91
+ --ui-neutral-text-secondary: #535256;
92
+ --ui-neutral-surface: #edebf7;
93
+ --ui-neutral-surface-raised: #fbf9fe;
94
+ --ui-neutral-separator: #cac5d8;
95
+ --ui-neutral-accent: #7c3aed;
96
+ --ui-neutral-danger: #b91c1c;
97
+ --ui-neutral-on-accent: #ffffff;
98
+ }
99
+
100
+ :root[data-season='24'][data-theme='dark'] {
101
+ --ui-season-text-primary: #d0ccd6;
102
+ --ui-season-text-secondary: #9b97a1;
103
+ --ui-season-surface: #1e1a24;
104
+ --ui-season-surface-raised: #130a24;
105
+ --ui-season-separator: #2e1a52;
106
+ --ui-season-accent: #a78bfa;
107
+ --ui-season-danger: #f87171;
108
+ --ui-season-on-accent: #000000;
109
+ --ui-neutral-text-primary: #f2f2f2;
110
+ --ui-neutral-text-secondary: #ababac;
111
+ --ui-neutral-surface: #07060a;
112
+ --ui-neutral-surface-raised: #201f25;
113
+ --ui-neutral-separator: #413f49;
114
+ --ui-neutral-accent: #a78bfa;
115
+ --ui-neutral-danger: #f87171;
116
+ --ui-neutral-on-accent: #000000;
117
+ }
118
+
119
+ :root[data-season='25'] {
120
+ --ui-season-text-primary: #9a3412;
121
+ --ui-season-text-secondary: #a85134;
122
+ --ui-season-surface: #f0eae7;
123
+ --ui-season-surface-raised: #fff8f5;
124
+ --ui-season-separator: #f5d5c8;
125
+ --ui-season-accent: #ec8266;
126
+ --ui-season-danger: #b91c1c;
127
+ --ui-season-on-accent: #000000;
128
+ --ui-neutral-text-primary: #111111;
129
+ --ui-neutral-text-secondary: #555354;
130
+ --ui-neutral-surface: #f2eef1;
131
+ --ui-neutral-surface-raised: #fefbfa;
132
+ --ui-neutral-separator: #d3cbcd;
133
+ --ui-neutral-accent: #ec8266;
134
+ --ui-neutral-danger: #b91c1c;
135
+ --ui-neutral-on-accent: #000000;
136
+ }
137
+
138
+ :root[data-season='25'][data-theme='dark'] {
139
+ --ui-season-text-primary: #d4cdc8;
140
+ --ui-season-text-secondary: #9f9894;
141
+ --ui-season-surface: #231c19;
142
+ --ui-season-surface-raised: #1a0f0a;
143
+ --ui-season-separator: #3d2318;
144
+ --ui-season-accent: #f0a08a;
145
+ --ui-season-danger: #f87171;
146
+ --ui-season-on-accent: #000000;
147
+ --ui-neutral-text-primary: #f2f2f2;
148
+ --ui-neutral-text-secondary: #acabab;
149
+ --ui-neutral-surface: #0a0606;
150
+ --ui-neutral-surface-raised: #222021;
151
+ --ui-neutral-separator: #474040;
152
+ --ui-neutral-accent: #f0a08a;
153
+ --ui-neutral-danger: #f87171;
154
+ --ui-neutral-on-accent: #000000;
155
+ }
156
+
157
+ :root[data-season='26'] {
158
+ --ui-season-text-primary: #152038;
159
+ --ui-season-text-secondary: #545c6e;
160
+ --ui-season-surface: #e7e9ec;
161
+ --ui-season-surface-raised: #f5f7fa;
162
+ --ui-season-separator: #c8ceda;
163
+ --ui-season-accent: #152038;
164
+ --ui-season-danger: #b91c1c;
165
+ --ui-season-on-accent: #ffffff;
166
+ --ui-neutral-text-primary: #111111;
167
+ --ui-neutral-text-secondary: #525254;
168
+ --ui-neutral-surface: #e9eaef;
169
+ --ui-neutral-surface-raised: #f8f8f9;
170
+ --ui-neutral-separator: #c2c3c9;
171
+ --ui-neutral-accent: #152038;
172
+ --ui-neutral-danger: #b91c1c;
173
+ --ui-neutral-on-accent: #ffffff;
174
+ }
175
+
176
+ :root[data-season='26'][data-theme='dark'] {
177
+ --ui-season-text-primary: #cacdd4;
178
+ --ui-season-text-secondary: #94979f;
179
+ --ui-season-surface: #171a22;
180
+ --ui-season-surface-raised: #0a0e18;
181
+ --ui-season-separator: #1e2d48;
182
+ --ui-season-accent: #6b8fd4;
183
+ --ui-season-danger: #f87171;
184
+ --ui-season-on-accent: #000000;
185
+ --ui-neutral-text-primary: #f2f2f2;
186
+ --ui-neutral-text-secondary: #ababac;
187
+ --ui-neutral-surface: #040608;
188
+ --ui-neutral-surface-raised: #1e1f23;
189
+ --ui-neutral-separator: #3c3f46;
190
+ --ui-neutral-accent: #6b8fd4;
191
+ --ui-neutral-danger: #f87171;
192
+ --ui-neutral-on-accent: #000000;
193
+ }
194
+
195
+ :root[data-season='wc26'] {
196
+ --ui-season-text-primary: #152038;
197
+ --ui-season-text-secondary: #545c6e;
198
+ --ui-season-surface: #e7e9ec;
199
+ --ui-season-surface-raised: #f5f7fa;
200
+ --ui-season-separator: #c8ceda;
201
+ --ui-season-accent: #152038;
202
+ --ui-season-danger: #b91c1c;
203
+ --ui-season-on-accent: #ffffff;
204
+ --ui-neutral-text-primary: #111111;
205
+ --ui-neutral-text-secondary: #525254;
206
+ --ui-neutral-surface: #e9eaef;
207
+ --ui-neutral-surface-raised: #f8f8f9;
208
+ --ui-neutral-separator: #c2c3c9;
209
+ --ui-neutral-accent: #152038;
210
+ --ui-neutral-danger: #b91c1c;
211
+ --ui-neutral-on-accent: #ffffff;
212
+ }
213
+
214
+ :root[data-season='wc26'][data-theme='dark'] {
215
+ --ui-season-text-primary: #cacdd4;
216
+ --ui-season-text-secondary: #94979f;
217
+ --ui-season-surface: #171a22;
218
+ --ui-season-surface-raised: #0a0e18;
219
+ --ui-season-separator: #1e2d48;
220
+ --ui-season-accent: #6b8fd4;
221
+ --ui-season-danger: #f87171;
222
+ --ui-season-on-accent: #000000;
223
+ --ui-neutral-text-primary: #f2f2f2;
224
+ --ui-neutral-text-secondary: #ababac;
225
+ --ui-neutral-surface: #040608;
226
+ --ui-neutral-surface-raised: #1e1f23;
227
+ --ui-neutral-separator: #3c3f46;
228
+ --ui-neutral-accent: #6b8fd4;
229
+ --ui-neutral-danger: #f87171;
230
+ --ui-neutral-on-accent: #000000;
231
+ }
232
+
233
+ :root[data-season='27'] {
234
+ --ui-season-text-primary: #422006;
235
+ --ui-season-text-secondary: #785e47;
236
+ --ui-season-surface: #f5f0e0;
237
+ --ui-season-surface-raised: #fefce8;
238
+ --ui-season-separator: #e2d6a4;
239
+ --ui-season-accent: #ca8a04;
240
+ --ui-season-danger: #b91c1c;
241
+ --ui-season-on-accent: #000000;
242
+ --ui-neutral-text-primary: #111111;
243
+ --ui-neutral-text-secondary: #545353;
244
+ --ui-neutral-surface: #f0eeed;
245
+ --ui-neutral-surface-raised: #fdfbf7;
246
+ --ui-neutral-separator: #d0cbc5;
247
+ --ui-neutral-accent: #ca8a04;
248
+ --ui-neutral-danger: #b91c1c;
249
+ --ui-neutral-on-accent: #000000;
250
+ }
251
+
252
+ :root[data-season='27'][data-theme='dark'] {
253
+ --ui-season-text-primary: #fde047;
254
+ --ui-season-text-secondary: #b6a136;
255
+ --ui-season-surface: #0f0f0f;
256
+ --ui-season-surface-raised: #1c1917;
257
+ --ui-season-separator: #44403c;
258
+ --ui-season-accent: #facc15;
259
+ --ui-season-danger: #f87171;
260
+ --ui-season-on-accent: #000000;
261
+ --ui-neutral-text-primary: #f2f2f2;
262
+ --ui-neutral-text-secondary: #acacaa;
263
+ --ui-neutral-surface: #0a0801;
264
+ --ui-neutral-surface-raised: #23211e;
265
+ --ui-neutral-separator: #484437;
266
+ --ui-neutral-accent: #facc15;
267
+ --ui-neutral-danger: #f87171;
268
+ --ui-neutral-on-accent: #000000;
269
+ }
270
+
271
+ :root[data-season='-1'] {
272
+ --ui-season-text-primary: #152038;
273
+ --ui-season-text-secondary: #545c6e;
274
+ --ui-season-surface: #e7e9ec;
275
+ --ui-season-surface-raised: #f5f7fa;
276
+ --ui-season-separator: #c8ceda;
277
+ --ui-season-accent: #152038;
278
+ --ui-season-danger: #b91c1c;
279
+ --ui-season-on-accent: #ffffff;
280
+ --ui-neutral-text-primary: #111111;
281
+ --ui-neutral-text-secondary: #525254;
282
+ --ui-neutral-surface: #e9eaef;
283
+ --ui-neutral-surface-raised: #f8f8f9;
284
+ --ui-neutral-separator: #c2c3c9;
285
+ --ui-neutral-accent: #152038;
286
+ --ui-neutral-danger: #b91c1c;
287
+ --ui-neutral-on-accent: #ffffff;
288
+ }
289
+
290
+ :root[data-season='-1'][data-theme='dark'] {
291
+ --ui-season-text-primary: #cacdd4;
292
+ --ui-season-text-secondary: #94979f;
293
+ --ui-season-surface: #171a22;
294
+ --ui-season-surface-raised: #0a0e18;
295
+ --ui-season-separator: #1e2d48;
296
+ --ui-season-accent: #6b8fd4;
297
+ --ui-season-danger: #f87171;
298
+ --ui-season-on-accent: #000000;
299
+ --ui-neutral-text-primary: #f2f2f2;
300
+ --ui-neutral-text-secondary: #ababac;
301
+ --ui-neutral-surface: #040608;
302
+ --ui-neutral-surface-raised: #1e1f23;
303
+ --ui-neutral-separator: #3c3f46;
304
+ --ui-neutral-accent: #6b8fd4;
305
+ --ui-neutral-danger: #f87171;
306
+ --ui-neutral-on-accent: #000000;
307
+ }
308
+
309
+ :root[data-season='0'] {
310
+ --ui-season-text-primary: #422006;
311
+ --ui-season-text-secondary: #785e47;
312
+ --ui-season-surface: #f5f0e0;
313
+ --ui-season-surface-raised: #fefce8;
314
+ --ui-season-separator: #e2d6a4;
315
+ --ui-season-accent: #ca8a04;
316
+ --ui-season-danger: #b91c1c;
317
+ --ui-season-on-accent: #000000;
318
+ --ui-neutral-text-primary: #111111;
319
+ --ui-neutral-text-secondary: #545353;
320
+ --ui-neutral-surface: #f0eeed;
321
+ --ui-neutral-surface-raised: #fdfbf7;
322
+ --ui-neutral-separator: #d0cbc5;
323
+ --ui-neutral-accent: #ca8a04;
324
+ --ui-neutral-danger: #b91c1c;
325
+ --ui-neutral-on-accent: #000000;
326
+ }
327
+
328
+ :root[data-season='0'][data-theme='dark'] {
329
+ --ui-season-text-primary: #fde047;
330
+ --ui-season-text-secondary: #b6a136;
331
+ --ui-season-surface: #0f0f0f;
332
+ --ui-season-surface-raised: #1c1917;
333
+ --ui-season-separator: #44403c;
334
+ --ui-season-accent: #facc15;
335
+ --ui-season-danger: #f87171;
336
+ --ui-season-on-accent: #000000;
337
+ --ui-neutral-text-primary: #f2f2f2;
338
+ --ui-neutral-text-secondary: #acacaa;
339
+ --ui-neutral-surface: #0a0801;
340
+ --ui-neutral-surface-raised: #23211e;
341
+ --ui-neutral-separator: #484437;
342
+ --ui-neutral-accent: #facc15;
343
+ --ui-neutral-danger: #f87171;
344
+ --ui-neutral-on-accent: #000000;
345
+ }
346
+
347
+ :root[data-season='1'] {
348
+ --ui-season-text-primary: #06423e;
349
+ --ui-season-text-secondary: #437371;
350
+ --ui-season-surface: #e0f0f5;
351
+ --ui-season-surface-raised: #e8f5fe;
352
+ --ui-season-separator: #a4cfe2;
353
+ --ui-season-accent: #04a7ca;
354
+ --ui-season-danger: #b91c1c;
355
+ --ui-season-on-accent: #000000;
356
+ --ui-neutral-text-primary: #111111;
357
+ --ui-neutral-text-secondary: #525455;
358
+ --ui-neutral-surface: #e8eff5;
359
+ --ui-neutral-surface-raised: #f7fcfd;
360
+ --ui-neutral-separator: #c1ced5;
361
+ --ui-neutral-accent: #04a7ca;
362
+ --ui-neutral-danger: #b91c1c;
363
+ --ui-neutral-on-accent: #000000;
364
+ }
365
+
366
+ :root[data-season='1'][data-theme='dark'] {
367
+ --ui-season-text-primary: #47bffd;
368
+ --ui-season-text-secondary: #378eba;
369
+ --ui-season-surface: #0f0f0f;
370
+ --ui-season-surface-raised: #171c1c;
371
+ --ui-season-separator: #3c4444;
372
+ --ui-season-accent: #15b5fa;
373
+ --ui-season-danger: #f87171;
374
+ --ui-season-on-accent: #000000;
375
+ --ui-neutral-text-primary: #f2f2f2;
376
+ --ui-neutral-text-secondary: #aaabac;
377
+ --ui-neutral-surface: #01070a;
378
+ --ui-neutral-surface-raised: #1c2125;
379
+ --ui-neutral-separator: #354249;
380
+ --ui-neutral-accent: #15b5fa;
381
+ --ui-neutral-danger: #f87171;
382
+ --ui-neutral-on-accent: #000000;
383
+ }
@@ -0,0 +1,93 @@
1
+ // The one source for spacing, radius, type, semantic colour and motion. scripts/generate-tokens.ts
2
+ // turns it into CSS custom properties, Tokens.swift and Tokens.kt; season colours stay in
3
+ // seasons/*/style.css and are referenced here by variable name.
4
+
5
+ /** A 4pt scale. Native names spell the numeric prefixes out (`3xs` → `xxxs`) to stay valid identifiers. */
6
+ export const space = { '3xs': 2, '2xs': 4, xs: 8, sm: 12, md: 16, lg: 24, xl: 32, '2xl': 48, '3xl': 64 } as const
7
+
8
+ export const radius = { sm: 4, md: 8, lg: 16, sheet: 28 } as const
9
+
10
+ export const motion = {
11
+ duration: { fast: 150, base: 250, slow: 400 },
12
+ /** M3's standard easing; reduced motion swaps every transition for an instant or opacity-only change. */
13
+ easing: [0.2, 0, 0, 1],
14
+ } as const
15
+
16
+ /** `Livvic-SemiBold.ttf` → `livvic_semibold`: an Android resource name is lowercase, digits and underscores. */
17
+ export const androidFont = (file: string) => file.replace(/\.ttf$/, '').replace(/-/g, '_').toLowerCase()
18
+
19
+ /** Livvic ships as static weights (no variable file, no 800), one file per weight the roles use. */
20
+ export const typeface = {
21
+ family: 'Livvic',
22
+ dir: 'livvic',
23
+ files: { 400: 'Livvic-Regular.ttf', 500: 'Livvic-Medium.ttf', 600: 'Livvic-SemiBold.ttf', 700: 'Livvic-Bold.ttf', 900: 'Livvic-Black.ttf' },
24
+ } as const
25
+
26
+ export type IosTextStyle = 'largeTitle' | 'title' | 'title2' | 'title3' | 'headline' | 'body' | 'callout' | 'subheadline' | 'footnote' | 'caption' | 'caption2'
27
+ export type M3Slot = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall' | 'labelLarge' | 'labelMedium' | 'labelSmall'
28
+
29
+ export interface TypeRole {
30
+ /** pt / sp / px at the default text size; native scales it through `ios` and `m3`. */
31
+ size: number
32
+ weight: number
33
+ /** A multiple of size. */
34
+ lineHeight: number
35
+ /** em. */
36
+ tracking: number
37
+ tabular?: boolean
38
+ /** Web only: the responsive step `typography.css` already used, where the role had one. */
39
+ clamp?: string
40
+ ios: IosTextStyle
41
+ m3: M3Slot
42
+ }
43
+
44
+ export const typeRoles = {
45
+ display: { size: 40, weight: 900, lineHeight: 1.1, tracking: -0.02, clamp: 'clamp(2rem, 5vw, 3.5rem)', ios: 'largeTitle', m3: 'displaySmall' },
46
+ title: { size: 28, weight: 700, lineHeight: 1.15, tracking: -0.01, clamp: 'clamp(1.5rem, 3.5vw, 2.25rem)', ios: 'title', m3: 'headlineMedium' },
47
+ headline: { size: 20, weight: 600, lineHeight: 1.25, tracking: 0, clamp: 'clamp(1.25rem, 2.5vw, 1.75rem)', ios: 'title3', m3: 'titleLarge' },
48
+ body: { size: 16, weight: 400, lineHeight: 1.5, tracking: 0, ios: 'body', m3: 'bodyLarge' },
49
+ label: { size: 14, weight: 500, lineHeight: 1.3, tracking: 0.01, ios: 'subheadline', m3: 'labelLarge' },
50
+ caption: { size: 12, weight: 400, lineHeight: 1.35, tracking: 0.01, ios: 'caption', m3: 'bodySmall' },
51
+ numeral: { size: 16, weight: 500, lineHeight: 1.3, tracking: 0, tabular: true, ios: 'body', m3: 'titleMedium' },
52
+ } as const satisfies Record<string, TypeRole>
53
+
54
+ // The season variables a semantic role can stand on: the names each season's style.css declares.
55
+ export type SeasonVar = '--color-season' | '--color-season-text' | '--color-accent' | '--color-surface' | '--color-border'
56
+
57
+ export type Mode = 'light' | 'dark'
58
+
59
+ /**
60
+ * Two tones, one set of names. `season` is the season's own colour at full strength, for the
61
+ * season track (season, round, question). `neutral` is the platform's own light/dark greys
62
+ * carrying a faint wash of the season's accent, for every page outside it.
63
+ */
64
+ export const semantic = {
65
+ season: {
66
+ surface: '--color-season',
67
+ surfaceRaised: '--color-surface',
68
+ separator: '--color-border',
69
+ textPrimary: '--color-season-text',
70
+ accent: '--color-accent',
71
+ },
72
+ neutral: {
73
+ base: {
74
+ light: { surface: '#f2f2f7', surfaceRaised: '#ffffff', separator: '#d1d1d6', textPrimary: '#111111' },
75
+ dark: { surface: '#000000', surfaceRaised: '#1c1c1e', separator: '#38383a', textPrimary: '#f2f2f2' },
76
+ },
77
+ /** Share of the season accent mixed into each neutral base. */
78
+ tint: { surface: 0.04, surfaceRaised: 0.03, separator: 0.08, textPrimary: 0 },
79
+ },
80
+ /** text-secondary is text-primary mixed into the surface: this share at the faintest, more where a season needs it to clear 4.5:1. */
81
+ textSecondaryShare: 0.7,
82
+ /** Not a season colour: red-700 / red-400, the reds that clear 4.5:1 on every season surface. */
83
+ danger: { light: '#b91c1c', dark: '#f87171' },
84
+ } as const satisfies {
85
+ season: Record<'surface' | 'surfaceRaised' | 'separator' | 'textPrimary' | 'accent', SeasonVar>
86
+ neutral: { base: Record<Mode, Record<string, string>>; tint: Record<string, number> }
87
+ textSecondaryShare: number
88
+ danger: Record<Mode, string>
89
+ }
90
+
91
+ export const semanticRoles = ['textPrimary', 'textSecondary', 'surface', 'surfaceRaised', 'separator', 'accent', 'danger', 'onAccent'] as const
92
+ export type SemanticRole = (typeof semanticRoles)[number]
93
+ export type Tone = 'season' | 'neutral'