@hanzo/design 0.5.11 → 0.5.13

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.
@@ -42,6 +42,11 @@ export interface Preference {
42
42
  * above 1 opens the display end and tightens the small one.
43
43
  */
44
44
  ratio?: number;
45
+ /**
46
+ * A modular scale for the display rungs — 1.618 for golden, 1.25 for a major
47
+ * third. Absent means the authored ramp, which is the tuned default.
48
+ */
49
+ modular?: number;
45
50
  density?: Density;
46
51
  /** Which face the page is set in. */
47
52
  font?: Face;
@@ -74,6 +79,31 @@ export declare const TYPE_MAX = 1.4;
74
79
  */
75
80
  export declare const RATIO_MIN = 0.75;
76
81
  export declare const RATIO_MAX = 1.5;
82
+ /**
83
+ * A MODULAR scale — the classical one, where each display rung is the one below
84
+ * it times a fixed ratio. Golden is 1.618; the musical intervals designers name
85
+ * are 1.2 (minor third), 1.25 (major third), 1.333 (perfect fourth) and 1.5
86
+ * (perfect fifth).
87
+ *
88
+ * This is a different RULE from `ratio`, not another dial on it. `ratio` tunes
89
+ * the contrast of the ramp `tokens/typography.css` authored; this REPLACES that
90
+ * ramp's display half with a geometric one. Naming a golden-ratio preset as a
91
+ * contrast value would have been the dishonest version of this feature.
92
+ *
93
+ * Bounded below 1.05 because a ratio at 1 is not a scale — every display rung
94
+ * collapses onto the one before it — and above 2 because doubling every step
95
+ * puts the fourth rung past a phone's whole width.
96
+ *
97
+ * Golden is exactly 1.618 here and is NOT clamped down to something tamer,
98
+ * because a scale that quietly gives you not-golden when you asked for golden is
99
+ * worse than one that refuses. What makes that safe is the ceiling below: this
100
+ * ramp has EIGHT display rungs and a classical modular scale is used with about
101
+ * four, so at 1.618 the eighth would be 799px. Measured, --text-8xl and
102
+ * --text-9xl are referenced in 15 files across the fleet, so that is a broken
103
+ * page and not a hypothetical.
104
+ */
105
+ export declare const MODULAR_MIN = 1.05;
106
+ export declare const MODULAR_MAX = 2;
77
107
  /**
78
108
  * Is this a colour, or is it something being smuggled into a style attribute?
79
109
  *
@@ -1 +1 @@
1
- {"version":3,"file":"preference.d.ts","sourceRoot":"","sources":["../src/preference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,CAAC;AAE5D;;;GAGG;AACH,MAAM,MAAM,IAAI,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AAE3D,8CAA8C;AAC9C,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;AAEpD,MAAM,WAAW,UAAU;IACzB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qCAAqC;IACrC,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,4DAA4D;IAC5D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,OAAO,CAAC;AAC7B,eAAO,MAAM,QAAQ,MAAM,CAAC;AAE5B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,OAAO,CAAC;AAC9B,eAAO,MAAM,SAAS,MAAM,CAAC;AAsD7B;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAS1C;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAqC1D;AAED,2EAA2E;AAC3E,wBAAgB,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,SAAc,GAAG,MAAM,CAKjE"}
1
+ {"version":3,"file":"preference.d.ts","sourceRoot":"","sources":["../src/preference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,CAAC;AAE5D;;;GAGG;AACH,MAAM,MAAM,IAAI,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AAE3D,8CAA8C;AAC9C,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;AAEpD,MAAM,WAAW,UAAU;IACzB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qCAAqC;IACrC,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,4DAA4D;IAC5D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,OAAO,CAAC;AAC7B,eAAO,MAAM,QAAQ,MAAM,CAAC;AAE5B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,OAAO,CAAC;AAC9B,eAAO,MAAM,SAAS,MAAM,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,WAAW,OAAO,CAAC;AAChC,eAAO,MAAM,WAAW,IAAI,CAAC;AAmE7B;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAS1C;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA0D1D;AAED,2EAA2E;AAC3E,wBAAgB,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,SAAc,GAAG,MAAM,CAKjE"}
@@ -49,6 +49,43 @@ export const TYPE_MAX = 1.4;
49
49
  */
50
50
  export const RATIO_MIN = 0.75;
51
51
  export const RATIO_MAX = 1.5;
52
+ /**
53
+ * A MODULAR scale — the classical one, where each display rung is the one below
54
+ * it times a fixed ratio. Golden is 1.618; the musical intervals designers name
55
+ * are 1.2 (minor third), 1.25 (major third), 1.333 (perfect fourth) and 1.5
56
+ * (perfect fifth).
57
+ *
58
+ * This is a different RULE from `ratio`, not another dial on it. `ratio` tunes
59
+ * the contrast of the ramp `tokens/typography.css` authored; this REPLACES that
60
+ * ramp's display half with a geometric one. Naming a golden-ratio preset as a
61
+ * contrast value would have been the dishonest version of this feature.
62
+ *
63
+ * Bounded below 1.05 because a ratio at 1 is not a scale — every display rung
64
+ * collapses onto the one before it — and above 2 because doubling every step
65
+ * puts the fourth rung past a phone's whole width.
66
+ *
67
+ * Golden is exactly 1.618 here and is NOT clamped down to something tamer,
68
+ * because a scale that quietly gives you not-golden when you asked for golden is
69
+ * worse than one that refuses. What makes that safe is the ceiling below: this
70
+ * ramp has EIGHT display rungs and a classical modular scale is used with about
71
+ * four, so at 1.618 the eighth would be 799px. Measured, --text-8xl and
72
+ * --text-9xl are referenced in 15 files across the fleet, so that is a broken
73
+ * page and not a hypothetical.
74
+ */
75
+ export const MODULAR_MIN = 1.05;
76
+ export const MODULAR_MAX = 2;
77
+ /**
78
+ * The rungs a modular scale REGENERATES, in order, and the one it starts from.
79
+ *
80
+ * It deliberately stops at the display register. A geometric scale through the
81
+ * interface rungs is unusable at any real ratio: at 1.25 from a 14px base the
82
+ * next rungs are 17.5 and 21.9, so the 13px nav label and the 15px lead — the
83
+ * near-linear steps a dense interface is built on — do not exist. `xl` is the
84
+ * anchor because it is the last interface rung, so the display half continues
85
+ * from where the interface ends rather than restarting under it.
86
+ */
87
+ const DISPLAY_RUNGS = ["2xl", "3xl", "4xl", "5xl", "6xl", "7xl", "8xl", "9xl"];
88
+ const MODULAR_ANCHOR_REM = 1.0625; // --text-xl, 17px
52
89
  /**
53
90
  * Density moves SPACING only, and its range is much tighter than type's.
54
91
  *
@@ -128,6 +165,26 @@ export function vars(p) {
128
165
  if (typeof p.ratio === "number" && Number.isFinite(p.ratio)) {
129
166
  out["--type-ratio"] = round(clamp(p.ratio, RATIO_MIN, RATIO_MAX));
130
167
  }
168
+ if (typeof p.modular === "number" && Number.isFinite(p.modular)) {
169
+ // Each display rung is the anchor times the ratio to its step, emitted as an
170
+ // explicit property. An INLINE custom property on :root outranks the
171
+ // stylesheet, so these simply replace the authored rungs — nothing has to be
172
+ // unset first, and the interface rungs the sheet declares are untouched
173
+ // because none is named here. It multiplies out with `--type-scale` for free,
174
+ // since every rung is still read through that ramp's own calc.
175
+ const r = clamp(p.modular, MODULAR_MIN, MODULAR_MAX);
176
+ DISPLAY_RUNGS.forEach((rung, i) => {
177
+ const rem = MODULAR_ANCHOR_REM * Math.pow(r, i + 1);
178
+ // THE SAME clamp the authored ramp carries, read from the same two names.
179
+ // A regenerated rung and an authored one must be bounded identically or
180
+ // the bound is a property of which code path produced the value, which is
181
+ // exactly the kind of "it depends" a design system exists to delete. The
182
+ // bounds are declared once, in tokens/typography.css, and referenced here
183
+ // — never restated, so moving one moves both.
184
+ out[`--text-${rung}`] =
185
+ `clamp(var(--text-floor), calc(${round(rem)}rem * var(--type-scale, 1)), var(--text-ceiling))`;
186
+ });
187
+ }
131
188
  if (p.density && p.density in DENSITY) {
132
189
  out["--density"] = round(DENSITY[p.density]);
133
190
  }
@@ -15,20 +15,25 @@ export declare const colors: {
15
15
  readonly 'pure-white': "#FFFFFF";
16
16
  readonly 'hanzo-black': "#0A0A0B";
17
17
  readonly 'hanzo-white': "#FFFFFF";
18
+ readonly 'white-03': "rgb(255 255 255 / .03)";
18
19
  readonly 'white-05': "rgb(255 255 255 / .05)";
19
20
  readonly 'white-06': "rgb(255 255 255 / .06)";
20
21
  readonly 'white-08': "rgb(255 255 255 / .08)";
21
22
  readonly 'white-10': "rgb(255 255 255 / .10)";
23
+ readonly 'white-14': "rgb(255 255 255 / .14)";
22
24
  readonly 'white-15': "rgb(255 255 255 / .15)";
23
25
  readonly 'white-16': "rgb(255 255 255 / .16)";
26
+ readonly 'white-18': "rgb(255 255 255 / .18)";
24
27
  readonly 'white-20': "rgb(255 255 255 / .20)";
25
28
  readonly 'white-22': "rgb(255 255 255 / .22)";
26
29
  readonly 'white-30': "rgb(255 255 255 / .30)";
27
30
  readonly 'white-40': "rgb(255 255 255 / .40)";
31
+ readonly 'white-45': "rgb(255 255 255 / .45)";
28
32
  readonly 'white-55': "rgb(255 255 255 / .55)";
29
33
  readonly 'white-60': "rgb(255 255 255 / .60)";
30
34
  readonly 'white-78': "rgb(255 255 255 / .78)";
31
35
  readonly 'white-80': "rgb(255 255 255 / .80)";
36
+ readonly 'white-92': "rgb(255 255 255 / .92)";
32
37
  readonly background: "#0a0a0a";
33
38
  readonly foreground: "#fafafa";
34
39
  readonly card: "#0f0f0f";
@@ -93,6 +98,8 @@ export declare const colors: {
93
98
  export declare const typography: {
94
99
  readonly 'type-scale': "1";
95
100
  readonly 'type-ratio': "1";
101
+ readonly 'text-floor': "0.625rem";
102
+ readonly 'text-ceiling': "30vw";
96
103
  readonly 'text-xs': "0.6875rem";
97
104
  readonly 'leading-xs': "1rem";
98
105
  readonly 'text-sm': "0.8125rem";
@@ -311,20 +318,25 @@ export declare const cssVars: {
311
318
  readonly '--pure-white': "#FFFFFF";
312
319
  readonly '--hanzo-black': "#0A0A0B";
313
320
  readonly '--hanzo-white': "#FFFFFF";
321
+ readonly '--white-03': "rgb(255 255 255 / .03)";
314
322
  readonly '--white-05': "rgb(255 255 255 / .05)";
315
323
  readonly '--white-06': "rgb(255 255 255 / .06)";
316
324
  readonly '--white-08': "rgb(255 255 255 / .08)";
317
325
  readonly '--white-10': "rgb(255 255 255 / .10)";
326
+ readonly '--white-14': "rgb(255 255 255 / .14)";
318
327
  readonly '--white-15': "rgb(255 255 255 / .15)";
319
328
  readonly '--white-16': "rgb(255 255 255 / .16)";
329
+ readonly '--white-18': "rgb(255 255 255 / .18)";
320
330
  readonly '--white-20': "rgb(255 255 255 / .20)";
321
331
  readonly '--white-22': "rgb(255 255 255 / .22)";
322
332
  readonly '--white-30': "rgb(255 255 255 / .30)";
323
333
  readonly '--white-40': "rgb(255 255 255 / .40)";
334
+ readonly '--white-45': "rgb(255 255 255 / .45)";
324
335
  readonly '--white-55': "rgb(255 255 255 / .55)";
325
336
  readonly '--white-60': "rgb(255 255 255 / .60)";
326
337
  readonly '--white-78': "rgb(255 255 255 / .78)";
327
338
  readonly '--white-80': "rgb(255 255 255 / .80)";
339
+ readonly '--white-92': "rgb(255 255 255 / .92)";
328
340
  readonly '--background': "#0a0a0a";
329
341
  readonly '--foreground': "#fafafa";
330
342
  readonly '--card': "#0f0f0f";
@@ -386,6 +398,8 @@ export declare const cssVars: {
386
398
  readonly '--chrome-dot-green': "rgb(34 197 94 / .6)";
387
399
  readonly '--type-scale': "1";
388
400
  readonly '--type-ratio': "1";
401
+ readonly '--text-floor': "0.625rem";
402
+ readonly '--text-ceiling': "30vw";
389
403
  readonly '--text-xs': "0.6875rem";
390
404
  readonly '--leading-xs': "1rem";
391
405
  readonly '--text-sm': "0.8125rem";
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.gen.d.ts","sourceRoot":"","sources":["../src/tokens.gen.ts"],"names":[],"mappings":"AAIA,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyFT,CAAA;AAEV,0EAA0E;AAC1E,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiEb,CAAA;AAEV,oEAAoE;AACpE,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CV,CAAA;AAEV,8DAA8D;AAC9D,eAAO,MAAM,IAAI;;;;;;;;;;;;CAYP,CAAA;AAEV,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;CAST,CAAA;AAEV,wEAAwE;AACxE,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BZ,CAAA;AAEV,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;;;;;CAaT,CAAA;AAEV,6DAA6D;AAC7D,eAAO,MAAM,MAAM;;;;;;;;;;;;CAYT,CAAA;AAEV,gEAAgE;AAChE,eAAO,MAAM,KAAK;;;;;;CAMR,CAAA;AAEV,8DAA8D;AAC9D,eAAO,MAAM,IAAI,IAEP,CAAA;AAEV,sFAAsF;AACtF,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4QV,CAAA;AAEV,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,OAAO,CAAA"}
1
+ {"version":3,"file":"tokens.gen.d.ts","sourceRoot":"","sources":["../src/tokens.gen.ts"],"names":[],"mappings":"AAIA,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8FT,CAAA;AAEV,0EAA0E;AAC1E,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmEb,CAAA;AAEV,oEAAoE;AACpE,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CV,CAAA;AAEV,8DAA8D;AAC9D,eAAO,MAAM,IAAI;;;;;;;;;;;;CAYP,CAAA;AAEV,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;CAST,CAAA;AAEV,wEAAwE;AACxE,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BZ,CAAA;AAEV,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;;;;;CAaT,CAAA;AAEV,6DAA6D;AAC7D,eAAO,MAAM,MAAM;;;;;;;;;;;;CAYT,CAAA;AAEV,gEAAgE;AAChE,eAAO,MAAM,KAAK;;;;;;CAMR,CAAA;AAEV,8DAA8D;AAC9D,eAAO,MAAM,IAAI,IAEP,CAAA;AAEV,sFAAsF;AACtF,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmRV,CAAA;AAEV,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,OAAO,CAAA"}
@@ -18,20 +18,25 @@ export const colors = {
18
18
  'pure-white': '#FFFFFF',
19
19
  'hanzo-black': '#0A0A0B',
20
20
  'hanzo-white': '#FFFFFF',
21
+ 'white-03': 'rgb(255 255 255 / .03)',
21
22
  'white-05': 'rgb(255 255 255 / .05)',
22
23
  'white-06': 'rgb(255 255 255 / .06)',
23
24
  'white-08': 'rgb(255 255 255 / .08)',
24
25
  'white-10': 'rgb(255 255 255 / .10)',
26
+ 'white-14': 'rgb(255 255 255 / .14)',
25
27
  'white-15': 'rgb(255 255 255 / .15)',
26
28
  'white-16': 'rgb(255 255 255 / .16)',
29
+ 'white-18': 'rgb(255 255 255 / .18)',
27
30
  'white-20': 'rgb(255 255 255 / .20)',
28
31
  'white-22': 'rgb(255 255 255 / .22)',
29
32
  'white-30': 'rgb(255 255 255 / .30)',
30
33
  'white-40': 'rgb(255 255 255 / .40)',
34
+ 'white-45': 'rgb(255 255 255 / .45)',
31
35
  'white-55': 'rgb(255 255 255 / .55)',
32
36
  'white-60': 'rgb(255 255 255 / .60)',
33
37
  'white-78': 'rgb(255 255 255 / .78)',
34
38
  'white-80': 'rgb(255 255 255 / .80)',
39
+ 'white-92': 'rgb(255 255 255 / .92)',
35
40
  'background': '#0a0a0a',
36
41
  'foreground': '#fafafa',
37
42
  'card': '#0f0f0f',
@@ -96,6 +101,8 @@ export const colors = {
96
101
  export const typography = {
97
102
  'type-scale': '1',
98
103
  'type-ratio': '1',
104
+ 'text-floor': '0.625rem',
105
+ 'text-ceiling': '30vw',
99
106
  'text-xs': '0.6875rem',
100
107
  'leading-xs': '1rem',
101
108
  'text-sm': '0.8125rem',
@@ -314,20 +321,25 @@ export const cssVars = {
314
321
  '--pure-white': '#FFFFFF',
315
322
  '--hanzo-black': '#0A0A0B',
316
323
  '--hanzo-white': '#FFFFFF',
324
+ '--white-03': 'rgb(255 255 255 / .03)',
317
325
  '--white-05': 'rgb(255 255 255 / .05)',
318
326
  '--white-06': 'rgb(255 255 255 / .06)',
319
327
  '--white-08': 'rgb(255 255 255 / .08)',
320
328
  '--white-10': 'rgb(255 255 255 / .10)',
329
+ '--white-14': 'rgb(255 255 255 / .14)',
321
330
  '--white-15': 'rgb(255 255 255 / .15)',
322
331
  '--white-16': 'rgb(255 255 255 / .16)',
332
+ '--white-18': 'rgb(255 255 255 / .18)',
323
333
  '--white-20': 'rgb(255 255 255 / .20)',
324
334
  '--white-22': 'rgb(255 255 255 / .22)',
325
335
  '--white-30': 'rgb(255 255 255 / .30)',
326
336
  '--white-40': 'rgb(255 255 255 / .40)',
337
+ '--white-45': 'rgb(255 255 255 / .45)',
327
338
  '--white-55': 'rgb(255 255 255 / .55)',
328
339
  '--white-60': 'rgb(255 255 255 / .60)',
329
340
  '--white-78': 'rgb(255 255 255 / .78)',
330
341
  '--white-80': 'rgb(255 255 255 / .80)',
342
+ '--white-92': 'rgb(255 255 255 / .92)',
331
343
  '--background': '#0a0a0a',
332
344
  '--foreground': '#fafafa',
333
345
  '--card': '#0f0f0f',
@@ -389,6 +401,8 @@ export const cssVars = {
389
401
  '--chrome-dot-green': 'rgb(34 197 94 / .6)',
390
402
  '--type-scale': '1',
391
403
  '--type-ratio': '1',
404
+ '--text-floor': '0.625rem',
405
+ '--text-ceiling': '30vw',
392
406
  '--text-xs': '0.6875rem',
393
407
  '--leading-xs': '1rem',
394
408
  '--text-sm': '0.8125rem',
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/design",
3
- "version": "0.5.11",
4
- "packageManager": "pnpm@11.17.0",
3
+ "version": "0.5.13",
5
4
  "description": "Hanzo Design System — monochrome, dark-default tokens + components + brand assets, the single source of truth for every Hanzo surface. CSS + typed programmatic tokens.",
6
5
  "license": "MIT OR Apache-2.0",
7
6
  "repository": {
@@ -32,13 +31,6 @@
32
31
  "./skills/*": "./skills/*",
33
32
  "./tailwind.css": "./tailwind.css"
34
33
  },
35
- "scripts": {
36
- "gen": "node scripts/gen-tokens.mjs",
37
- "test": "node scripts/check-tokens.mjs && node scripts/check-preference.mjs && node scripts/lint.mjs .",
38
- "build": "pnpm gen && pnpm test && tsc -p tsconfig.json && node scripts/check-dist.mjs",
39
- "prepublishOnly": "pnpm build",
40
- "lint": "node scripts/lint.mjs"
41
- },
42
34
  "devDependencies": {
43
35
  "typescript": "^5.9.3"
44
36
  },
@@ -71,5 +63,11 @@
71
63
  },
72
64
  "bin": {
73
65
  "hanzo-design-lint": "./scripts/lint.mjs"
66
+ },
67
+ "scripts": {
68
+ "gen": "node scripts/gen-tokens.mjs",
69
+ "test": "node scripts/check-tokens.mjs && node scripts/check-preference.mjs && node scripts/lint.mjs .",
70
+ "build": "pnpm gen && pnpm test && tsc -p tsconfig.json && node scripts/check-dist.mjs",
71
+ "lint": "node scripts/lint.mjs"
74
72
  }
75
- }
73
+ }
@@ -102,6 +102,12 @@ check("EVERY ramp rung carries its multiplier — no rung can opt out", () => {
102
102
  const value = m[1].trim();
103
103
  if (value === "0") continue; // zero times anything is zero
104
104
  if (value.startsWith("var(")) continue; // an alias inherits its target's calc
105
+ // --text-floor / --text-ceiling are the BOUNDS every rung is clamped
106
+ // between, not rungs. A bound that scaled with the knob would move with
107
+ // the value it exists to bound and could never bind — which is the whole
108
+ // reason a floor is stated in absolute units and a ceiling in the
109
+ // viewport's.
110
+ if (/^--text-(floor|ceiling):/.test(m[0])) continue;
105
111
  if (!value.includes(`var(${knob}`)) bare.push(`${file}: ${m[0].trim()}`);
106
112
  }
107
113
  }
@@ -144,6 +150,45 @@ check("an unknown stored value is refused, not passed through", () => {
144
150
  eq(vars({ width: "enormous" }), {});
145
151
  });
146
152
 
153
+ check("a modular scale regenerates the DISPLAY rungs, geometrically", () => {
154
+ const v = vars({ modular: 1.618 });
155
+ const names = Object.keys(v);
156
+ eq(names.length, 8, "a modular scale emits the eight display rungs:");
157
+ // Each rung is the one below it times the ratio — that IS the scale.
158
+ const rem = (n) => parseFloat(v[`--text-${n}`].match(/([\d.]+)rem/)[1]);
159
+ const rungs = ["2xl","3xl","4xl","5xl","6xl","7xl","8xl","9xl"];
160
+ for (let i = 1; i < rungs.length; i++) {
161
+ const r = rem(rungs[i]) / rem(rungs[i - 1]);
162
+ ok(Math.abs(r - 1.618) < 0.001, `${rungs[i]}/${rungs[i-1]} = ${r}, want the ratio`);
163
+ }
164
+ // Anchored on xl (17px), so the display half continues the interface rather
165
+ // than restarting under it.
166
+ ok(Math.abs(rem("2xl") - 1.0625 * 1.618) < 0.001, "2xl must be xl x ratio");
167
+ });
168
+
169
+ check("a modular scale leaves the INTERFACE rungs alone", () => {
170
+ // A geometric scale through xs..xl is unusable at any real ratio: the 13px nav
171
+ // label and the 15px lead simply do not exist on one.
172
+ const emitted = Object.keys(vars({ modular: 1.618 }));
173
+ for (const n of ["--text-xs", "--text-sm", "--text-base", "--text-lg", "--text-xl"]) {
174
+ ok(!emitted.includes(n), `${n} is the interface register and must not be regenerated`);
175
+ }
176
+ });
177
+
178
+ check("a modular scale still answers to the size knob", () => {
179
+ // It multiplies out with --type-scale rather than freezing a px value.
180
+ ok(vars({ modular: 1.5 })["--text-4xl"].includes("var(--type-scale, 1)"),
181
+ "a regenerated rung must carry the size knob");
182
+ });
183
+
184
+ check("the modular ratio is CLAMPED to something that is still a scale", () => {
185
+ // At 1 every display rung collapses onto the one below it.
186
+ const flat = vars({ modular: 1 });
187
+ const rem = (v, n) => parseFloat(v[`--text-${n}`].match(/([\d.]+)rem/)[1]);
188
+ ok(rem(flat, "3xl") > rem(flat, "2xl"), "a clamped ratio must still ascend");
189
+ eq(vars({ modular: 99 })["--text-2xl"], vars({ modular: 2 })["--text-2xl"], "max:");
190
+ });
191
+
147
192
  check("a colour lands on both --primary and --accent", () => {
148
193
  const v = vars({ accent: "#808000" });
149
194
  eq(v["--primary"], "#808000");
@@ -64,12 +64,24 @@ function parse(css) {
64
64
  * against a knob the table does not have.
65
65
  */
66
66
  const KNOB = /^calc\(\s*([^*]+?)\s*\*\s*var\(--(?:type-scale|density),\s*1\)\s*\)$/
67
- const FLOOR = /^max\(\s*[\d.]+[a-z%]*\s*,\s*(calc\(.+\))\s*\)$/
67
+ // The BOUNDS peel first. Every rung is `clamp(floor, value, ceiling)` — one
68
+ // rule at both ends — and the table carries the VALUE: a bound is a rendering
69
+ // decision the browser makes against a live viewport, so a JS table cannot hold
70
+ // one and must not pretend to. `max(floor, value)` is the older one-ended form,
71
+ // kept so a token file mid-migration still generates.
72
+ const BOUNDS = [
73
+ /^clamp\(\s*[^,]+,\s*(calc\(.*\))\s*,\s*[^,]+\)$/,
74
+ /^max\(\s*[\d.]+[a-z%]*\s*,\s*(calc\(.+\))\s*\)$/,
75
+ ]
68
76
  const RUNG =
69
77
  /^calc\(\(\s*([\d.]+)(r?em|px)\s*([+-])\s*([\d.]+)(?:r?em|px)\s*\*\s*var\(--type-ratio,\s*1\)\s*\)\s*\*\s*var\(--type-scale,\s*1\)\s*\)$/
70
78
 
71
79
  const scale = (v) => {
72
- const inner = v.match(FLOOR)?.[1] ?? v
80
+ let inner = v
81
+ for (const b of BOUNDS) {
82
+ const m = inner.match(b)
83
+ if (m) { inner = m[1]; break }
84
+ }
73
85
  const r = inner.match(RUNG)
74
86
  if (r) {
75
87
  const [, anchor, unit, sign, delta] = r
package/src/preference.ts CHANGED
@@ -46,6 +46,11 @@ export interface Preference {
46
46
  * above 1 opens the display end and tightens the small one.
47
47
  */
48
48
  ratio?: number;
49
+ /**
50
+ * A modular scale for the display rungs — 1.618 for golden, 1.25 for a major
51
+ * third. Absent means the authored ramp, which is the tuned default.
52
+ */
53
+ modular?: number;
49
54
  density?: Density;
50
55
  /** Which face the page is set in. */
51
56
  font?: Face;
@@ -81,6 +86,45 @@ export const TYPE_MAX = 1.4;
81
86
  export const RATIO_MIN = 0.75;
82
87
  export const RATIO_MAX = 1.5;
83
88
 
89
+ /**
90
+ * A MODULAR scale — the classical one, where each display rung is the one below
91
+ * it times a fixed ratio. Golden is 1.618; the musical intervals designers name
92
+ * are 1.2 (minor third), 1.25 (major third), 1.333 (perfect fourth) and 1.5
93
+ * (perfect fifth).
94
+ *
95
+ * This is a different RULE from `ratio`, not another dial on it. `ratio` tunes
96
+ * the contrast of the ramp `tokens/typography.css` authored; this REPLACES that
97
+ * ramp's display half with a geometric one. Naming a golden-ratio preset as a
98
+ * contrast value would have been the dishonest version of this feature.
99
+ *
100
+ * Bounded below 1.05 because a ratio at 1 is not a scale — every display rung
101
+ * collapses onto the one before it — and above 2 because doubling every step
102
+ * puts the fourth rung past a phone's whole width.
103
+ *
104
+ * Golden is exactly 1.618 here and is NOT clamped down to something tamer,
105
+ * because a scale that quietly gives you not-golden when you asked for golden is
106
+ * worse than one that refuses. What makes that safe is the ceiling below: this
107
+ * ramp has EIGHT display rungs and a classical modular scale is used with about
108
+ * four, so at 1.618 the eighth would be 799px. Measured, --text-8xl and
109
+ * --text-9xl are referenced in 15 files across the fleet, so that is a broken
110
+ * page and not a hypothetical.
111
+ */
112
+ export const MODULAR_MIN = 1.05;
113
+ export const MODULAR_MAX = 2;
114
+
115
+ /**
116
+ * The rungs a modular scale REGENERATES, in order, and the one it starts from.
117
+ *
118
+ * It deliberately stops at the display register. A geometric scale through the
119
+ * interface rungs is unusable at any real ratio: at 1.25 from a 14px base the
120
+ * next rungs are 17.5 and 21.9, so the 13px nav label and the 15px lead — the
121
+ * near-linear steps a dense interface is built on — do not exist. `xl` is the
122
+ * anchor because it is the last interface rung, so the display half continues
123
+ * from where the interface ends rather than restarting under it.
124
+ */
125
+ const DISPLAY_RUNGS = ["2xl", "3xl", "4xl", "5xl", "6xl", "7xl", "8xl", "9xl"];
126
+ const MODULAR_ANCHOR_REM = 1.0625; // --text-xl, 17px
127
+
84
128
  /**
85
129
  * Density moves SPACING only, and its range is much tighter than type's.
86
130
  *
@@ -169,6 +213,27 @@ export function vars(p: Preference): Record<string, string> {
169
213
  out["--type-ratio"] = round(clamp(p.ratio, RATIO_MIN, RATIO_MAX));
170
214
  }
171
215
 
216
+ if (typeof p.modular === "number" && Number.isFinite(p.modular)) {
217
+ // Each display rung is the anchor times the ratio to its step, emitted as an
218
+ // explicit property. An INLINE custom property on :root outranks the
219
+ // stylesheet, so these simply replace the authored rungs — nothing has to be
220
+ // unset first, and the interface rungs the sheet declares are untouched
221
+ // because none is named here. It multiplies out with `--type-scale` for free,
222
+ // since every rung is still read through that ramp's own calc.
223
+ const r = clamp(p.modular, MODULAR_MIN, MODULAR_MAX);
224
+ DISPLAY_RUNGS.forEach((rung, i) => {
225
+ const rem = MODULAR_ANCHOR_REM * Math.pow(r, i + 1);
226
+ // THE SAME clamp the authored ramp carries, read from the same two names.
227
+ // A regenerated rung and an authored one must be bounded identically or
228
+ // the bound is a property of which code path produced the value, which is
229
+ // exactly the kind of "it depends" a design system exists to delete. The
230
+ // bounds are declared once, in tokens/typography.css, and referenced here
231
+ // — never restated, so moving one moves both.
232
+ out[`--text-${rung}`] =
233
+ `clamp(var(--text-floor), calc(${round(rem)}rem * var(--type-scale, 1)), var(--text-ceiling))`;
234
+ });
235
+ }
236
+
172
237
  if (p.density && p.density in DENSITY) {
173
238
  out["--density"] = round(DENSITY[p.density]);
174
239
  }
package/src/tokens.gen.ts CHANGED
@@ -19,20 +19,25 @@ export const colors = {
19
19
  'pure-white': '#FFFFFF',
20
20
  'hanzo-black': '#0A0A0B',
21
21
  'hanzo-white': '#FFFFFF',
22
+ 'white-03': 'rgb(255 255 255 / .03)',
22
23
  'white-05': 'rgb(255 255 255 / .05)',
23
24
  'white-06': 'rgb(255 255 255 / .06)',
24
25
  'white-08': 'rgb(255 255 255 / .08)',
25
26
  'white-10': 'rgb(255 255 255 / .10)',
27
+ 'white-14': 'rgb(255 255 255 / .14)',
26
28
  'white-15': 'rgb(255 255 255 / .15)',
27
29
  'white-16': 'rgb(255 255 255 / .16)',
30
+ 'white-18': 'rgb(255 255 255 / .18)',
28
31
  'white-20': 'rgb(255 255 255 / .20)',
29
32
  'white-22': 'rgb(255 255 255 / .22)',
30
33
  'white-30': 'rgb(255 255 255 / .30)',
31
34
  'white-40': 'rgb(255 255 255 / .40)',
35
+ 'white-45': 'rgb(255 255 255 / .45)',
32
36
  'white-55': 'rgb(255 255 255 / .55)',
33
37
  'white-60': 'rgb(255 255 255 / .60)',
34
38
  'white-78': 'rgb(255 255 255 / .78)',
35
39
  'white-80': 'rgb(255 255 255 / .80)',
40
+ 'white-92': 'rgb(255 255 255 / .92)',
36
41
  'background': '#0a0a0a',
37
42
  'foreground': '#fafafa',
38
43
  'card': '#0f0f0f',
@@ -98,6 +103,8 @@ export const colors = {
98
103
  export const typography = {
99
104
  'type-scale': '1',
100
105
  'type-ratio': '1',
106
+ 'text-floor': '0.625rem',
107
+ 'text-ceiling': '30vw',
101
108
  'text-xs': '0.6875rem',
102
109
  'leading-xs': '1rem',
103
110
  'text-sm': '0.8125rem',
@@ -327,20 +334,25 @@ export const cssVars = {
327
334
  '--pure-white': '#FFFFFF',
328
335
  '--hanzo-black': '#0A0A0B',
329
336
  '--hanzo-white': '#FFFFFF',
337
+ '--white-03': 'rgb(255 255 255 / .03)',
330
338
  '--white-05': 'rgb(255 255 255 / .05)',
331
339
  '--white-06': 'rgb(255 255 255 / .06)',
332
340
  '--white-08': 'rgb(255 255 255 / .08)',
333
341
  '--white-10': 'rgb(255 255 255 / .10)',
342
+ '--white-14': 'rgb(255 255 255 / .14)',
334
343
  '--white-15': 'rgb(255 255 255 / .15)',
335
344
  '--white-16': 'rgb(255 255 255 / .16)',
345
+ '--white-18': 'rgb(255 255 255 / .18)',
336
346
  '--white-20': 'rgb(255 255 255 / .20)',
337
347
  '--white-22': 'rgb(255 255 255 / .22)',
338
348
  '--white-30': 'rgb(255 255 255 / .30)',
339
349
  '--white-40': 'rgb(255 255 255 / .40)',
350
+ '--white-45': 'rgb(255 255 255 / .45)',
340
351
  '--white-55': 'rgb(255 255 255 / .55)',
341
352
  '--white-60': 'rgb(255 255 255 / .60)',
342
353
  '--white-78': 'rgb(255 255 255 / .78)',
343
354
  '--white-80': 'rgb(255 255 255 / .80)',
355
+ '--white-92': 'rgb(255 255 255 / .92)',
344
356
  '--background': '#0a0a0a',
345
357
  '--foreground': '#fafafa',
346
358
  '--card': '#0f0f0f',
@@ -402,6 +414,8 @@ export const cssVars = {
402
414
  '--chrome-dot-green': 'rgb(34 197 94 / .6)',
403
415
  '--type-scale': '1',
404
416
  '--type-ratio': '1',
417
+ '--text-floor': '0.625rem',
418
+ '--text-ceiling': '30vw',
405
419
  '--text-xs': '0.6875rem',
406
420
  '--leading-xs': '1rem',
407
421
  '--text-sm': '0.8125rem',
package/styles.css CHANGED
@@ -56,20 +56,39 @@
56
56
  /* ——— the opacity ladder: the real palette ——— */
57
57
  /* Every rung below .30 is a HAIRLINE or a LIFT — the two things this system
58
58
  draws with. The rungs are the reference's own values, not a rounded ramp. */
59
+ /* The five rungs marked (chrome) below were being retyped as raw rgba in
60
+ @hanzogui/shell's theme.ts, which is the shared bar every Hanzo property
61
+ mounts. That is the same defect this ladder exists to end — a value in two
62
+ places drifts, and this pair already had: the shell's resting hairline sat
63
+ at .06 against this file's .10 for long enough that the bar and the page
64
+ under it drew different hairlines. They are rungs here now, and the shell
65
+ references them, so the chrome has no colour of its own left to drift.
66
+
67
+ They are the shell's OWN measured values, added at the alpha it already
68
+ ships. Snapping them to neighbouring rungs instead would have been a
69
+ silent redesign of the estate's chrome, and two of them carry decisions
70
+ that a round number would have destroyed: .92 is ink deliberately short of
71
+ the .98 hover so a hover still reads as a response, and .03 is the one
72
+ lift that must stay below the .05 hairline it sits inside. */
73
+ --white-03:rgb(255 255 255 / .03); /* (chrome) the raised fill */
59
74
  --white-05:rgb(255 255 255 / .05);
60
75
  --white-06:rgb(255 255 255 / .06);
61
76
  --white-08:rgb(255 255 255 / .08);
62
77
  --white-10:rgb(255 255 255 / .10);
78
+ --white-14:rgb(255 255 255 / .14); /* (chrome) selection wash */
63
79
  --white-15:rgb(255 255 255 / .15);
64
80
  --white-16:rgb(255 255 255 / .16);
81
+ --white-18:rgb(255 255 255 / .18); /* (chrome) selection tint */
65
82
  --white-20:rgb(255 255 255 / .20);
66
83
  --white-22:rgb(255 255 255 / .22);
67
84
  --white-30:rgb(255 255 255 / .30);
68
85
  --white-40:rgb(255 255 255 / .40);
86
+ --white-45:rgb(255 255 255 / .45); /* (chrome) the dimmest ink */
69
87
  --white-55:rgb(255 255 255 / .55);
70
88
  --white-60:rgb(255 255 255 / .60);
71
89
  --white-78:rgb(255 255 255 / .78);
72
90
  --white-80:rgb(255 255 255 / .80);
91
+ --white-92:rgb(255 255 255 / .92); /* (chrome) body ink */
73
92
 
74
93
  /* ——— semantic aliases (dark, the default) ——— */
75
94
  --background:#0a0a0a;
@@ -316,24 +335,36 @@
316
335
  A rung is one place, so a rung added later needs a delta and nothing else. */
317
336
  --type-ratio:1;
318
337
 
319
- /* --text-xs/sm carry a floor because the knobs COMPOUND: a flat ratio shrinks
320
- the small end while a small scale shrinks everything, and the two multiply.
321
- Clamping each knob's range separately cannot fix that (0.85 x 0.9 is inside
322
- both bounds and lands at 8.5px); flooring the RESULT can, so no combination
323
- of a person's settings and an org's renders its own labels illegible. */
324
- --text-xs:max(0.625rem, calc((0.875rem - 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1))); --leading-xs:calc((1.35rem - 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 11px — eyebrows / section labels */
325
- --text-sm:max(0.6875rem, calc((0.875rem - 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1))); --leading-sm:calc((1.35rem - 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 13px nav labels, dense body */
326
- --text-base:calc(0.875rem * var(--type-scale, 1)); --leading-base:calc(1.35rem * var(--type-scale, 1)); /* 14px — the ANCHOR: the rung the ratio pivots on, so it carries no ratio term */
327
- --text-lg:calc((0.875rem + 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-lg:calc((1.35rem + 0.05rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 15px */
328
- --text-xl:calc((0.875rem + 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-xl:calc((1.35rem + 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 17px */
329
- --text-2xl:calc((0.875rem + 0.4375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-2xl:calc((1.35rem + 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 21px */
330
- --text-3xl:calc((0.875rem + 0.75rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-3xl:calc((1.35rem + 0.6rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 26px */
331
- --text-4xl:calc((0.875rem + 1.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-4xl:calc((1.35rem + 0.9rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 32px */
332
- --text-5xl:calc((0.875rem + 1.625rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-5xl:1.05; /* 40px display leading is unitless, so it tracks its own size */
333
- --text-6xl:calc((0.875rem + 2.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-6xl:1; /* 52px */
334
- --text-7xl:calc((0.875rem + 3.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-7xl:1; /* 64px */
335
- --text-8xl:calc((0.875rem + 4.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-8xl:1; /* 84px */
336
- --text-9xl:calc((0.875rem + 6.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-9xl:1; /* 112px */
338
+ /* EVERY rung is bounded at BOTH ends, by one rule, because the knobs COMPOUND
339
+ and they compound in both directions. Clamping each knob's range separately
340
+ cannot fix either end 0.85 x 0.9 is inside both bounds and lands at 8.5px
341
+ so the RESULT is what carries the bound:
342
+
343
+ --text-floor 10px below this a label is not small, it is unreadable
344
+ --text-ceiling 30vw above this a heading is not type, it is a texture
345
+
346
+ The ceiling is in the VIEWPORT's units because that is what "too big" means:
347
+ measured, the tallest rung (--text-9xl, 112px) is 28.7vw on a 390px phone
348
+ and 7.8vw at 1440, so 30vw binds no published value at any real screen and
349
+ catches only a scale that has run away. Both bounds are stated once here and
350
+ `vars()` emits the same clamp for a modular scale, so a regenerated rung and
351
+ an authored one are bounded identicallythe rule does not depend on which
352
+ produced the value. */
353
+ --text-floor:0.625rem;
354
+ --text-ceiling:30vw;
355
+ --text-xs:clamp(var(--text-floor), calc((0.875rem - 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-xs:calc((1.35rem - 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 11px — eyebrows / section labels */
356
+ --text-sm:clamp(var(--text-floor), calc((0.875rem - 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-sm:calc((1.35rem - 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 13px — nav labels, dense body */
357
+ --text-base:clamp(var(--text-floor), calc(0.875rem * var(--type-scale, 1)), var(--text-ceiling)); --leading-base:calc(1.35rem * var(--type-scale, 1)); /* 14px — the ANCHOR: the rung the ratio pivots on, so it carries no ratio term */
358
+ --text-lg:clamp(var(--text-floor), calc((0.875rem + 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-lg:calc((1.35rem + 0.05rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 15px */
359
+ --text-xl:clamp(var(--text-floor), calc((0.875rem + 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-xl:calc((1.35rem + 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 17px */
360
+ --text-2xl:clamp(var(--text-floor), calc((0.875rem + 0.4375rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-2xl:calc((1.35rem + 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 21px */
361
+ --text-3xl:clamp(var(--text-floor), calc((0.875rem + 0.75rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-3xl:calc((1.35rem + 0.6rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 26px */
362
+ --text-4xl:clamp(var(--text-floor), calc((0.875rem + 1.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-4xl:calc((1.35rem + 0.9rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 32px */
363
+ --text-5xl:clamp(var(--text-floor), calc((0.875rem + 1.625rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-5xl:1.05; /* 40px — display leading is unitless, so it tracks its own size */
364
+ --text-6xl:clamp(var(--text-floor), calc((0.875rem + 2.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-6xl:1; /* 52px */
365
+ --text-7xl:clamp(var(--text-floor), calc((0.875rem + 3.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-7xl:1; /* 64px */
366
+ --text-8xl:clamp(var(--text-floor), calc((0.875rem + 4.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-8xl:1; /* 84px */
367
+ --text-9xl:clamp(var(--text-floor), calc((0.875rem + 6.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-9xl:1; /* 112px */
337
368
 
338
369
  /* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
339
370
  Hanzo Studio address the ramp as --font-size-*; both names are one value, so
package/tailwind.css CHANGED
@@ -79,20 +79,39 @@
79
79
  /* ——— the opacity ladder: the real palette ——— */
80
80
  /* Every rung below .30 is a HAIRLINE or a LIFT — the two things this system
81
81
  draws with. The rungs are the reference's own values, not a rounded ramp. */
82
+ /* The five rungs marked (chrome) below were being retyped as raw rgba in
83
+ @hanzogui/shell's theme.ts, which is the shared bar every Hanzo property
84
+ mounts. That is the same defect this ladder exists to end — a value in two
85
+ places drifts, and this pair already had: the shell's resting hairline sat
86
+ at .06 against this file's .10 for long enough that the bar and the page
87
+ under it drew different hairlines. They are rungs here now, and the shell
88
+ references them, so the chrome has no colour of its own left to drift.
89
+
90
+ They are the shell's OWN measured values, added at the alpha it already
91
+ ships. Snapping them to neighbouring rungs instead would have been a
92
+ silent redesign of the estate's chrome, and two of them carry decisions
93
+ that a round number would have destroyed: .92 is ink deliberately short of
94
+ the .98 hover so a hover still reads as a response, and .03 is the one
95
+ lift that must stay below the .05 hairline it sits inside. */
96
+ --white-03:rgb(255 255 255 / .03); /* (chrome) the raised fill */
82
97
  --white-05:rgb(255 255 255 / .05);
83
98
  --white-06:rgb(255 255 255 / .06);
84
99
  --white-08:rgb(255 255 255 / .08);
85
100
  --white-10:rgb(255 255 255 / .10);
101
+ --white-14:rgb(255 255 255 / .14); /* (chrome) selection wash */
86
102
  --white-15:rgb(255 255 255 / .15);
87
103
  --white-16:rgb(255 255 255 / .16);
104
+ --white-18:rgb(255 255 255 / .18); /* (chrome) selection tint */
88
105
  --white-20:rgb(255 255 255 / .20);
89
106
  --white-22:rgb(255 255 255 / .22);
90
107
  --white-30:rgb(255 255 255 / .30);
91
108
  --white-40:rgb(255 255 255 / .40);
109
+ --white-45:rgb(255 255 255 / .45); /* (chrome) the dimmest ink */
92
110
  --white-55:rgb(255 255 255 / .55);
93
111
  --white-60:rgb(255 255 255 / .60);
94
112
  --white-78:rgb(255 255 255 / .78);
95
113
  --white-80:rgb(255 255 255 / .80);
114
+ --white-92:rgb(255 255 255 / .92); /* (chrome) body ink */
96
115
 
97
116
  /* ——— semantic aliases (dark, the default) ——— */
98
117
  --background:#0a0a0a;
@@ -339,24 +358,36 @@
339
358
  A rung is one place, so a rung added later needs a delta and nothing else. */
340
359
  --type-ratio:1;
341
360
 
342
- /* --text-xs/sm carry a floor because the knobs COMPOUND: a flat ratio shrinks
343
- the small end while a small scale shrinks everything, and the two multiply.
344
- Clamping each knob's range separately cannot fix that (0.85 x 0.9 is inside
345
- both bounds and lands at 8.5px); flooring the RESULT can, so no combination
346
- of a person's settings and an org's renders its own labels illegible. */
347
- --text-xs:max(0.625rem, calc((0.875rem - 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1))); --leading-xs:calc((1.35rem - 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 11px — eyebrows / section labels */
348
- --text-sm:max(0.6875rem, calc((0.875rem - 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1))); --leading-sm:calc((1.35rem - 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 13px nav labels, dense body */
349
- --text-base:calc(0.875rem * var(--type-scale, 1)); --leading-base:calc(1.35rem * var(--type-scale, 1)); /* 14px — the ANCHOR: the rung the ratio pivots on, so it carries no ratio term */
350
- --text-lg:calc((0.875rem + 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-lg:calc((1.35rem + 0.05rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 15px */
351
- --text-xl:calc((0.875rem + 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-xl:calc((1.35rem + 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 17px */
352
- --text-2xl:calc((0.875rem + 0.4375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-2xl:calc((1.35rem + 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 21px */
353
- --text-3xl:calc((0.875rem + 0.75rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-3xl:calc((1.35rem + 0.6rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 26px */
354
- --text-4xl:calc((0.875rem + 1.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-4xl:calc((1.35rem + 0.9rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 32px */
355
- --text-5xl:calc((0.875rem + 1.625rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-5xl:1.05; /* 40px display leading is unitless, so it tracks its own size */
356
- --text-6xl:calc((0.875rem + 2.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-6xl:1; /* 52px */
357
- --text-7xl:calc((0.875rem + 3.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-7xl:1; /* 64px */
358
- --text-8xl:calc((0.875rem + 4.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-8xl:1; /* 84px */
359
- --text-9xl:calc((0.875rem + 6.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-9xl:1; /* 112px */
361
+ /* EVERY rung is bounded at BOTH ends, by one rule, because the knobs COMPOUND
362
+ and they compound in both directions. Clamping each knob's range separately
363
+ cannot fix either end 0.85 x 0.9 is inside both bounds and lands at 8.5px
364
+ so the RESULT is what carries the bound:
365
+
366
+ --text-floor 10px below this a label is not small, it is unreadable
367
+ --text-ceiling 30vw above this a heading is not type, it is a texture
368
+
369
+ The ceiling is in the VIEWPORT's units because that is what "too big" means:
370
+ measured, the tallest rung (--text-9xl, 112px) is 28.7vw on a 390px phone
371
+ and 7.8vw at 1440, so 30vw binds no published value at any real screen and
372
+ catches only a scale that has run away. Both bounds are stated once here and
373
+ `vars()` emits the same clamp for a modular scale, so a regenerated rung and
374
+ an authored one are bounded identicallythe rule does not depend on which
375
+ produced the value. */
376
+ --text-floor:0.625rem;
377
+ --text-ceiling:30vw;
378
+ --text-xs:clamp(var(--text-floor), calc((0.875rem - 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-xs:calc((1.35rem - 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 11px — eyebrows / section labels */
379
+ --text-sm:clamp(var(--text-floor), calc((0.875rem - 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-sm:calc((1.35rem - 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 13px — nav labels, dense body */
380
+ --text-base:clamp(var(--text-floor), calc(0.875rem * var(--type-scale, 1)), var(--text-ceiling)); --leading-base:calc(1.35rem * var(--type-scale, 1)); /* 14px — the ANCHOR: the rung the ratio pivots on, so it carries no ratio term */
381
+ --text-lg:clamp(var(--text-floor), calc((0.875rem + 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-lg:calc((1.35rem + 0.05rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 15px */
382
+ --text-xl:clamp(var(--text-floor), calc((0.875rem + 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-xl:calc((1.35rem + 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 17px */
383
+ --text-2xl:clamp(var(--text-floor), calc((0.875rem + 0.4375rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-2xl:calc((1.35rem + 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 21px */
384
+ --text-3xl:clamp(var(--text-floor), calc((0.875rem + 0.75rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-3xl:calc((1.35rem + 0.6rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 26px */
385
+ --text-4xl:clamp(var(--text-floor), calc((0.875rem + 1.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-4xl:calc((1.35rem + 0.9rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 32px */
386
+ --text-5xl:clamp(var(--text-floor), calc((0.875rem + 1.625rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-5xl:1.05; /* 40px — display leading is unitless, so it tracks its own size */
387
+ --text-6xl:clamp(var(--text-floor), calc((0.875rem + 2.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-6xl:1; /* 52px */
388
+ --text-7xl:clamp(var(--text-floor), calc((0.875rem + 3.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-7xl:1; /* 64px */
389
+ --text-8xl:clamp(var(--text-floor), calc((0.875rem + 4.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-8xl:1; /* 84px */
390
+ --text-9xl:clamp(var(--text-floor), calc((0.875rem + 6.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-9xl:1; /* 112px */
360
391
 
361
392
  /* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
362
393
  Hanzo Studio address the ramp as --font-size-*; both names are one value, so
package/tokens/colors.css CHANGED
@@ -41,20 +41,39 @@
41
41
  /* ——— the opacity ladder: the real palette ——— */
42
42
  /* Every rung below .30 is a HAIRLINE or a LIFT — the two things this system
43
43
  draws with. The rungs are the reference's own values, not a rounded ramp. */
44
+ /* The five rungs marked (chrome) below were being retyped as raw rgba in
45
+ @hanzogui/shell's theme.ts, which is the shared bar every Hanzo property
46
+ mounts. That is the same defect this ladder exists to end — a value in two
47
+ places drifts, and this pair already had: the shell's resting hairline sat
48
+ at .06 against this file's .10 for long enough that the bar and the page
49
+ under it drew different hairlines. They are rungs here now, and the shell
50
+ references them, so the chrome has no colour of its own left to drift.
51
+
52
+ They are the shell's OWN measured values, added at the alpha it already
53
+ ships. Snapping them to neighbouring rungs instead would have been a
54
+ silent redesign of the estate's chrome, and two of them carry decisions
55
+ that a round number would have destroyed: .92 is ink deliberately short of
56
+ the .98 hover so a hover still reads as a response, and .03 is the one
57
+ lift that must stay below the .05 hairline it sits inside. */
58
+ --white-03:rgb(255 255 255 / .03); /* (chrome) the raised fill */
44
59
  --white-05:rgb(255 255 255 / .05);
45
60
  --white-06:rgb(255 255 255 / .06);
46
61
  --white-08:rgb(255 255 255 / .08);
47
62
  --white-10:rgb(255 255 255 / .10);
63
+ --white-14:rgb(255 255 255 / .14); /* (chrome) selection wash */
48
64
  --white-15:rgb(255 255 255 / .15);
49
65
  --white-16:rgb(255 255 255 / .16);
66
+ --white-18:rgb(255 255 255 / .18); /* (chrome) selection tint */
50
67
  --white-20:rgb(255 255 255 / .20);
51
68
  --white-22:rgb(255 255 255 / .22);
52
69
  --white-30:rgb(255 255 255 / .30);
53
70
  --white-40:rgb(255 255 255 / .40);
71
+ --white-45:rgb(255 255 255 / .45); /* (chrome) the dimmest ink */
54
72
  --white-55:rgb(255 255 255 / .55);
55
73
  --white-60:rgb(255 255 255 / .60);
56
74
  --white-78:rgb(255 255 255 / .78);
57
75
  --white-80:rgb(255 255 255 / .80);
76
+ --white-92:rgb(255 255 255 / .92); /* (chrome) body ink */
58
77
 
59
78
  /* ——— semantic aliases (dark, the default) ——— */
60
79
  --background:#0a0a0a;
@@ -31,24 +31,36 @@
31
31
  A rung is one place, so a rung added later needs a delta and nothing else. */
32
32
  --type-ratio:1;
33
33
 
34
- /* --text-xs/sm carry a floor because the knobs COMPOUND: a flat ratio shrinks
35
- the small end while a small scale shrinks everything, and the two multiply.
36
- Clamping each knob's range separately cannot fix that (0.85 x 0.9 is inside
37
- both bounds and lands at 8.5px); flooring the RESULT can, so no combination
38
- of a person's settings and an org's renders its own labels illegible. */
39
- --text-xs:max(0.625rem, calc((0.875rem - 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1))); --leading-xs:calc((1.35rem - 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 11px — eyebrows / section labels */
40
- --text-sm:max(0.6875rem, calc((0.875rem - 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1))); --leading-sm:calc((1.35rem - 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 13px nav labels, dense body */
41
- --text-base:calc(0.875rem * var(--type-scale, 1)); --leading-base:calc(1.35rem * var(--type-scale, 1)); /* 14px — the ANCHOR: the rung the ratio pivots on, so it carries no ratio term */
42
- --text-lg:calc((0.875rem + 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-lg:calc((1.35rem + 0.05rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 15px */
43
- --text-xl:calc((0.875rem + 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-xl:calc((1.35rem + 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 17px */
44
- --text-2xl:calc((0.875rem + 0.4375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-2xl:calc((1.35rem + 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 21px */
45
- --text-3xl:calc((0.875rem + 0.75rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-3xl:calc((1.35rem + 0.6rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 26px */
46
- --text-4xl:calc((0.875rem + 1.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-4xl:calc((1.35rem + 0.9rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 32px */
47
- --text-5xl:calc((0.875rem + 1.625rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-5xl:1.05; /* 40px display leading is unitless, so it tracks its own size */
48
- --text-6xl:calc((0.875rem + 2.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-6xl:1; /* 52px */
49
- --text-7xl:calc((0.875rem + 3.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-7xl:1; /* 64px */
50
- --text-8xl:calc((0.875rem + 4.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-8xl:1; /* 84px */
51
- --text-9xl:calc((0.875rem + 6.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-9xl:1; /* 112px */
34
+ /* EVERY rung is bounded at BOTH ends, by one rule, because the knobs COMPOUND
35
+ and they compound in both directions. Clamping each knob's range separately
36
+ cannot fix either end 0.85 x 0.9 is inside both bounds and lands at 8.5px
37
+ so the RESULT is what carries the bound:
38
+
39
+ --text-floor 10px below this a label is not small, it is unreadable
40
+ --text-ceiling 30vw above this a heading is not type, it is a texture
41
+
42
+ The ceiling is in the VIEWPORT's units because that is what "too big" means:
43
+ measured, the tallest rung (--text-9xl, 112px) is 28.7vw on a 390px phone
44
+ and 7.8vw at 1440, so 30vw binds no published value at any real screen and
45
+ catches only a scale that has run away. Both bounds are stated once here and
46
+ `vars()` emits the same clamp for a modular scale, so a regenerated rung and
47
+ an authored one are bounded identicallythe rule does not depend on which
48
+ produced the value. */
49
+ --text-floor:0.625rem;
50
+ --text-ceiling:30vw;
51
+ --text-xs:clamp(var(--text-floor), calc((0.875rem - 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-xs:calc((1.35rem - 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 11px — eyebrows / section labels */
52
+ --text-sm:clamp(var(--text-floor), calc((0.875rem - 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-sm:calc((1.35rem - 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 13px — nav labels, dense body */
53
+ --text-base:clamp(var(--text-floor), calc(0.875rem * var(--type-scale, 1)), var(--text-ceiling)); --leading-base:calc(1.35rem * var(--type-scale, 1)); /* 14px — the ANCHOR: the rung the ratio pivots on, so it carries no ratio term */
54
+ --text-lg:clamp(var(--text-floor), calc((0.875rem + 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-lg:calc((1.35rem + 0.05rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 15px */
55
+ --text-xl:clamp(var(--text-floor), calc((0.875rem + 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-xl:calc((1.35rem + 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 17px */
56
+ --text-2xl:clamp(var(--text-floor), calc((0.875rem + 0.4375rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-2xl:calc((1.35rem + 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 21px */
57
+ --text-3xl:clamp(var(--text-floor), calc((0.875rem + 0.75rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-3xl:calc((1.35rem + 0.6rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 26px */
58
+ --text-4xl:clamp(var(--text-floor), calc((0.875rem + 1.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-4xl:calc((1.35rem + 0.9rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 32px */
59
+ --text-5xl:clamp(var(--text-floor), calc((0.875rem + 1.625rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-5xl:1.05; /* 40px — display leading is unitless, so it tracks its own size */
60
+ --text-6xl:clamp(var(--text-floor), calc((0.875rem + 2.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-6xl:1; /* 52px */
61
+ --text-7xl:clamp(var(--text-floor), calc((0.875rem + 3.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-7xl:1; /* 64px */
62
+ --text-8xl:clamp(var(--text-floor), calc((0.875rem + 4.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-8xl:1; /* 84px */
63
+ --text-9xl:clamp(var(--text-floor), calc((0.875rem + 6.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-9xl:1; /* 112px */
52
64
 
53
65
  /* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
54
66
  Hanzo Studio address the ramp as --font-size-*; both names are one value, so