@hanzo/design 0.5.8 → 0.5.9

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.
@@ -29,6 +29,12 @@ export type Density = "compact" | "default" | "comfortable";
29
29
  export interface Preference {
30
30
  /** Multiplier on the type ramp. 1 is the published scale. */
31
31
  type?: number;
32
+ /**
33
+ * How far APART the rungs sit — the ramp's contrast, where `type` is its size.
34
+ * 1 is the published scale; below 1 flattens it toward a uniform register,
35
+ * above 1 opens the display end and tightens the small one.
36
+ */
37
+ ratio?: number;
32
38
  density?: Density;
33
39
  /** A CSS colour for --primary / --accent. Rejected unless it is one. */
34
40
  accent?: string;
@@ -44,6 +50,19 @@ export interface Preference {
44
50
  */
45
51
  export declare const TYPE_MIN = 0.85;
46
52
  export declare const TYPE_MAX = 1.4;
53
+ /**
54
+ * The ratio's bounds are looser than type's, because the ramp defends its own
55
+ * floor.
56
+ *
57
+ * `tokens/typography.css` floors --text-xs/sm with `max()`, so the failure mode
58
+ * that forces type's tight clamp — a knob quietly rendering 9px labels — cannot
59
+ * happen on this axis however it combines with type. What is left to bound is
60
+ * only whether the ramp still READS as a ramp: at 0.75 the app register is
61
+ * within a hair of uniform, and past 1.5 a page's h2 has left its own body text
62
+ * behind entirely.
63
+ */
64
+ export declare const RATIO_MIN = 0.75;
65
+ export declare const RATIO_MAX = 1.5;
47
66
  /**
48
67
  * Is this a colour, or is it something being smuggled into a style attribute?
49
68
  *
@@ -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,MAAM,WAAW,UAAU;IACzB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,OAAO,CAAC;AAC7B,eAAO,MAAM,QAAQ,MAAM,CAAC;AAqB5B;;;;;;;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,CAmB1D;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,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,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;AAqB7B;;;;;;;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,CAuB1D;AAED,2EAA2E;AAC3E,wBAAgB,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,SAAc,GAAG,MAAM,CAKjE"}
@@ -36,6 +36,19 @@
36
36
  */
37
37
  export const TYPE_MIN = 0.85;
38
38
  export const TYPE_MAX = 1.4;
39
+ /**
40
+ * The ratio's bounds are looser than type's, because the ramp defends its own
41
+ * floor.
42
+ *
43
+ * `tokens/typography.css` floors --text-xs/sm with `max()`, so the failure mode
44
+ * that forces type's tight clamp — a knob quietly rendering 9px labels — cannot
45
+ * happen on this axis however it combines with type. What is left to bound is
46
+ * only whether the ramp still READS as a ramp: at 0.75 the app register is
47
+ * within a hair of uniform, and past 1.5 a page's h2 has left its own body text
48
+ * behind entirely.
49
+ */
50
+ export const RATIO_MIN = 0.75;
51
+ export const RATIO_MAX = 1.5;
39
52
  /**
40
53
  * Density moves SPACING only, and its range is much tighter than type's.
41
54
  *
@@ -81,6 +94,9 @@ export function vars(p) {
81
94
  if (typeof p.type === "number" && Number.isFinite(p.type)) {
82
95
  out["--type-scale"] = round(clamp(p.type, TYPE_MIN, TYPE_MAX));
83
96
  }
97
+ if (typeof p.ratio === "number" && Number.isFinite(p.ratio)) {
98
+ out["--type-ratio"] = round(clamp(p.ratio, RATIO_MIN, RATIO_MAX));
99
+ }
84
100
  if (p.density && p.density in DENSITY) {
85
101
  out["--density"] = round(DENSITY[p.density]);
86
102
  }
@@ -92,6 +92,7 @@ export declare const colors: {
92
92
  /** typography tokens (from tokens/typography.css). Values are raw CSS. */
93
93
  export declare const typography: {
94
94
  readonly 'type-scale': "1";
95
+ readonly 'type-ratio': "1";
95
96
  readonly 'text-xs': "0.6875rem";
96
97
  readonly 'leading-xs': "1rem";
97
98
  readonly 'text-sm': "0.8125rem";
@@ -384,6 +385,7 @@ export declare const cssVars: {
384
385
  readonly '--chrome-dot-yellow': "rgb(234 179 8 / .6)";
385
386
  readonly '--chrome-dot-green': "rgb(34 197 94 / .6)";
386
387
  readonly '--type-scale': "1";
388
+ readonly '--type-ratio': "1";
387
389
  readonly '--text-xs': "0.6875rem";
388
390
  readonly '--leading-xs': "1rem";
389
391
  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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgEb,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2QV,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
@@ -95,6 +95,7 @@ export const colors = {
95
95
  /** typography tokens (from tokens/typography.css). Values are raw CSS. */
96
96
  export const typography = {
97
97
  'type-scale': '1',
98
+ 'type-ratio': '1',
98
99
  'text-xs': '0.6875rem',
99
100
  'leading-xs': '1rem',
100
101
  'text-sm': '0.8125rem',
@@ -387,6 +388,7 @@ export const cssVars = {
387
388
  '--chrome-dot-yellow': 'rgb(234 179 8 / .6)',
388
389
  '--chrome-dot-green': 'rgb(34 197 94 / .6)',
389
390
  '--type-scale': '1',
391
+ '--type-ratio': '1',
390
392
  '--text-xs': '0.6875rem',
391
393
  '--leading-xs': '1rem',
392
394
  '--text-sm': '0.8125rem',
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@hanzo/design",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
4
4
  "packageManager": "pnpm@11.17.0",
5
- "description": "Hanzo Design System \u2014 monochrome, dark-default tokens + components + brand assets, the single source of truth for every Hanzo surface. CSS + typed programmatic tokens.",
5
+ "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
6
  "license": "MIT OR Apache-2.0",
7
7
  "repository": {
8
8
  "type": "git",
@@ -52,9 +52,33 @@ function parse(css) {
52
52
  * The shape is exact (`calc(<value> * var(--<knob>, 1))`) and only that shape is
53
53
  * unwrapped; a genuinely composite calc (`calc(var(--fold) - 1px)`) is untouched.
54
54
  * check-tokens.mjs holds it: no emitted value may name a knob.
55
+ *
56
+ * The type ramp adds a second knob INSIDE the first — a rung is
57
+ * `calc((<anchor> ± <delta> * var(--type-ratio, 1)) * var(--type-scale, 1))`, and
58
+ * the small rungs wrap that in a `max()` floor. Both are peeled here, in the
59
+ * order they nest, and the arithmetic is then done at the published defaults
60
+ * (both knobs 1) so the table carries exactly the rung the sheet renders — which
61
+ * is the same number it carried before the ratio existed. The floor is DROPPED
62
+ * rather than evaluated: it exists to catch a person's settings compounding, and
63
+ * at the defaults it never binds, so keeping it would put a guard in the table
64
+ * against a knob the table does not have.
55
65
  */
56
66
  const KNOB = /^calc\(\s*([^*]+?)\s*\*\s*var\(--(?:type-scale|density),\s*1\)\s*\)$/
57
- const scale = (v) => v.match(KNOB)?.[1] ?? v
67
+ const FLOOR = /^max\(\s*[\d.]+[a-z%]*\s*,\s*(calc\(.+\))\s*\)$/
68
+ const RUNG =
69
+ /^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
+
71
+ const scale = (v) => {
72
+ const inner = v.match(FLOOR)?.[1] ?? v
73
+ const r = inner.match(RUNG)
74
+ if (r) {
75
+ const [, anchor, unit, sign, delta] = r
76
+ const n = sign === '-' ? Number(anchor) - Number(delta) : Number(anchor) + Number(delta)
77
+ // 4dp, then trim — 0.875 + 0.75 must read `1.625rem`, not `1.6250000000000002rem`.
78
+ return `${Number(n.toFixed(4))}${unit}`
79
+ }
80
+ return inner.match(KNOB)?.[1] ?? inner
81
+ }
58
82
 
59
83
  const groupMaps = {} // groupName -> Map(name->value), first-wins
60
84
  const flatMap = new Map() // '--name' -> value, first-wins
package/src/preference.ts CHANGED
@@ -31,6 +31,12 @@ export type Density = "compact" | "default" | "comfortable";
31
31
  export interface Preference {
32
32
  /** Multiplier on the type ramp. 1 is the published scale. */
33
33
  type?: number;
34
+ /**
35
+ * How far APART the rungs sit — the ramp's contrast, where `type` is its size.
36
+ * 1 is the published scale; below 1 flattens it toward a uniform register,
37
+ * above 1 opens the display end and tightens the small one.
38
+ */
39
+ ratio?: number;
34
40
  density?: Density;
35
41
  /** A CSS colour for --primary / --accent. Rejected unless it is one. */
36
42
  accent?: string;
@@ -48,6 +54,20 @@ export interface Preference {
48
54
  export const TYPE_MIN = 0.85;
49
55
  export const TYPE_MAX = 1.4;
50
56
 
57
+ /**
58
+ * The ratio's bounds are looser than type's, because the ramp defends its own
59
+ * floor.
60
+ *
61
+ * `tokens/typography.css` floors --text-xs/sm with `max()`, so the failure mode
62
+ * that forces type's tight clamp — a knob quietly rendering 9px labels — cannot
63
+ * happen on this axis however it combines with type. What is left to bound is
64
+ * only whether the ramp still READS as a ramp: at 0.75 the app register is
65
+ * within a hair of uniform, and past 1.5 a page's h2 has left its own body text
66
+ * behind entirely.
67
+ */
68
+ export const RATIO_MIN = 0.75;
69
+ export const RATIO_MAX = 1.5;
70
+
51
71
  /**
52
72
  * Density moves SPACING only, and its range is much tighter than type's.
53
73
  *
@@ -99,6 +119,10 @@ export function vars(p: Preference): Record<string, string> {
99
119
  out["--type-scale"] = round(clamp(p.type, TYPE_MIN, TYPE_MAX));
100
120
  }
101
121
 
122
+ if (typeof p.ratio === "number" && Number.isFinite(p.ratio)) {
123
+ out["--type-ratio"] = round(clamp(p.ratio, RATIO_MIN, RATIO_MAX));
124
+ }
125
+
102
126
  if (p.density && p.density in DENSITY) {
103
127
  out["--density"] = round(DENSITY[p.density]);
104
128
  }
package/src/tokens.gen.ts CHANGED
@@ -97,6 +97,7 @@ export const colors = {
97
97
  /** typography tokens (from tokens/typography.css). Values are raw CSS. */
98
98
  export const typography = {
99
99
  'type-scale': '1',
100
+ 'type-ratio': '1',
100
101
  'text-xs': '0.6875rem',
101
102
  'leading-xs': '1rem',
102
103
  'text-sm': '0.8125rem',
@@ -400,6 +401,7 @@ export const cssVars = {
400
401
  '--chrome-dot-yellow': 'rgb(234 179 8 / .6)',
401
402
  '--chrome-dot-green': 'rgb(34 197 94 / .6)',
402
403
  '--type-scale': '1',
404
+ '--type-ratio': '1',
403
405
  '--text-xs': '0.6875rem',
404
406
  '--leading-xs': '1rem',
405
407
  '--text-sm': '0.8125rem',
package/styles.css CHANGED
@@ -296,19 +296,44 @@
296
296
  @hanzo/design's `vars({type})` emits exactly this name. */
297
297
  --type-scale:1;
298
298
 
299
- --text-xs:calc(0.6875rem * var(--type-scale, 1)); --leading-xs:calc(1rem * var(--type-scale, 1)); /* 11px eyebrows / section labels */
300
- --text-sm:calc(0.8125rem * var(--type-scale, 1)); --leading-sm:calc(1.15rem * var(--type-scale, 1)); /* 13px nav labels, dense body */
301
- --text-base:calc(0.875rem * var(--type-scale, 1)); --leading-base:calc(1.35rem * var(--type-scale, 1)); /* 14px base app text (was 16px) */
302
- --text-lg:calc(0.9375rem * var(--type-scale, 1)); --leading-lg:calc(1.4rem * var(--type-scale, 1)); /* 15px */
303
- --text-xl:calc(1.0625rem * var(--type-scale, 1)); --leading-xl:calc(1.55rem * var(--type-scale, 1)); /* 17px */
304
- --text-2xl:calc(1.3125rem * var(--type-scale, 1)); --leading-2xl:calc(1.7rem * var(--type-scale, 1)); /* 21px */
305
- --text-3xl:calc(1.625rem * var(--type-scale, 1)); --leading-3xl:calc(1.95rem * var(--type-scale, 1)); /* 26px */
306
- --text-4xl:calc(2rem * var(--type-scale, 1)); --leading-4xl:calc(2.25rem * var(--type-scale, 1)); /* 32px */
307
- --text-5xl:calc(2.5rem * var(--type-scale, 1)); --leading-5xl:1.05; /* 40px */
308
- --text-6xl:calc(3.25rem * var(--type-scale, 1)); --leading-6xl:1; /* 52px */
309
- --text-7xl:calc(4rem * var(--type-scale, 1)); --leading-7xl:1; /* 64px */
310
- --text-8xl:calc(5.25rem * var(--type-scale, 1)); --leading-8xl:1; /* 84px */
311
- --text-9xl:calc(7rem * var(--type-scale, 1)); --leading-9xl:1; /* 112px */
299
+ /* The CONTRAST knob how far apart the rungs sit, where --type-scale is how
300
+ big they all are. Two different questions, and only the first one was
301
+ askable: a surface could take the whole ramp up 15% but could not flatten a
302
+ dense table's steps or open up a marketing page's, so both settled for the
303
+ app register's tight default.
304
+
305
+ It is authored as a TRANSFORM and not as a generated ramp, which is the
306
+ whole point. `base + (authored - base) * ratio` keeps each rung's authored
307
+ value as the INPUT, so ratio 1 reduces to exactly the published number by
308
+ algebra rather than by care — the deltas below are just `authored - 0.875`.
309
+ The alternative, deriving each rung as base * ratio^n, would restate the
310
+ ramp in a second place, and this scale is not geometric anyway: the app
311
+ register (11/13/14/15/17) is near-linear on purpose and a ratio applied to
312
+ it would erase the 1px steps the compact register is FOR. That drift is not
313
+ hypothetical here — see src/preference.ts, where a recomputed copy of this
314
+ ramp shipped `lg: 16px` against this file's 15px.
315
+
316
+ A rung is one place, so a rung added later needs a delta and nothing else. */
317
+ --type-ratio:1;
318
+
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 */
312
337
 
313
338
  /* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
314
339
  Hanzo Studio address the ramp as --font-size-*; both names are one value, so
package/tailwind.css CHANGED
@@ -319,19 +319,44 @@
319
319
  @hanzo/design's `vars({type})` emits exactly this name. */
320
320
  --type-scale:1;
321
321
 
322
- --text-xs:calc(0.6875rem * var(--type-scale, 1)); --leading-xs:calc(1rem * var(--type-scale, 1)); /* 11px eyebrows / section labels */
323
- --text-sm:calc(0.8125rem * var(--type-scale, 1)); --leading-sm:calc(1.15rem * var(--type-scale, 1)); /* 13px nav labels, dense body */
324
- --text-base:calc(0.875rem * var(--type-scale, 1)); --leading-base:calc(1.35rem * var(--type-scale, 1)); /* 14px base app text (was 16px) */
325
- --text-lg:calc(0.9375rem * var(--type-scale, 1)); --leading-lg:calc(1.4rem * var(--type-scale, 1)); /* 15px */
326
- --text-xl:calc(1.0625rem * var(--type-scale, 1)); --leading-xl:calc(1.55rem * var(--type-scale, 1)); /* 17px */
327
- --text-2xl:calc(1.3125rem * var(--type-scale, 1)); --leading-2xl:calc(1.7rem * var(--type-scale, 1)); /* 21px */
328
- --text-3xl:calc(1.625rem * var(--type-scale, 1)); --leading-3xl:calc(1.95rem * var(--type-scale, 1)); /* 26px */
329
- --text-4xl:calc(2rem * var(--type-scale, 1)); --leading-4xl:calc(2.25rem * var(--type-scale, 1)); /* 32px */
330
- --text-5xl:calc(2.5rem * var(--type-scale, 1)); --leading-5xl:1.05; /* 40px */
331
- --text-6xl:calc(3.25rem * var(--type-scale, 1)); --leading-6xl:1; /* 52px */
332
- --text-7xl:calc(4rem * var(--type-scale, 1)); --leading-7xl:1; /* 64px */
333
- --text-8xl:calc(5.25rem * var(--type-scale, 1)); --leading-8xl:1; /* 84px */
334
- --text-9xl:calc(7rem * var(--type-scale, 1)); --leading-9xl:1; /* 112px */
322
+ /* The CONTRAST knob how far apart the rungs sit, where --type-scale is how
323
+ big they all are. Two different questions, and only the first one was
324
+ askable: a surface could take the whole ramp up 15% but could not flatten a
325
+ dense table's steps or open up a marketing page's, so both settled for the
326
+ app register's tight default.
327
+
328
+ It is authored as a TRANSFORM and not as a generated ramp, which is the
329
+ whole point. `base + (authored - base) * ratio` keeps each rung's authored
330
+ value as the INPUT, so ratio 1 reduces to exactly the published number by
331
+ algebra rather than by care — the deltas below are just `authored - 0.875`.
332
+ The alternative, deriving each rung as base * ratio^n, would restate the
333
+ ramp in a second place, and this scale is not geometric anyway: the app
334
+ register (11/13/14/15/17) is near-linear on purpose and a ratio applied to
335
+ it would erase the 1px steps the compact register is FOR. That drift is not
336
+ hypothetical here — see src/preference.ts, where a recomputed copy of this
337
+ ramp shipped `lg: 16px` against this file's 15px.
338
+
339
+ A rung is one place, so a rung added later needs a delta and nothing else. */
340
+ --type-ratio:1;
341
+
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 */
335
360
 
336
361
  /* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
337
362
  Hanzo Studio address the ramp as --font-size-*; both names are one value, so
@@ -11,19 +11,44 @@
11
11
  @hanzo/design's `vars({type})` emits exactly this name. */
12
12
  --type-scale:1;
13
13
 
14
- --text-xs:calc(0.6875rem * var(--type-scale, 1)); --leading-xs:calc(1rem * var(--type-scale, 1)); /* 11px eyebrows / section labels */
15
- --text-sm:calc(0.8125rem * var(--type-scale, 1)); --leading-sm:calc(1.15rem * var(--type-scale, 1)); /* 13px nav labels, dense body */
16
- --text-base:calc(0.875rem * var(--type-scale, 1)); --leading-base:calc(1.35rem * var(--type-scale, 1)); /* 14px base app text (was 16px) */
17
- --text-lg:calc(0.9375rem * var(--type-scale, 1)); --leading-lg:calc(1.4rem * var(--type-scale, 1)); /* 15px */
18
- --text-xl:calc(1.0625rem * var(--type-scale, 1)); --leading-xl:calc(1.55rem * var(--type-scale, 1)); /* 17px */
19
- --text-2xl:calc(1.3125rem * var(--type-scale, 1)); --leading-2xl:calc(1.7rem * var(--type-scale, 1)); /* 21px */
20
- --text-3xl:calc(1.625rem * var(--type-scale, 1)); --leading-3xl:calc(1.95rem * var(--type-scale, 1)); /* 26px */
21
- --text-4xl:calc(2rem * var(--type-scale, 1)); --leading-4xl:calc(2.25rem * var(--type-scale, 1)); /* 32px */
22
- --text-5xl:calc(2.5rem * var(--type-scale, 1)); --leading-5xl:1.05; /* 40px */
23
- --text-6xl:calc(3.25rem * var(--type-scale, 1)); --leading-6xl:1; /* 52px */
24
- --text-7xl:calc(4rem * var(--type-scale, 1)); --leading-7xl:1; /* 64px */
25
- --text-8xl:calc(5.25rem * var(--type-scale, 1)); --leading-8xl:1; /* 84px */
26
- --text-9xl:calc(7rem * var(--type-scale, 1)); --leading-9xl:1; /* 112px */
14
+ /* The CONTRAST knob how far apart the rungs sit, where --type-scale is how
15
+ big they all are. Two different questions, and only the first one was
16
+ askable: a surface could take the whole ramp up 15% but could not flatten a
17
+ dense table's steps or open up a marketing page's, so both settled for the
18
+ app register's tight default.
19
+
20
+ It is authored as a TRANSFORM and not as a generated ramp, which is the
21
+ whole point. `base + (authored - base) * ratio` keeps each rung's authored
22
+ value as the INPUT, so ratio 1 reduces to exactly the published number by
23
+ algebra rather than by care — the deltas below are just `authored - 0.875`.
24
+ The alternative, deriving each rung as base * ratio^n, would restate the
25
+ ramp in a second place, and this scale is not geometric anyway: the app
26
+ register (11/13/14/15/17) is near-linear on purpose and a ratio applied to
27
+ it would erase the 1px steps the compact register is FOR. That drift is not
28
+ hypothetical here — see src/preference.ts, where a recomputed copy of this
29
+ ramp shipped `lg: 16px` against this file's 15px.
30
+
31
+ A rung is one place, so a rung added later needs a delta and nothing else. */
32
+ --type-ratio:1;
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 */
27
52
 
28
53
  /* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
29
54
  Hanzo Studio address the ramp as --font-size-*; both names are one value, so