@hanzo/design 0.5.8 → 0.5.10

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.
@@ -26,10 +26,27 @@
26
26
  * preview and a server render apply it identically.
27
27
  */
28
28
  export type Density = "compact" | "default" | "comfortable";
29
+ /**
30
+ * The face, named by what it IS rather than by a family — the families are the
31
+ * token file's to choose, and it already names four.
32
+ */
33
+ export type Face = "default" | "system" | "serif" | "mono";
34
+ /** How wide the page runs before it stops. */
35
+ export type Measure = "narrow" | "default" | "wide";
29
36
  export interface Preference {
30
37
  /** Multiplier on the type ramp. 1 is the published scale. */
31
38
  type?: number;
39
+ /**
40
+ * How far APART the rungs sit — the ramp's contrast, where `type` is its size.
41
+ * 1 is the published scale; below 1 flattens it toward a uniform register,
42
+ * above 1 opens the display end and tightens the small one.
43
+ */
44
+ ratio?: number;
32
45
  density?: Density;
46
+ /** Which face the page is set in. */
47
+ font?: Face;
48
+ /** How wide the page runs — the measure, not the window. */
49
+ width?: Measure;
33
50
  /** A CSS colour for --primary / --accent. Rejected unless it is one. */
34
51
  accent?: string;
35
52
  }
@@ -44,6 +61,19 @@ export interface Preference {
44
61
  */
45
62
  export declare const TYPE_MIN = 0.85;
46
63
  export declare const TYPE_MAX = 1.4;
64
+ /**
65
+ * The ratio's bounds are looser than type's, because the ramp defends its own
66
+ * floor.
67
+ *
68
+ * `tokens/typography.css` floors --text-xs/sm with `max()`, so the failure mode
69
+ * that forces type's tight clamp — a knob quietly rendering 9px labels — cannot
70
+ * happen on this axis however it combines with type. What is left to bound is
71
+ * only whether the ramp still READS as a ramp: at 0.75 the app register is
72
+ * within a hair of uniform, and past 1.5 a page's h2 has left its own body text
73
+ * behind entirely.
74
+ */
75
+ export declare const RATIO_MIN = 0.75;
76
+ export declare const RATIO_MAX = 1.5;
47
77
  /**
48
78
  * Is this a colour, or is it something being smuggled into a style attribute?
49
79
  *
@@ -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;;;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"}
@@ -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
  *
@@ -49,6 +62,37 @@ const DENSITY = {
49
62
  default: 1,
50
63
  comfortable: 1.15,
51
64
  };
65
+ /**
66
+ * A face is chosen from the ones `tokens/fonts.css` already declares, and it is
67
+ * set by REFERENCE — `var(--font-serif)`, never a family list restated here.
68
+ *
69
+ * That is what keeps a preference from pinning a face: when the token file
70
+ * changes what "mono" means, or a brand overrides it, a person who chose mono
71
+ * follows. Writing `Georgia, serif` here would freeze this file's idea of serif
72
+ * into every document that ever stored the preference.
73
+ *
74
+ * `default` is deliberately absent from the map rather than mapped to
75
+ * `var(--font-sans)`: the axis writes `--font-sans`, so resolving it to itself
76
+ * is a cycle, and "no opinion" is already how every other axis says default.
77
+ */
78
+ const FACE = {
79
+ system: "ui-sans-serif, system-ui, -apple-system, sans-serif",
80
+ serif: "var(--font-serif)",
81
+ mono: "var(--font-mono)",
82
+ };
83
+ /**
84
+ * The measure moves the CONTAINERS, not the columns.
85
+ *
86
+ * `--grid-columns` is 12 because layouts are authored against 12; changing it
87
+ * re-flows every span a page declares and is a different page, not a wider one.
88
+ * What a reader actually means by "wider" is how far the text runs before it
89
+ * wraps, which is `--container-*` — so that is the knob, and the grid inside it
90
+ * is untouched.
91
+ */
92
+ const MEASURE = {
93
+ narrow: { max: "64rem", prose: "40rem", wide: "58rem" },
94
+ wide: { max: "96rem", prose: "56rem", wide: "86rem" },
95
+ };
52
96
  const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
53
97
  /** Trim to 4dp so a multiplier cannot emit a 17-digit float into a stylesheet. */
54
98
  const round = (n) => String(Math.round(n * 10000) / 10000);
@@ -81,9 +125,25 @@ export function vars(p) {
81
125
  if (typeof p.type === "number" && Number.isFinite(p.type)) {
82
126
  out["--type-scale"] = round(clamp(p.type, TYPE_MIN, TYPE_MAX));
83
127
  }
128
+ if (typeof p.ratio === "number" && Number.isFinite(p.ratio)) {
129
+ out["--type-ratio"] = round(clamp(p.ratio, RATIO_MIN, RATIO_MAX));
130
+ }
84
131
  if (p.density && p.density in DENSITY) {
85
132
  out["--density"] = round(DENSITY[p.density]);
86
133
  }
134
+ // Indexed, not `in`-checked: the maps omit `default`, so a lookup answers
135
+ // undefined for it and for anything stored that is not a face at all. One
136
+ // read, no cast, and an unknown value is refused by the same line that
137
+ // resolves a known one.
138
+ const face = p.font ? FACE[p.font] : undefined;
139
+ if (face)
140
+ out["--font-sans"] = face;
141
+ const measure = p.width ? MEASURE[p.width] : undefined;
142
+ if (measure) {
143
+ out["--container-max"] = measure.max;
144
+ out["--container-prose"] = measure.prose;
145
+ out["--container-wide"] = measure.wide;
146
+ }
87
147
  if (p.accent && isColor(p.accent)) {
88
148
  // Both names, because the ramp uses --primary for action surfaces and
89
149
  // --accent for selection. One hue, stated once, landing on both.
@@ -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.10",
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",
@@ -24,7 +24,13 @@ const js = src
24
24
  .replace(/^import[^\n]*\n/gm, "")
25
25
  .replace(/export (type|interface) [\s\S]*?\n}\n/g, "")
26
26
  .replace(/export type [^\n]*\n/g, "")
27
- .replace(/: Record<[^>]*>/g, "")
27
+ // A generic annotation may nest (`Partial<Record<Face, string>>`), and
28
+ // stopping at the first `>` leaves `, string>` behind, so Function() fails to
29
+ // parse for a reason that has nothing to do with the preference. One level of
30
+ // nesting is matched explicitly — balanced, and it cannot run past the
31
+ // annotation the way a lookahead to the initializer can (that ate a function
32
+ // body, because a return type has no initializer to stop at).
33
+ .replace(/:\s*\w+<(?:[^<>]|<[^<>]*>)*>/g, "")
28
34
  .replace(/: Preference/g, "")
29
35
  .replace(/: Density/g, "")
30
36
  .replace(/: string/g, "")
@@ -114,6 +120,30 @@ check("a unitless leading stays a RATIO — scaling it would double-apply", () =
114
120
  eq(bad, [], "unitless leadings must not carry a multiplier:");
115
121
  });
116
122
 
123
+ check("a face is set by REFERENCE, so a brand's own face still wins", () => {
124
+ // Restating "Georgia, serif" here would freeze this file's idea of serif into
125
+ // every document that ever stored the preference.
126
+ eq(vars({ font: "serif" })["--font-sans"], "var(--font-serif)");
127
+ eq(vars({ font: "mono" })["--font-sans"], "var(--font-mono)");
128
+ ok(!("--font-mono" in vars({ font: "serif" })), "code must stay monospaced");
129
+ eq(vars({ font: "default" }), {}, "default is no opinion, not a self-reference:");
130
+ });
131
+
132
+ check("the measure moves containers, never the column count", () => {
133
+ const v = vars({ width: "wide" });
134
+ ok(!("--grid-columns" in v), "changing the columns is a different page, not a wider one");
135
+ ok("--container-max" in v && "--container-prose" in v, Object.keys(v).join(","));
136
+ ok(parseFloat(v["--container-prose"]) > parseFloat(vars({ width: "narrow" })["--container-prose"]), "wide must exceed narrow");
137
+ eq(vars({ width: "default" }), {}, "default is no opinion:");
138
+ });
139
+
140
+ check("an unknown stored value is refused, not passed through", () => {
141
+ // A preference is user input; these axes are string unions, so a stored value
142
+ // from a future version or a hand-edited store must not reach a stylesheet.
143
+ eq(vars({ font: "comic" }), {});
144
+ eq(vars({ width: "enormous" }), {});
145
+ });
146
+
117
147
  check("a colour lands on both --primary and --accent", () => {
118
148
  const v = vars({ accent: "#808000" });
119
149
  eq(v["--primary"], "#808000");
@@ -156,7 +186,11 @@ check("every emitted name is one the token files actually read", () => {
156
186
  const declared = new Set([...text.matchAll(/^\s*(--[a-z0-9-]+)\s*:/gm)].map((m) => m[1]));
157
187
  const read = new Set([...text.matchAll(/var\((--[a-z0-9-]+)/g)].map((m) => m[1]));
158
188
 
159
- const emitted = Object.keys(vars({ type: 1.1, density: "compact", accent: "#fff" }));
189
+ // EVERY axis, not a sample an axis missing here is an axis whose names
190
+ // nothing gates, which is how a knob starts writing into another document.
191
+ const emitted = Object.keys(
192
+ vars({ type: 1.1, ratio: 1.2, density: "compact", font: "serif", width: "wide", accent: "#fff" })
193
+ );
160
194
  const orphans = emitted.filter((k) => !declared.has(k) && !read.has(k));
161
195
  eq(orphans, [], "emitted names no token file declares or reads:");
162
196
  });
@@ -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
@@ -28,10 +28,29 @@
28
28
 
29
29
  export type Density = "compact" | "default" | "comfortable";
30
30
 
31
+ /**
32
+ * The face, named by what it IS rather than by a family — the families are the
33
+ * token file's to choose, and it already names four.
34
+ */
35
+ export type Face = "default" | "system" | "serif" | "mono";
36
+
37
+ /** How wide the page runs before it stops. */
38
+ export type Measure = "narrow" | "default" | "wide";
39
+
31
40
  export interface Preference {
32
41
  /** Multiplier on the type ramp. 1 is the published scale. */
33
42
  type?: number;
43
+ /**
44
+ * How far APART the rungs sit — the ramp's contrast, where `type` is its size.
45
+ * 1 is the published scale; below 1 flattens it toward a uniform register,
46
+ * above 1 opens the display end and tightens the small one.
47
+ */
48
+ ratio?: number;
34
49
  density?: Density;
50
+ /** Which face the page is set in. */
51
+ font?: Face;
52
+ /** How wide the page runs — the measure, not the window. */
53
+ width?: Measure;
35
54
  /** A CSS colour for --primary / --accent. Rejected unless it is one. */
36
55
  accent?: string;
37
56
  }
@@ -48,6 +67,20 @@ export interface Preference {
48
67
  export const TYPE_MIN = 0.85;
49
68
  export const TYPE_MAX = 1.4;
50
69
 
70
+ /**
71
+ * The ratio's bounds are looser than type's, because the ramp defends its own
72
+ * floor.
73
+ *
74
+ * `tokens/typography.css` floors --text-xs/sm with `max()`, so the failure mode
75
+ * that forces type's tight clamp — a knob quietly rendering 9px labels — cannot
76
+ * happen on this axis however it combines with type. What is left to bound is
77
+ * only whether the ramp still READS as a ramp: at 0.75 the app register is
78
+ * within a hair of uniform, and past 1.5 a page's h2 has left its own body text
79
+ * behind entirely.
80
+ */
81
+ export const RATIO_MIN = 0.75;
82
+ export const RATIO_MAX = 1.5;
83
+
51
84
  /**
52
85
  * Density moves SPACING only, and its range is much tighter than type's.
53
86
  *
@@ -62,6 +95,39 @@ const DENSITY: Record<Density, number> = {
62
95
  comfortable: 1.15,
63
96
  };
64
97
 
98
+ /**
99
+ * A face is chosen from the ones `tokens/fonts.css` already declares, and it is
100
+ * set by REFERENCE — `var(--font-serif)`, never a family list restated here.
101
+ *
102
+ * That is what keeps a preference from pinning a face: when the token file
103
+ * changes what "mono" means, or a brand overrides it, a person who chose mono
104
+ * follows. Writing `Georgia, serif` here would freeze this file's idea of serif
105
+ * into every document that ever stored the preference.
106
+ *
107
+ * `default` is deliberately absent from the map rather than mapped to
108
+ * `var(--font-sans)`: the axis writes `--font-sans`, so resolving it to itself
109
+ * is a cycle, and "no opinion" is already how every other axis says default.
110
+ */
111
+ const FACE: Partial<Record<Face, string>> = {
112
+ system: "ui-sans-serif, system-ui, -apple-system, sans-serif",
113
+ serif: "var(--font-serif)",
114
+ mono: "var(--font-mono)",
115
+ };
116
+
117
+ /**
118
+ * The measure moves the CONTAINERS, not the columns.
119
+ *
120
+ * `--grid-columns` is 12 because layouts are authored against 12; changing it
121
+ * re-flows every span a page declares and is a different page, not a wider one.
122
+ * What a reader actually means by "wider" is how far the text runs before it
123
+ * wraps, which is `--container-*` — so that is the knob, and the grid inside it
124
+ * is untouched.
125
+ */
126
+ const MEASURE: Partial<Record<Measure, { max: string; prose: string; wide: string }>> = {
127
+ narrow: { max: "64rem", prose: "40rem", wide: "58rem" },
128
+ wide: { max: "96rem", prose: "56rem", wide: "86rem" },
129
+ };
130
+
65
131
  const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));
66
132
 
67
133
  /** Trim to 4dp so a multiplier cannot emit a 17-digit float into a stylesheet. */
@@ -99,10 +165,28 @@ export function vars(p: Preference): Record<string, string> {
99
165
  out["--type-scale"] = round(clamp(p.type, TYPE_MIN, TYPE_MAX));
100
166
  }
101
167
 
168
+ if (typeof p.ratio === "number" && Number.isFinite(p.ratio)) {
169
+ out["--type-ratio"] = round(clamp(p.ratio, RATIO_MIN, RATIO_MAX));
170
+ }
171
+
102
172
  if (p.density && p.density in DENSITY) {
103
173
  out["--density"] = round(DENSITY[p.density]);
104
174
  }
105
175
 
176
+ // Indexed, not `in`-checked: the maps omit `default`, so a lookup answers
177
+ // undefined for it and for anything stored that is not a face at all. One
178
+ // read, no cast, and an unknown value is refused by the same line that
179
+ // resolves a known one.
180
+ const face = p.font ? FACE[p.font] : undefined;
181
+ if (face) out["--font-sans"] = face;
182
+
183
+ const measure = p.width ? MEASURE[p.width] : undefined;
184
+ if (measure) {
185
+ out["--container-max"] = measure.max;
186
+ out["--container-prose"] = measure.prose;
187
+ out["--container-wide"] = measure.wide;
188
+ }
189
+
106
190
  if (p.accent && isColor(p.accent)) {
107
191
  // Both names, because the ramp uses --primary for action surfaces and
108
192
  // --accent for selection. One hue, stated once, landing on both.
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