@pokela/tokens 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Veli-Matti Pokela
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @pokela/tokens
2
+
3
+ Design-tokenit: **Pokela Portfolio**, versio 1.1.0.
4
+
5
+ > Tämä paketti on generoitu tiedostosta `tokens.json`. Älä muokkaa sitä
6
+ > käsin — muutokset tehdään lähteeseen ja paketti rakennetaan uudelleen
7
+ > komennolla `npm run paketti`.
8
+
9
+ ## Käyttö
10
+
11
+ ```js
12
+ import tokens, { color, space } from '@pokela/tokens';
13
+ import '@pokela/tokens/tokens.css';
14
+
15
+ color.light.ink; // '#101112'
16
+ space['24']; // '24px'
17
+ ```
18
+
19
+ CSS-muuttujat tulevat `tokens.css`:stä, esimerkiksi `var(--ink)` ja
20
+ `var(--space-24)`. Teema vaihtuu attribuutilla `data-theme`, ja ilman
21
+ sitä se seuraa käyttöjärjestelmää.
22
+
23
+ ## Viennit
24
+
25
+ - `meta`
26
+ - `modes`
27
+ - `color`
28
+ - `font`
29
+ - `type`
30
+ - `typeScale`
31
+ - `lineHeightScale`
32
+ - `weight`
33
+ - `space`
34
+ - `layout`
35
+ - `border`
36
+ - `icon`
37
+ - `motion`
38
+
39
+ ## Lisenssi
40
+
41
+ MIT. Koskee tätä pakettia, ei sitä repoa josta se on generoitu.
42
+
43
+ ## Moodit
44
+
45
+ Moodi on ulottuvuus jolla tokenin arvo vaihtelee.
46
+
47
+ - `viewport` — base, sm, md, lg
48
+
49
+ Teema (vaalea/tumma) hoituu CSS:ssä `data-theme`-attribuutilla, joten
50
+ sitä ei tarvitse valita koodista.
package/index.d.ts ADDED
@@ -0,0 +1,327 @@
1
+ /* @pokela/tokens — generoitu tiedostosta tokens.json.
2
+ Älä muokkaa: aja `npm run paketti`. */
3
+
4
+ export declare const meta: {
5
+ readonly name: string;
6
+ readonly version: string;
7
+ readonly direction: string;
8
+ readonly defaultTheme: string;
9
+ readonly source: string;
10
+ };
11
+
12
+ /**
13
+ * Moodiakselit: millä ulottuvuuksilla tokenin arvo vaihtelee. minWidth
14
+ * kertoo myös suunnan — arvot kasvavat alhaalta ylös, eli CSS on
15
+ * mobile-first.
16
+ */
17
+ export declare const modes: {
18
+ readonly viewport: {
19
+ readonly values: readonly string[];
20
+ readonly minWidth: {
21
+ readonly base: string;
22
+ readonly sm: string;
23
+ readonly md: string;
24
+ readonly lg: string;
25
+ };
26
+ };
27
+ };
28
+
29
+ export declare const color: {
30
+ readonly light: {
31
+ readonly paper: string;
32
+ readonly "paper-alt": string;
33
+ readonly line: string;
34
+ readonly "line-strong": string;
35
+ readonly ink: string;
36
+ readonly "ink-muted": string;
37
+ readonly "ink-faint": string;
38
+ readonly "badge-surface": string;
39
+ readonly "invert-surface": string;
40
+ readonly "invert-ink": string;
41
+ readonly "invert-faint": string;
42
+ readonly "placeholder-base": string;
43
+ readonly "placeholder-stripe": string;
44
+ readonly "code-surface": string;
45
+ readonly "code-ink": string;
46
+ readonly "code-muted": string;
47
+ readonly "code-faint": string;
48
+ };
49
+ readonly dark: {
50
+ readonly paper: string;
51
+ readonly "paper-alt": string;
52
+ readonly line: string;
53
+ readonly "line-strong": string;
54
+ readonly ink: string;
55
+ readonly "ink-muted": string;
56
+ readonly "ink-faint": string;
57
+ readonly "badge-surface": string;
58
+ readonly "invert-surface": string;
59
+ readonly "invert-ink": string;
60
+ readonly "invert-faint": string;
61
+ readonly "placeholder-base": string;
62
+ readonly "placeholder-stripe": string;
63
+ readonly "code-surface": string;
64
+ readonly "code-ink": string;
65
+ readonly "code-muted": string;
66
+ readonly "code-faint": string;
67
+ };
68
+ };
69
+
70
+ /**
71
+ * Fallback on var():n sisällä — muuten puuttuva next/font-muuttuja tekisi
72
+ * koko määrittelystä epäkelvon.
73
+ */
74
+ export declare const font: {
75
+ readonly display: string;
76
+ readonly ui: string;
77
+ readonly code: string;
78
+ };
79
+
80
+ /**
81
+ * size/lineHeight ovat lg-arvoja (≥1200px). Pienemmät koot ovat kohdassa
82
+ * typeScale.
83
+ */
84
+ export declare const type: {
85
+ readonly "display-xl": {
86
+ readonly size: string;
87
+ readonly lineHeight: string;
88
+ readonly tracking: string;
89
+ readonly weight: number;
90
+ readonly font: string;
91
+ readonly note: string;
92
+ };
93
+ readonly "display-l": {
94
+ readonly size: string;
95
+ readonly lineHeight: string;
96
+ readonly tracking: string;
97
+ readonly weight: number;
98
+ readonly font: string;
99
+ readonly note: string;
100
+ };
101
+ readonly "display-m": {
102
+ readonly size: string;
103
+ readonly lineHeight: string;
104
+ readonly tracking: string;
105
+ readonly weight: number;
106
+ readonly font: string;
107
+ readonly note: string;
108
+ };
109
+ readonly "display-s": {
110
+ readonly size: string;
111
+ readonly lineHeight: string;
112
+ readonly tracking: string;
113
+ readonly weight: number;
114
+ readonly font: string;
115
+ readonly note: string;
116
+ };
117
+ readonly "body-l": {
118
+ readonly size: string;
119
+ readonly lineHeight: string;
120
+ readonly weight: number;
121
+ readonly font: string;
122
+ };
123
+ readonly body: {
124
+ readonly size: string;
125
+ readonly lineHeight: string;
126
+ readonly weight: number;
127
+ readonly font: string;
128
+ };
129
+ readonly "body-s": {
130
+ readonly size: string;
131
+ readonly lineHeight: string;
132
+ readonly weight: number;
133
+ readonly font: string;
134
+ };
135
+ readonly meta: {
136
+ readonly size: string;
137
+ readonly lineHeight: string;
138
+ readonly tracking: string;
139
+ readonly weight: number;
140
+ readonly transform: string;
141
+ readonly font: string;
142
+ };
143
+ readonly "meta-s": {
144
+ readonly size: string;
145
+ readonly lineHeight: string;
146
+ readonly tracking: string;
147
+ readonly weight: number;
148
+ readonly transform: string;
149
+ readonly font: string;
150
+ };
151
+ readonly code: {
152
+ readonly size: string;
153
+ readonly lineHeight: string;
154
+ readonly weight: number;
155
+ readonly font: string;
156
+ readonly note: string;
157
+ };
158
+ };
159
+
160
+ export declare const typeScale: {
161
+ readonly "display-xl": {
162
+ readonly base: string;
163
+ readonly sm: string;
164
+ readonly md: string;
165
+ readonly lg: string;
166
+ };
167
+ readonly "display-l": {
168
+ readonly base: string;
169
+ readonly sm: string;
170
+ readonly md: string;
171
+ readonly lg: string;
172
+ };
173
+ readonly "display-m": {
174
+ readonly base: string;
175
+ readonly sm: string;
176
+ readonly md: string;
177
+ readonly lg: string;
178
+ };
179
+ readonly "display-s": {
180
+ readonly base: string;
181
+ readonly sm: string;
182
+ readonly md: string;
183
+ readonly lg: string;
184
+ };
185
+ };
186
+
187
+ /**
188
+ * Rivivälit breakpointeittain. Vain xl ja l muuttuvat (lg-tasolla
189
+ * tiukempi).
190
+ */
191
+ export declare const lineHeightScale: {
192
+ readonly "display-xl": {
193
+ readonly base: string;
194
+ readonly sm: string;
195
+ readonly md: string;
196
+ readonly lg: string;
197
+ };
198
+ readonly "display-l": {
199
+ readonly base: string;
200
+ readonly sm: string;
201
+ readonly md: string;
202
+ readonly lg: string;
203
+ };
204
+ readonly "display-m": {
205
+ readonly base: string;
206
+ readonly sm: string;
207
+ readonly md: string;
208
+ readonly lg: string;
209
+ };
210
+ readonly "display-s": {
211
+ readonly base: string;
212
+ readonly sm: string;
213
+ readonly md: string;
214
+ readonly lg: string;
215
+ };
216
+ };
217
+
218
+ /**
219
+ * Nimi = arvo. Vain ladatut leikkaukset: Archivo 400/500/600, Bodoni
220
+ * 400/500.
221
+ */
222
+ export declare const weight: {
223
+ readonly "400": number;
224
+ readonly "500": number;
225
+ readonly "600": number;
226
+ };
227
+
228
+ /**
229
+ * Nimetty arvon mukaan: --space-24 on 24px. 32/40/48 lisättiin v1.1:ssä
230
+ * koska sivupohjat käyttivät niitä skaalan ulkopuolelta.
231
+ */
232
+ export declare const space: {
233
+ readonly "4": string;
234
+ readonly "8": string;
235
+ readonly "12": string;
236
+ readonly "16": string;
237
+ readonly "20": string;
238
+ readonly "24": string;
239
+ readonly "28": string;
240
+ readonly "32": string;
241
+ readonly "36": string;
242
+ readonly "40": string;
243
+ readonly "44": string;
244
+ readonly "48": string;
245
+ readonly "56": string;
246
+ readonly "72": string;
247
+ readonly "96": string;
248
+ readonly "128": string;
249
+ };
250
+
251
+ /**
252
+ * Lukumittoja on yksi: measure (62ch). Se riittää myös 14 pikselin
253
+ * tekstille, koska ch skaalautuu fonttikoon mukana — sama luku tarkoittaa
254
+ * suunnilleen samaa merkkimäärää joka koossa. Kaikki muut ch-arvot
255
+ * sivustolla olivat taittovarmistuksia eivätkä lukumittoja, ja ne on
256
+ * kirjoitettu pikseleiksi. Harvennettuun metatekstiin ch ei sovi lainkaan:
257
+ * se ei tunne letter-spacingia, joten 68ch lupasi 68 merkkiä mutta riville
258
+ * mahtui 35.
259
+ */
260
+ export declare const layout: {
261
+ readonly maxWidth: string;
262
+ readonly pagePadding: {
263
+ readonly base: string;
264
+ readonly sm: string;
265
+ readonly md: string;
266
+ readonly lg: string;
267
+ };
268
+ readonly columns: {
269
+ readonly base: number;
270
+ readonly sm: number;
271
+ readonly md: number;
272
+ readonly lg: number;
273
+ };
274
+ readonly gutter: {
275
+ readonly base: string;
276
+ readonly sm: string;
277
+ readonly md: string;
278
+ readonly lg: string;
279
+ };
280
+ readonly sectionGap: {
281
+ readonly base: string;
282
+ readonly sm: string;
283
+ readonly md: string;
284
+ readonly lg: string;
285
+ };
286
+ readonly tapMin: string;
287
+ readonly measure: string;
288
+ };
289
+
290
+ export declare const border: {
291
+ readonly hairline: string;
292
+ readonly hairlineStrong: string;
293
+ readonly radius: string;
294
+ readonly focusWidth: string;
295
+ readonly focusOffset: string;
296
+ };
297
+
298
+ /**
299
+ * Ikonit piirretään 16×16-ruudukolle ja skaalataan näihin kolmeen kokoon.
300
+ * Viivanpaksuus ei ole oma tokeninsa vaan border.hairline — ikoni on
301
+ * piirretty samalla viivalla kuin jokainen reuna. Kaaria ei ole
302
+ * (border.radius on 0), joten päätteet ovat tylpät ja kulmat terävät.
303
+ */
304
+ export declare const icon: {
305
+ readonly s: string;
306
+ readonly m: string;
307
+ readonly l: string;
308
+ };
309
+
310
+ export declare const motion: {
311
+ readonly duration: {
312
+ readonly fast: string;
313
+ readonly base: string;
314
+ readonly slow: string;
315
+ readonly reveal: string;
316
+ };
317
+ readonly easing: {
318
+ readonly standard: string;
319
+ readonly out: string;
320
+ };
321
+ readonly reveal: {
322
+ readonly translate: string;
323
+ };
324
+ };
325
+
326
+ declare const _default: { meta: typeof meta; modes: typeof modes; color: typeof color; font: typeof font; type: typeof type; typeScale: typeof typeScale; lineHeightScale: typeof lineHeightScale; weight: typeof weight; space: typeof space; layout: typeof layout; border: typeof border; icon: typeof icon; motion: typeof motion };
327
+ export default _default;
package/index.js ADDED
@@ -0,0 +1,326 @@
1
+ /* @pokela/tokens — generoitu tiedostosta tokens.json.
2
+ Älä muokkaa: aja `npm run paketti`. */
3
+
4
+ export const meta = {
5
+ name: "Pokela Portfolio",
6
+ version: "1.1.0",
7
+ direction: "1f / 1g — editorial grotesk layout with didone display",
8
+ defaultTheme: "system (light-dark), override via data-theme",
9
+ source: "styles/tokens.css — tämä tiedosto on sen koneluettava peili. scripts/check-tokens.mjs valvoo vastaavuutta.",
10
+ };
11
+
12
+ /**
13
+ * Moodiakselit: millä ulottuvuuksilla tokenin arvo vaihtelee. minWidth
14
+ * kertoo myös suunnan — arvot kasvavat alhaalta ylös, eli CSS on
15
+ * mobile-first.
16
+ */
17
+ export const modes = {
18
+ viewport: {
19
+ values: ["base", "sm", "md", "lg"],
20
+ minWidth: {
21
+ base: "0",
22
+ sm: "600px",
23
+ md: "900px",
24
+ lg: "1200px",
25
+ },
26
+ },
27
+ };
28
+
29
+ export const color = {
30
+ light: {
31
+ paper: "#FFFFFF",
32
+ "paper-alt": "#FAFAF8",
33
+ line: "#E8E8E6",
34
+ "line-strong": "#101112",
35
+ ink: "#101112",
36
+ "ink-muted": "#45464A",
37
+ "ink-faint": "#6A6C6E",
38
+ "badge-surface": "#EDEDEA",
39
+ "invert-surface": "#101112",
40
+ "invert-ink": "#F4F4F2",
41
+ "invert-faint": "#B6B8BA",
42
+ "placeholder-base": "#EFEFEC",
43
+ "placeholder-stripe": "#E7E7E3",
44
+ "code-surface": "#101112",
45
+ "code-ink": "#F4F4F2",
46
+ "code-muted": "#A8ABAD",
47
+ "code-faint": "#8A8D90",
48
+ },
49
+ dark: {
50
+ paper: "#0E0F10",
51
+ "paper-alt": "#16181A",
52
+ line: "#262829",
53
+ "line-strong": "#F4F4F2",
54
+ ink: "#F4F4F2",
55
+ "ink-muted": "#B9BCBF",
56
+ "ink-faint": "#9A9DA1",
57
+ "badge-surface": "#232527",
58
+ "invert-surface": "#F4F4F2",
59
+ "invert-ink": "#0E0F10",
60
+ "invert-faint": "#4A4C4E",
61
+ "placeholder-base": "#1B1D1F",
62
+ "placeholder-stripe": "#16181A",
63
+ "code-surface": "#1B1D1F",
64
+ "code-ink": "#F4F4F2",
65
+ "code-muted": "#A8ABAD",
66
+ "code-faint": "#8A8D90",
67
+ },
68
+ };
69
+
70
+ /**
71
+ * Fallback on var():n sisällä — muuten puuttuva next/font-muuttuja tekisi
72
+ * koko määrittelystä epäkelvon.
73
+ */
74
+ export const font = {
75
+ display: "var(--font-bodoni, 'Bodoni Moda'), Georgia, serif",
76
+ ui: "var(--font-archivo, 'Archivo'), Helvetica, Arial, sans-serif",
77
+ code: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
78
+ };
79
+
80
+ /**
81
+ * size/lineHeight ovat lg-arvoja (≥1200px). Pienemmät koot ovat kohdassa
82
+ * typeScale.
83
+ */
84
+ export const type = {
85
+ "display-xl": {
86
+ size: "136px",
87
+ lineHeight: "0.88",
88
+ tracking: "-0.02em",
89
+ weight: 400,
90
+ font: "display",
91
+ note: "wordmark / etusivun nimi",
92
+ },
93
+ "display-l": {
94
+ size: "82px",
95
+ lineHeight: "0.96",
96
+ tracking: "-0.005em",
97
+ weight: 400,
98
+ font: "display",
99
+ note: "footer-CTA, casesivun otsikko",
100
+ },
101
+ "display-m": {
102
+ size: "46px",
103
+ lineHeight: "1.12",
104
+ tracking: "-0.005em",
105
+ weight: 400,
106
+ font: "display",
107
+ note: "väitelause, osion otsikko",
108
+ },
109
+ "display-s": {
110
+ size: "38px",
111
+ lineHeight: "1",
112
+ tracking: "-0.005em",
113
+ weight: 400,
114
+ font: "display",
115
+ note: "indeksirivin työn nimi",
116
+ },
117
+ "body-l": {
118
+ size: "17px",
119
+ lineHeight: "1.65",
120
+ weight: 400,
121
+ font: "ui",
122
+ },
123
+ body: {
124
+ size: "16px",
125
+ lineHeight: "1.65",
126
+ weight: 400,
127
+ font: "ui",
128
+ },
129
+ "body-s": {
130
+ size: "14px",
131
+ lineHeight: "1.6",
132
+ weight: 400,
133
+ font: "ui",
134
+ },
135
+ meta: {
136
+ size: "11px",
137
+ lineHeight: "1.4",
138
+ tracking: "0.16em",
139
+ weight: 600,
140
+ transform: "uppercase",
141
+ font: "ui",
142
+ },
143
+ "meta-s": {
144
+ size: "10px",
145
+ lineHeight: "1.4",
146
+ tracking: "0.2em",
147
+ weight: 600,
148
+ transform: "uppercase",
149
+ font: "ui",
150
+ },
151
+ code: {
152
+ size: "13px",
153
+ lineHeight: "1.5",
154
+ weight: 400,
155
+ font: "code",
156
+ note: "koodilohko ja inline-koodi",
157
+ },
158
+ };
159
+
160
+ export const typeScale = {
161
+ "display-xl": {
162
+ base: "52px",
163
+ sm: "76px",
164
+ md: "104px",
165
+ lg: "136px",
166
+ },
167
+ "display-l": {
168
+ base: "40px",
169
+ sm: "52px",
170
+ md: "64px",
171
+ lg: "82px",
172
+ },
173
+ "display-m": {
174
+ base: "28px",
175
+ sm: "34px",
176
+ md: "40px",
177
+ lg: "46px",
178
+ },
179
+ "display-s": {
180
+ base: "26px",
181
+ sm: "30px",
182
+ md: "34px",
183
+ lg: "38px",
184
+ },
185
+ };
186
+
187
+ /**
188
+ * Rivivälit breakpointeittain. Vain xl ja l muuttuvat (lg-tasolla
189
+ * tiukempi).
190
+ */
191
+ export const lineHeightScale = {
192
+ "display-xl": {
193
+ base: "0.92",
194
+ sm: "0.92",
195
+ md: "0.92",
196
+ lg: "0.88",
197
+ },
198
+ "display-l": {
199
+ base: "1",
200
+ sm: "1",
201
+ md: "1",
202
+ lg: "0.96",
203
+ },
204
+ "display-m": {
205
+ base: "1.12",
206
+ sm: "1.12",
207
+ md: "1.12",
208
+ lg: "1.12",
209
+ },
210
+ "display-s": {
211
+ base: "1",
212
+ sm: "1",
213
+ md: "1",
214
+ lg: "1",
215
+ },
216
+ };
217
+
218
+ /**
219
+ * Nimi = arvo. Vain ladatut leikkaukset: Archivo 400/500/600, Bodoni
220
+ * 400/500.
221
+ */
222
+ export const weight = {
223
+ "400": 400,
224
+ "500": 500,
225
+ "600": 600,
226
+ };
227
+
228
+ /**
229
+ * Nimetty arvon mukaan: --space-24 on 24px. 32/40/48 lisättiin v1.1:ssä
230
+ * koska sivupohjat käyttivät niitä skaalan ulkopuolelta.
231
+ */
232
+ export const space = {
233
+ "4": "4px",
234
+ "8": "8px",
235
+ "12": "12px",
236
+ "16": "16px",
237
+ "20": "20px",
238
+ "24": "24px",
239
+ "28": "28px",
240
+ "32": "32px",
241
+ "36": "36px",
242
+ "40": "40px",
243
+ "44": "44px",
244
+ "48": "48px",
245
+ "56": "56px",
246
+ "72": "72px",
247
+ "96": "96px",
248
+ "128": "128px",
249
+ };
250
+
251
+ /**
252
+ * Lukumittoja on yksi: measure (62ch). Se riittää myös 14 pikselin
253
+ * tekstille, koska ch skaalautuu fonttikoon mukana — sama luku tarkoittaa
254
+ * suunnilleen samaa merkkimäärää joka koossa. Kaikki muut ch-arvot
255
+ * sivustolla olivat taittovarmistuksia eivätkä lukumittoja, ja ne on
256
+ * kirjoitettu pikseleiksi. Harvennettuun metatekstiin ch ei sovi lainkaan:
257
+ * se ei tunne letter-spacingia, joten 68ch lupasi 68 merkkiä mutta riville
258
+ * mahtui 35.
259
+ */
260
+ export const layout = {
261
+ maxWidth: "1440px",
262
+ pagePadding: {
263
+ base: "20px",
264
+ sm: "24px",
265
+ md: "28px",
266
+ lg: "36px",
267
+ },
268
+ columns: {
269
+ base: 4,
270
+ sm: 8,
271
+ md: 12,
272
+ lg: 12,
273
+ },
274
+ gutter: {
275
+ base: "16px",
276
+ sm: "20px",
277
+ md: "24px",
278
+ lg: "24px",
279
+ },
280
+ sectionGap: {
281
+ base: "48px",
282
+ sm: "64px",
283
+ md: "80px",
284
+ lg: "96px",
285
+ },
286
+ tapMin: "44px",
287
+ measure: "62ch",
288
+ };
289
+
290
+ export const border = {
291
+ hairline: "1px",
292
+ hairlineStrong: "2px",
293
+ radius: "0px",
294
+ focusWidth: "2px",
295
+ focusOffset: "2px",
296
+ };
297
+
298
+ /**
299
+ * Ikonit piirretään 16×16-ruudukolle ja skaalataan näihin kolmeen kokoon.
300
+ * Viivanpaksuus ei ole oma tokeninsa vaan border.hairline — ikoni on
301
+ * piirretty samalla viivalla kuin jokainen reuna. Kaaria ei ole
302
+ * (border.radius on 0), joten päätteet ovat tylpät ja kulmat terävät.
303
+ */
304
+ export const icon = {
305
+ s: "12px",
306
+ m: "16px",
307
+ l: "24px",
308
+ };
309
+
310
+ export const motion = {
311
+ duration: {
312
+ fast: "180ms",
313
+ base: "220ms",
314
+ slow: "320ms",
315
+ reveal: "600ms",
316
+ },
317
+ easing: {
318
+ standard: "cubic-bezier(0.2, 0.8, 0.2, 1)",
319
+ out: "cubic-bezier(0.16, 1, 0.3, 1)",
320
+ },
321
+ reveal: {
322
+ translate: "12px",
323
+ },
324
+ };
325
+
326
+ export default { meta, modes, color, font, type, typeScale, lineHeightScale, weight, space, layout, border, icon, motion };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@pokela/tokens",
3
+ "version": "1.1.0",
4
+ "description": "Design-tokenit: Pokela Portfolio. Generoitu, ei käsin ylläpidetty.",
5
+ "license": "MIT",
6
+ "author": "Veli-Matti Pokela",
7
+ "type": "module",
8
+ "sideEffects": [
9
+ "*.css"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./index.d.ts",
14
+ "import": "./index.js"
15
+ },
16
+ "./tokens.css": "./tokens.css",
17
+ "./tokens.json": "./tokens.json"
18
+ },
19
+ "files": [
20
+ "index.js",
21
+ "index.d.ts",
22
+ "tokens.css",
23
+ "tokens.json",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/VeliMattiPokela/pokela.fi.git",
30
+ "directory": "packages/tokens"
31
+ }
32
+ }
package/tokens.css ADDED
@@ -0,0 +1,260 @@
1
+ /* Pokela — design tokens
2
+ ------------------------------------------------------------------
3
+ Väriarvot määritellään kerran parina (-l / -d). Teemanvaihto vain
4
+ uudelleenohjaa eikä sisällä yhtään väriarvoa, joten vaalea ja
5
+ tumma eivät voi ajautua erilleen.
6
+
7
+ Aksenttiväriä ei ole. Korostus = musta/valkoinen-käännös
8
+ (--invert-surface / --invert-ink) tai hiljainen --badge-surface.
9
+ Väri sivulle tulee vain työn kuvista.
10
+
11
+ Mobile first: :root on mobiilikoko, min-width kasvattaa.
12
+ Arvojen lähde: tokens.json. scripts/check-tokens.mjs valvoo että
13
+ nämä kaksi pysyvät samana.
14
+
15
+ Fontit (Google Fonts, OFL): Bodoni Moda 400/500, Archivo 400/500/600.
16
+ */
17
+
18
+ :root {
19
+ color-scheme: light;
20
+
21
+ /* ---- type ------------------------------------------------------
22
+ --font-bodoni / --font-archivo tulevat next/fontista (itse
23
+ tarjoiltu, ei ulkoista pyyntöä).
24
+
25
+ Fallback on var()-funktion SISÄLLÄ, ei sen perässä. Jos
26
+ --font-bodoni puuttuu, `var(--font-bodoni), 'Bodoni Moda'`
27
+ tekisi koko määrittelystä epäkelvon (invalid at computed-value
28
+ time) ja typografia putoaisi selaimen oletusfonttiin ilman
29
+ virhettä. Tämä muoto kestää sen. */
30
+ --font-display: var(--font-bodoni, 'Bodoni Moda'), Georgia, serif;
31
+ --font-ui: var(--font-archivo, 'Archivo'), Helvetica, Arial, sans-serif;
32
+ /* Koodi on sivustolla toistuva elementti (komponenttinäkymä, /system,
33
+ Storybook), joten sen pino on token eikä viisi kertaa kirjoitettu
34
+ lista — joista yksi ehti jo erota muista. */
35
+ --font-code: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
36
+
37
+ /* MOBILE FIRST: base = 360–599 px. Isommat koot min-width-kyselyissä. */
38
+ --text-display-xl: 52px; --lh-display-xl: 0.92; --tr-display-xl: -0.02em;
39
+ --text-display-l: 40px; --lh-display-l: 1; --tr-display-l: -0.005em;
40
+ --text-display-m: 28px; --lh-display-m: 1.12; --tr-display-m: -0.005em;
41
+ --text-display-s: 26px; --lh-display-s: 1; --tr-display-s: -0.005em;
42
+ --text-body-l: 17px; --lh-body-l: 1.65;
43
+ --text-body: 16px; --lh-body: 1.65; /* älä laske alle 16px: iOS zoomaa inputit */
44
+ --text-body-s: 14px; --lh-body-s: 1.6;
45
+ --text-meta: 11px; --lh-meta: 1.4; --tr-meta: 0.16em;
46
+ --text-meta-s: 10px; --lh-meta-s: 1.4; --tr-meta-s: 0.2em;
47
+ --text-code: 13px; --lh-code: 1.5; /* monospace on optisesti isompi kuin body-s */
48
+ /* Paino on asteikko, koska ladattuja leikkauksia on vain kolme:
49
+ Archivo 400/500/600, Bodoni 400/500. font-synthesis-weight on
50
+ pois päältä, joten ladatun ulkopuolinen paino ei lihavoi vaan
51
+ jää huomaamatta. Nimi on arvo, kuten välistysasteikossa. */
52
+ --weight-400: 400;
53
+ --weight-500: 500;
54
+ --weight-600: 600;
55
+ --measure: 62ch; /* lukumitta — ainoa, myös 14px-tekstille */
56
+
57
+ /* ---- space (4px perusyksikkö) ----------------------------------
58
+ Nimetty arvon mukaan, ei juoksevalla numerolla: --space-24 on
59
+ 24px eikä vaadi taulukon muistamista. 32 / 40 / 48 lisättiin
60
+ koska sivupohjat käyttävät niitä — aiemmin ne jäivät skaalan
61
+ ulkopuolelle ja päätyivät kovakoodatuiksi. */
62
+ --space-4: 4px; --space-8: 8px; --space-12: 12px; --space-16: 16px;
63
+ --space-20: 20px; --space-24: 24px; --space-28: 28px; --space-32: 32px;
64
+ --space-36: 36px; --space-40: 40px; --space-44: 44px; --space-48: 48px;
65
+ --space-56: 56px; --space-72: 72px; --space-96: 96px; --space-128: 128px;
66
+
67
+ /* ---- layout ---------------------------------------------------- */
68
+ --max-width: 1440px;
69
+ --page-padding: 20px;
70
+ --columns: 4;
71
+ --gutter: 16px;
72
+ --section-gap: 48px;
73
+ --tap-min: 44px;
74
+ --hairline: 1px;
75
+ /* Korostettu viiva: hoverin ja valitun tilan paksuus. Sama arvo
76
+ kuin --focus-width, mutta eri asia — focus on saavutettavuutta,
77
+ tämä on typografiaa, ja ne saavat muuttua toisistaan riippumatta. */
78
+ --hairline-strong: 2px;
79
+ --radius: 0px;
80
+ --focus-width: 2px;
81
+ --focus-offset: 2px;
82
+
83
+ /* ---- ikonit ------------------------------------------------------
84
+ Ikonit piirretään 16×16-ruudukolle ja skaalataan näihin kolmeen
85
+ kokoon. Viivanpaksuutta ei ole tässä: se on --hairline, sama viiva
86
+ jolla jokainen reuna piirretään. Ks. styles/components/icon.css. */
87
+ --icon-s: 12px;
88
+ --icon-m: 16px;
89
+ --icon-l: 24px;
90
+
91
+ /* ---- motion ---------------------------------------------------- */
92
+ --dur-fast: 180ms; --dur-base: 220ms; --dur-slow: 320ms; --dur-reveal: 600ms;
93
+ --ease-standard: cubic-bezier(0.2, 0.8, 0.2, 1);
94
+ --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
95
+ --reveal-shift: 12px;
96
+
97
+ /* ---- colour ----------------------------------------------------
98
+ Arvot määritellään KERRAN parina: -l on vaalea, -d tumma.
99
+ Teemanvaihto alla vain uudelleenohjaa, eikä sisällä yhtään
100
+ väriarvoa — siksi vaalea ja tumma eivät voi ajautua erilleen.
101
+
102
+ Miksi ei light-dark(): Next 16:n Lightning CSS transpiloi sen
103
+ omaksi polyfillikseen, joka reagoi vain käyttöjärjestelmän
104
+ prefers-color-schemeen. Nimenomainen data-theme-valinta jäisi
105
+ silloin tehottomaksi ja teksti näkymättömiin. Tämä muoto toimii
106
+ kaikkialla eikä riipu käännösketjusta.
107
+
108
+ scripts/check-tokens.mjs valvoo että jokaisella tokenilla on
109
+ molemmat arvot ja että molemmat vaihtolohkot kattavat saman
110
+ listan. */
111
+ --paper-l: #FFFFFF; --paper-d: #0E0F10;
112
+ --paper-alt-l: #FAFAF8; --paper-alt-d: #16181A;
113
+ --line-l: #E8E8E6; --line-d: #262829;
114
+ --line-strong-l: #101112; --line-strong-d: #F4F4F2;
115
+ --ink-l: #101112; --ink-d: #F4F4F2;
116
+ --ink-muted-l: #45464A; --ink-muted-d: #B9BCBF;
117
+ --ink-faint-l: #6A6C6E; --ink-faint-d: #9A9DA1;
118
+ --badge-surface-l: #EDEDEA; --badge-surface-d: #232527;
119
+ --invert-surface-l: #101112; --invert-surface-d: #F4F4F2;
120
+ --invert-ink-l: #F4F4F2; --invert-ink-d: #0E0F10;
121
+ --invert-faint-l: #B6B8BA; --invert-faint-d: #4A4C4E;
122
+ --placeholder-base-l: #EFEFEC; --placeholder-base-d: #1B1D1F;
123
+ --placeholder-stripe-l: #E7E7E3; --placeholder-stripe-d: #16181A;
124
+
125
+ /* Koodipaneeli. Tumma molemmissa teemoissa: koodi on oma
126
+ artefaktinsa, ei leipätekstiä, ja tumma pinta on se mitä lukija
127
+ odottaa koodilta. Vaaleassa teemassa se on käännetty pinta,
128
+ tummassa hieman paperista nostettu. Musteet ovat samat
129
+ molemmilla, joten korostuksen kartta ei muutu teeman mukana. */
130
+ --code-surface-l: #101112; --code-surface-d: #1B1D1F;
131
+ --code-ink-l: #F4F4F2; --code-ink-d: #F4F4F2;
132
+ --code-muted-l: #A8ABAD; --code-muted-d: #A8ABAD;
133
+ --code-faint-l: #8A8D90; --code-faint-d: #8A8D90;
134
+
135
+ /* Oletus: vaalea. */
136
+ --paper: var(--paper-l);
137
+ --paper-alt: var(--paper-alt-l);
138
+ --line: var(--line-l);
139
+ --line-strong: var(--line-strong-l);
140
+ --ink: var(--ink-l);
141
+ --ink-muted: var(--ink-muted-l);
142
+ --ink-faint: var(--ink-faint-l);
143
+ --badge-surface: var(--badge-surface-l);
144
+ --invert-surface: var(--invert-surface-l);
145
+ --invert-ink: var(--invert-ink-l);
146
+ --invert-faint: var(--invert-faint-l);
147
+ --placeholder-base: var(--placeholder-base-l);
148
+ --placeholder-stripe: var(--placeholder-stripe-l);
149
+ --code-surface: var(--code-surface-l);
150
+ --code-ink: var(--code-ink-l);
151
+ --code-muted: var(--code-muted-l);
152
+ --code-faint: var(--code-faint-l);
153
+ }
154
+
155
+ /* Tumma paletti. Sama lista kahdesti, mutta ilman yhtäkään
156
+ väriarvoa — vain uudelleenohjaus. */
157
+ :root[data-theme='dark'] {
158
+ color-scheme: dark;
159
+ --paper: var(--paper-d);
160
+ --paper-alt: var(--paper-alt-d);
161
+ --line: var(--line-d);
162
+ --line-strong: var(--line-strong-d);
163
+ --ink: var(--ink-d);
164
+ --ink-muted: var(--ink-muted-d);
165
+ --ink-faint: var(--ink-faint-d);
166
+ --badge-surface: var(--badge-surface-d);
167
+ --invert-surface: var(--invert-surface-d);
168
+ --invert-ink: var(--invert-ink-d);
169
+ --invert-faint: var(--invert-faint-d);
170
+ --placeholder-base: var(--placeholder-base-d);
171
+ --placeholder-stripe: var(--placeholder-stripe-d);
172
+ --code-surface: var(--code-surface-d);
173
+ --code-ink: var(--code-ink-d);
174
+ --code-muted: var(--code-muted-d);
175
+ --code-faint: var(--code-faint-d);
176
+ }
177
+
178
+ /* Ilman nimenomaista valintaa seurataan käyttöjärjestelmää. */
179
+ @media (prefers-color-scheme: dark) {
180
+ :root:not([data-theme='light']) {
181
+ color-scheme: dark;
182
+ --paper: var(--paper-d);
183
+ --paper-alt: var(--paper-alt-d);
184
+ --line: var(--line-d);
185
+ --line-strong: var(--line-strong-d);
186
+ --ink: var(--ink-d);
187
+ --ink-muted: var(--ink-muted-d);
188
+ --ink-faint: var(--ink-faint-d);
189
+ --badge-surface: var(--badge-surface-d);
190
+ --invert-surface: var(--invert-surface-d);
191
+ --invert-ink: var(--invert-ink-d);
192
+ --invert-faint: var(--invert-faint-d);
193
+ --placeholder-base: var(--placeholder-base-d);
194
+ --placeholder-stripe: var(--placeholder-stripe-d);
195
+ --code-surface: var(--code-surface-d);
196
+ --code-ink: var(--code-ink-d);
197
+ --code-muted: var(--code-muted-d);
198
+ --code-faint: var(--code-faint-d);
199
+ }
200
+ }
201
+
202
+ :root[data-theme='light'] { color-scheme: light; }
203
+
204
+ /* ---- paperi -------------------------------------------------------
205
+ Sama uudelleenkytkentä kuin tummalla teemalla, mutta medialle eikä
206
+ mieltymykselle. Ilman tätä tummalla teemalla tulostettaisiin
207
+ valkoinen teksti valkoiselle paperille: selain ei tulosta taustoja,
208
+ joten pinta katoaa mutta teksti ei vaihda väriä.
209
+
210
+ Viivat ovat paperilla tummempia kuin ruudulla. #E8E8E6 on ruudulla
211
+ juuri sopiva hiusviiva, mutta painossa se katoaa. */
212
+ @media print {
213
+ :root,
214
+ :root[data-theme='dark'],
215
+ :root[data-theme='light'] {
216
+ color-scheme: light;
217
+ --paper: #ffffff;
218
+ --paper-alt: #ffffff;
219
+ --line: #b8b8b4;
220
+ --line-strong: #101112;
221
+ --ink: #101112;
222
+ --ink-muted: #3a3b3e;
223
+ --ink-faint: #56585a;
224
+ --badge-surface: #ffffff;
225
+ --invert-surface: #ffffff;
226
+ --invert-ink: #101112;
227
+ --invert-faint: #56585a;
228
+ --placeholder-base: #f2f2f0;
229
+ --placeholder-stripe: #e9e9e6;
230
+ }
231
+ }
232
+
233
+ /* ---- breakpoints (mobile first, min-width) ------------------------
234
+ base 0–599 4 saraketta, padding 20, yksi palsta
235
+ sm 600 8 saraketta, padding 24
236
+ md 900 12 saraketta, padding 28
237
+ lg 1200 12 saraketta, padding 36, täysi editoriaalinen skaala
238
+ */
239
+ @media (min-width: 600px) {
240
+ :root {
241
+ --page-padding: 24px; --columns: 8; --gutter: 20px; --section-gap: 64px;
242
+ --text-display-xl: 76px; --text-display-l: 52px;
243
+ --text-display-m: 34px; --text-display-s: 30px;
244
+ }
245
+ }
246
+ @media (min-width: 900px) {
247
+ :root {
248
+ --page-padding: 28px; --columns: 12; --gutter: 24px; --section-gap: 80px;
249
+ --text-display-xl: 104px; --text-display-l: 64px;
250
+ --text-display-m: 40px; --text-display-s: 34px;
251
+ }
252
+ }
253
+ @media (min-width: 1200px) {
254
+ :root {
255
+ --page-padding: 36px; --section-gap: 96px;
256
+ --text-display-xl: 136px; --lh-display-xl: 0.88;
257
+ --text-display-l: 82px; --lh-display-l: 0.96;
258
+ --text-display-m: 46px; --text-display-s: 38px;
259
+ }
260
+ }
package/tokens.json ADDED
@@ -0,0 +1,284 @@
1
+ {
2
+ "$meta": {
3
+ "name": "Pokela Portfolio",
4
+ "version": "1.1.0",
5
+ "direction": "1f / 1g — editorial grotesk layout with didone display",
6
+ "defaultTheme": "system (light-dark), override via data-theme",
7
+ "source": "styles/tokens.css — tämä tiedosto on sen koneluettava peili. scripts/check-tokens.mjs valvoo vastaavuutta."
8
+ },
9
+ "$modes": {
10
+ "$note": "Moodiakselit: millä ulottuvuuksilla tokenin arvo vaihtelee. minWidth kertoo myös suunnan — arvot kasvavat alhaalta ylös, eli CSS on mobile-first.",
11
+ "viewport": {
12
+ "values": [
13
+ "base",
14
+ "sm",
15
+ "md",
16
+ "lg"
17
+ ],
18
+ "minWidth": {
19
+ "base": "0",
20
+ "sm": "600px",
21
+ "md": "900px",
22
+ "lg": "1200px"
23
+ }
24
+ }
25
+ },
26
+ "color": {
27
+ "light": {
28
+ "paper": "#FFFFFF",
29
+ "paper-alt": "#FAFAF8",
30
+ "line": "#E8E8E6",
31
+ "line-strong": "#101112",
32
+ "ink": "#101112",
33
+ "ink-muted": "#45464A",
34
+ "ink-faint": "#6A6C6E",
35
+ "badge-surface": "#EDEDEA",
36
+ "invert-surface": "#101112",
37
+ "invert-ink": "#F4F4F2",
38
+ "invert-faint": "#B6B8BA",
39
+ "placeholder-base": "#EFEFEC",
40
+ "placeholder-stripe": "#E7E7E3",
41
+ "code-surface": "#101112",
42
+ "code-ink": "#F4F4F2",
43
+ "code-muted": "#A8ABAD",
44
+ "code-faint": "#8A8D90"
45
+ },
46
+ "dark": {
47
+ "paper": "#0E0F10",
48
+ "paper-alt": "#16181A",
49
+ "line": "#262829",
50
+ "line-strong": "#F4F4F2",
51
+ "ink": "#F4F4F2",
52
+ "ink-muted": "#B9BCBF",
53
+ "ink-faint": "#9A9DA1",
54
+ "badge-surface": "#232527",
55
+ "invert-surface": "#F4F4F2",
56
+ "invert-ink": "#0E0F10",
57
+ "invert-faint": "#4A4C4E",
58
+ "placeholder-base": "#1B1D1F",
59
+ "placeholder-stripe": "#16181A",
60
+ "code-surface": "#1B1D1F",
61
+ "code-ink": "#F4F4F2",
62
+ "code-muted": "#A8ABAD",
63
+ "code-faint": "#8A8D90"
64
+ }
65
+ },
66
+ "font": {
67
+ "display": "var(--font-bodoni, 'Bodoni Moda'), Georgia, serif",
68
+ "ui": "var(--font-archivo, 'Archivo'), Helvetica, Arial, sans-serif",
69
+ "$note": "Fallback on var():n sisällä — muuten puuttuva next/font-muuttuja tekisi koko määrittelystä epäkelvon.",
70
+ "code": "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
71
+ },
72
+ "typeNote": "size/lineHeight ovat lg-arvoja (≥1200px). Pienemmät koot ovat kohdassa typeScale.",
73
+ "type": {
74
+ "display-xl": {
75
+ "size": "136px",
76
+ "lineHeight": "0.88",
77
+ "tracking": "-0.02em",
78
+ "weight": 400,
79
+ "font": "display",
80
+ "note": "wordmark / etusivun nimi"
81
+ },
82
+ "display-l": {
83
+ "size": "82px",
84
+ "lineHeight": "0.96",
85
+ "tracking": "-0.005em",
86
+ "weight": 400,
87
+ "font": "display",
88
+ "note": "footer-CTA, casesivun otsikko"
89
+ },
90
+ "display-m": {
91
+ "size": "46px",
92
+ "lineHeight": "1.12",
93
+ "tracking": "-0.005em",
94
+ "weight": 400,
95
+ "font": "display",
96
+ "note": "väitelause, osion otsikko"
97
+ },
98
+ "display-s": {
99
+ "size": "38px",
100
+ "lineHeight": "1",
101
+ "tracking": "-0.005em",
102
+ "weight": 400,
103
+ "font": "display",
104
+ "note": "indeksirivin työn nimi"
105
+ },
106
+ "body-l": {
107
+ "size": "17px",
108
+ "lineHeight": "1.65",
109
+ "weight": 400,
110
+ "font": "ui"
111
+ },
112
+ "body": {
113
+ "size": "16px",
114
+ "lineHeight": "1.65",
115
+ "weight": 400,
116
+ "font": "ui"
117
+ },
118
+ "body-s": {
119
+ "size": "14px",
120
+ "lineHeight": "1.6",
121
+ "weight": 400,
122
+ "font": "ui"
123
+ },
124
+ "meta": {
125
+ "size": "11px",
126
+ "lineHeight": "1.4",
127
+ "tracking": "0.16em",
128
+ "weight": 600,
129
+ "transform": "uppercase",
130
+ "font": "ui"
131
+ },
132
+ "meta-s": {
133
+ "size": "10px",
134
+ "lineHeight": "1.4",
135
+ "tracking": "0.2em",
136
+ "weight": 600,
137
+ "transform": "uppercase",
138
+ "font": "ui"
139
+ },
140
+ "code": {
141
+ "size": "13px",
142
+ "lineHeight": "1.5",
143
+ "weight": 400,
144
+ "font": "code",
145
+ "note": "koodilohko ja inline-koodi"
146
+ }
147
+ },
148
+ "weight": {
149
+ "400": 400,
150
+ "500": 500,
151
+ "600": 600
152
+ },
153
+ "weightNote": "Nimi = arvo. Vain ladatut leikkaukset: Archivo 400/500/600, Bodoni 400/500.",
154
+ "typeScale": {
155
+ "display-xl": {
156
+ "base": "52px",
157
+ "sm": "76px",
158
+ "md": "104px",
159
+ "lg": "136px"
160
+ },
161
+ "display-l": {
162
+ "base": "40px",
163
+ "sm": "52px",
164
+ "md": "64px",
165
+ "lg": "82px"
166
+ },
167
+ "display-m": {
168
+ "base": "28px",
169
+ "sm": "34px",
170
+ "md": "40px",
171
+ "lg": "46px"
172
+ },
173
+ "display-s": {
174
+ "base": "26px",
175
+ "sm": "30px",
176
+ "md": "34px",
177
+ "lg": "38px"
178
+ }
179
+ },
180
+ "space": {
181
+ "4": "4px",
182
+ "8": "8px",
183
+ "12": "12px",
184
+ "16": "16px",
185
+ "20": "20px",
186
+ "24": "24px",
187
+ "28": "28px",
188
+ "32": "32px",
189
+ "36": "36px",
190
+ "40": "40px",
191
+ "44": "44px",
192
+ "48": "48px",
193
+ "56": "56px",
194
+ "72": "72px",
195
+ "96": "96px",
196
+ "128": "128px"
197
+ },
198
+ "spaceNote": "Nimetty arvon mukaan: --space-24 on 24px. 32/40/48 lisättiin v1.1:ssä koska sivupohjat käyttivät niitä skaalan ulkopuolelta.",
199
+ "layout": {
200
+ "maxWidth": "1440px",
201
+ "pagePadding": {
202
+ "base": "20px",
203
+ "sm": "24px",
204
+ "md": "28px",
205
+ "lg": "36px"
206
+ },
207
+ "columns": {
208
+ "base": 4,
209
+ "sm": 8,
210
+ "md": 12,
211
+ "lg": 12
212
+ },
213
+ "gutter": {
214
+ "base": "16px",
215
+ "sm": "20px",
216
+ "md": "24px",
217
+ "lg": "24px"
218
+ },
219
+ "sectionGap": {
220
+ "base": "48px",
221
+ "sm": "64px",
222
+ "md": "80px",
223
+ "lg": "96px"
224
+ },
225
+ "tapMin": "44px",
226
+ "measure": "62ch"
227
+ },
228
+ "border": {
229
+ "hairline": "1px",
230
+ "hairlineStrong": "2px",
231
+ "radius": "0px",
232
+ "focusWidth": "2px",
233
+ "focusOffset": "2px"
234
+ },
235
+ "icon": {
236
+ "s": "12px",
237
+ "m": "16px",
238
+ "l": "24px"
239
+ },
240
+ "measureNote": "Lukumittoja on yksi: measure (62ch). Se riittää myös 14 pikselin tekstille, koska ch skaalautuu fonttikoon mukana — sama luku tarkoittaa suunnilleen samaa merkkimäärää joka koossa. Kaikki muut ch-arvot sivustolla olivat taittovarmistuksia eivätkä lukumittoja, ja ne on kirjoitettu pikseleiksi. Harvennettuun metatekstiin ch ei sovi lainkaan: se ei tunne letter-spacingia, joten 68ch lupasi 68 merkkiä mutta riville mahtui 35.",
241
+ "iconNote": "Ikonit piirretään 16×16-ruudukolle ja skaalataan näihin kolmeen kokoon. Viivanpaksuus ei ole oma tokeninsa vaan border.hairline — ikoni on piirretty samalla viivalla kuin jokainen reuna. Kaaria ei ole (border.radius on 0), joten päätteet ovat tylpät ja kulmat terävät.",
242
+ "motion": {
243
+ "duration": {
244
+ "fast": "180ms",
245
+ "base": "220ms",
246
+ "slow": "320ms",
247
+ "reveal": "600ms"
248
+ },
249
+ "easing": {
250
+ "standard": "cubic-bezier(0.2, 0.8, 0.2, 1)",
251
+ "out": "cubic-bezier(0.16, 1, 0.3, 1)"
252
+ },
253
+ "reveal": {
254
+ "translate": "12px"
255
+ }
256
+ },
257
+ "lineHeightScale": {
258
+ "display-xl": {
259
+ "base": "0.92",
260
+ "sm": "0.92",
261
+ "md": "0.92",
262
+ "lg": "0.88"
263
+ },
264
+ "display-l": {
265
+ "base": "1",
266
+ "sm": "1",
267
+ "md": "1",
268
+ "lg": "0.96"
269
+ },
270
+ "display-m": {
271
+ "base": "1.12",
272
+ "sm": "1.12",
273
+ "md": "1.12",
274
+ "lg": "1.12"
275
+ },
276
+ "display-s": {
277
+ "base": "1",
278
+ "sm": "1",
279
+ "md": "1",
280
+ "lg": "1"
281
+ }
282
+ },
283
+ "lineHeightNote": "Rivivälit breakpointeittain. Vain xl ja l muuttuvat (lg-tasolla tiukempi)."
284
+ }