@hanzo/design 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/styles.css CHANGED
@@ -229,23 +229,23 @@
229
229
  silent (the border simply stops existing) and has shipped before. */
230
230
  .light{
231
231
  color-scheme:light;
232
- --background:#ffffff;
232
+ --background:#f7f7f7;
233
233
  --foreground:#0a0a0a;
234
234
  /* A ladder, not four names for #f5f5f5. Light lifts by the same tiny steps
235
235
  dark does — ~2% per rung — so --surface-0..3 mean something in both themes. */
236
- --card:#fafafa;
236
+ --card:#f2f2f2;
237
237
  --card-foreground:#0a0a0a;
238
- --popover:#ffffff;
238
+ --popover:#fbfbfb;
239
239
  --popover-foreground:#0a0a0a;
240
240
  --primary:#0a0a0a;
241
241
  --primary-hover:#262626;
242
242
  --primary-foreground:#fafafa;
243
- --secondary:#ededed;
243
+ --secondary:#e4e4e4;
244
244
  --secondary-hover:#e0e0e0;
245
245
  --secondary-foreground:#0a0a0a;
246
- --muted:#f5f5f5;
246
+ --muted:#ededed;
247
247
  --muted-foreground:#525252;
248
- --accent:#ededed;
248
+ --accent:#e4e4e4;
249
249
  --accent-foreground:#0a0a0a;
250
250
  --destructive:var(--state-error);
251
251
  --destructive-hover:#dc2626;
@@ -270,9 +270,9 @@
270
270
  --selection:rgb(0 0 0 / .16);
271
271
  --glass:rgb(0 0 0 / .04);
272
272
  --glass-strong:rgb(0 0 0 / .07);
273
- --surface-card:#fafafa;
274
- --surface-card-emphasis:#ffffff;
275
- --surface-card-quiet:#fcfcfc;
273
+ --surface-card:#f2f2f2;
274
+ --surface-card-emphasis:#fdfdfd;
275
+ --surface-card-quiet:#f5f5f5;
276
276
  --surface-overlay:rgb(255 255 255 / .95);
277
277
  --surface-header:rgb(255 255 255 / .8);
278
278
  --surface-scrim:rgb(0 0 0 / .5);
@@ -290,19 +290,25 @@
290
290
  — the two are the SAME scale, mirrored. A surface/tenant overrides any --text-*
291
291
  on :root to retune density on demand. */
292
292
  :root{
293
- --text-xs:0.6875rem; --leading-xs:1rem; /* 11px eyebrows / section labels */
294
- --text-sm:0.8125rem; --leading-sm:1.15rem; /* 13px nav labels, dense body */
295
- --text-base:0.875rem; --leading-base:1.35rem; /* 14px base app text (was 16px) */
296
- --text-lg:0.9375rem; --leading-lg:1.4rem; /* 15px */
297
- --text-xl:1.0625rem; --leading-xl:1.55rem; /* 17px */
298
- --text-2xl:1.3125rem; --leading-2xl:1.7rem; /* 21px */
299
- --text-3xl:1.625rem; --leading-3xl:1.95rem; /* 26px */
300
- --text-4xl:2rem; --leading-4xl:2.25rem; /* 32px */
301
- --text-5xl:2.5rem; --leading-5xl:1.05; /* 40px */
302
- --text-6xl:3.25rem; --leading-6xl:1; /* 52px */
303
- --text-7xl:4rem; --leading-7xl:1; /* 64px */
304
- --text-8xl:5.25rem; --leading-8xl:1; /* 84px */
305
- --text-9xl:7rem; --leading-9xl:1; /* 112px */
293
+ /* The type knob. 1 is the published scale; every rung below multiplies by
294
+ it, so a surface or a person retunes the WHOLE ramp by setting one
295
+ number and no rung can drift out of relation with the others.
296
+ @hanzo/design's `vars({type})` emits exactly this name. */
297
+ --type-scale:1;
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 */
306
312
 
307
313
  /* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
308
314
  Hanzo Studio address the ramp as --font-size-*; both names are one value, so
@@ -367,7 +373,7 @@
367
373
  form feels broken. pointer:coarse is the real signal — a desktop mouse keeps
368
374
  the compact 13px field. */
369
375
  @media (pointer:coarse){
370
- :root{--text-control:1rem}
376
+ :root{--text-control:calc(1rem * var(--type-scale, 1))}
371
377
  }
372
378
 
373
379
  /* ── tokens/spacing.css ─────────────────────────────────────── */
@@ -375,21 +381,27 @@
375
381
  declared in hanzo.ai's tailwind.config.ts (legacy v3 config, kept for
376
382
  reference) — use it for editorial layouts, not for component padding. */
377
383
  :root{
384
+ /* The density knob. 1 is the published spacing; every --space-* rung below
385
+ multiplies by it, and --grid-gap-* reference those rungs rather than
386
+ restating them, so gaps, padding and section rhythm move together.
387
+ @hanzo/design's `vars({density})` emits exactly this name. */
388
+ --density:1;
389
+
378
390
  --space-0:0;
379
- --space-1:0.25rem;
380
- --space-2:0.5rem;
381
- --space-3:0.75rem;
382
- --space-4:1rem;
383
- --space-5:1.25rem;
384
- --space-6:1.5rem;
385
- --space-8:2rem;
386
- --space-10:2.5rem;
387
- --space-12:3rem;
388
- --space-14:3.5rem;
389
- --space-16:4rem;
390
- --space-20:5rem;
391
- --space-24:6rem;
392
- --space-32:8rem;
391
+ --space-1:calc(0.25rem * var(--density, 1));
392
+ --space-2:calc(0.5rem * var(--density, 1));
393
+ --space-3:calc(0.75rem * var(--density, 1));
394
+ --space-4:calc(1rem * var(--density, 1));
395
+ --space-5:calc(1.25rem * var(--density, 1));
396
+ --space-6:calc(1.5rem * var(--density, 1));
397
+ --space-8:calc(2rem * var(--density, 1));
398
+ --space-10:calc(2.5rem * var(--density, 1));
399
+ --space-12:calc(3rem * var(--density, 1));
400
+ --space-14:calc(3.5rem * var(--density, 1));
401
+ --space-16:calc(4rem * var(--density, 1));
402
+ --space-20:calc(5rem * var(--density, 1));
403
+ --space-24:calc(6rem * var(--density, 1));
404
+ --space-32:calc(8rem * var(--density, 1));
393
405
 
394
406
  /* golden ramp (φ) — hanzo.ai tailwind.config.ts */
395
407
  --golden-1:0.25rem;
@@ -834,19 +846,26 @@
834
846
  padding:0 var(--space-3);
835
847
  }
836
848
  :where(textarea){padding:var(--space-2) var(--space-3);resize:vertical}
837
- /* ——— focus, on a control ———
838
- Not the generic ring. A field already HAS an edge, so focus brightens that
839
- edge (.15 -> .22) and adds a soft halo just outside it — which is what the
840
- reference does (.composer-box:focus-within) and what separates a focused
841
- field from a browser default. The generic `outline` is suppressed here
842
- precisely because it would draw a second, harder box around this one. */
843
- :where(input,select,textarea):focus-visible{
844
- outline:none;
845
- border-color:var(--border-focus);
846
- box-shadow:var(--ring-focus);
847
- }
848
- /* A hovered field lifts its surface a rung. Nudging its EDGE from .15 to .16
849
- is a state nobody can see. */
849
+ /* A field has NO focus rule of its own — see the ring at the bottom of this
850
+ layer, which is the one focus indicator for everything.
851
+
852
+ There used to be one here: `outline:none` plus a brightened edge (.15 ->
853
+ .22) plus a soft halo, the composer look. It was removed because it could
854
+ not do the job in either of the two ways that matter.
855
+
856
+ It was never VISIBLE ENOUGH. Composited on --background the brightened edge
857
+ measures 1.91:1 and the halo 1.25:1, against the 3:1 that WCAG 1.4.11 asks
858
+ of a focus indicator and that this package already gates --ring on. The
859
+ budget was documented as being spent entirely on --ring "because that is
860
+ what a keyboard user navigates by" true for a button, and false for a
861
+ field for exactly as long as this rule told fields not to use it.
862
+
863
+ And it was SUPPRESSIBLE. It carried the indicator on `border-color`, so any
864
+ app that states `border` on its own fields overrode it — @hanzo/id does,
865
+ unlayered, which beats this layer whatever its specificity, and its focused
866
+ fields sat at the resting .15 while both files read as correct. An outline
867
+ is not a border: nothing in an app's field styling reaches it, so the ring
868
+ paints whether or not the app has opinions about edges. */
850
869
  :where(input,select,textarea):hover:not(:focus-visible):not(:disabled){background:var(--surface-3)}
851
870
  :where(input,textarea)::placeholder{color:var(--text-disabled)}
852
871
  :where(input,select,textarea,button):disabled{opacity:.5;cursor:not-allowed}
@@ -886,6 +905,19 @@
886
905
  }
887
906
  }
888
907
 
908
+ /* THE focus indicator. One rule, every focusable thing, no exceptions — a
909
+ button, a link, a summary, a field. 2px at --ring is 3.77:1 on the darkest
910
+ canvas and clears the 2px perimeter WCAG 2.4.13 asks for; the gate in
911
+ check-tokens holds --ring to that and nothing else here may weaken it.
912
+
913
+ There were two rules until 0.4.9, and they collided invisibly. Both computed
914
+ to (0,1,0) — :where() zeroes whatever it wraps, leaving one pseudo-class on
915
+ each side — so the cascade fell through to SOURCE ORDER inside this layer,
916
+ this rule was written later, and it overrode the `outline:none` the field
917
+ rule stated expressly to prevent it. Every focused input on every consumer
918
+ drew BOTH the ring and the edge+halo. Each rule read as correct alone, which
919
+ is why it survived review in both files; the defect existed only in their
920
+ order. One rule cannot disagree with itself. */
889
921
  :focus-visible{outline:2px solid var(--ring);outline-offset:2px}
890
922
  /* --white-20 is white-on-white in the light theme, so selection reads through
891
923
  --selection, which BOTH themes define. */
package/tailwind.css CHANGED
@@ -252,23 +252,23 @@
252
252
  silent (the border simply stops existing) and has shipped before. */
253
253
  .light{
254
254
  color-scheme:light;
255
- --background:#ffffff;
255
+ --background:#f7f7f7;
256
256
  --foreground:#0a0a0a;
257
257
  /* A ladder, not four names for #f5f5f5. Light lifts by the same tiny steps
258
258
  dark does — ~2% per rung — so --surface-0..3 mean something in both themes. */
259
- --card:#fafafa;
259
+ --card:#f2f2f2;
260
260
  --card-foreground:#0a0a0a;
261
- --popover:#ffffff;
261
+ --popover:#fbfbfb;
262
262
  --popover-foreground:#0a0a0a;
263
263
  --primary:#0a0a0a;
264
264
  --primary-hover:#262626;
265
265
  --primary-foreground:#fafafa;
266
- --secondary:#ededed;
266
+ --secondary:#e4e4e4;
267
267
  --secondary-hover:#e0e0e0;
268
268
  --secondary-foreground:#0a0a0a;
269
- --muted:#f5f5f5;
269
+ --muted:#ededed;
270
270
  --muted-foreground:#525252;
271
- --accent:#ededed;
271
+ --accent:#e4e4e4;
272
272
  --accent-foreground:#0a0a0a;
273
273
  --destructive:var(--state-error);
274
274
  --destructive-hover:#dc2626;
@@ -293,9 +293,9 @@
293
293
  --selection:rgb(0 0 0 / .16);
294
294
  --glass:rgb(0 0 0 / .04);
295
295
  --glass-strong:rgb(0 0 0 / .07);
296
- --surface-card:#fafafa;
297
- --surface-card-emphasis:#ffffff;
298
- --surface-card-quiet:#fcfcfc;
296
+ --surface-card:#f2f2f2;
297
+ --surface-card-emphasis:#fdfdfd;
298
+ --surface-card-quiet:#f5f5f5;
299
299
  --surface-overlay:rgb(255 255 255 / .95);
300
300
  --surface-header:rgb(255 255 255 / .8);
301
301
  --surface-scrim:rgb(0 0 0 / .5);
@@ -313,19 +313,25 @@
313
313
  — the two are the SAME scale, mirrored. A surface/tenant overrides any --text-*
314
314
  on :root to retune density on demand. */
315
315
  :root{
316
- --text-xs:0.6875rem; --leading-xs:1rem; /* 11px eyebrows / section labels */
317
- --text-sm:0.8125rem; --leading-sm:1.15rem; /* 13px nav labels, dense body */
318
- --text-base:0.875rem; --leading-base:1.35rem; /* 14px base app text (was 16px) */
319
- --text-lg:0.9375rem; --leading-lg:1.4rem; /* 15px */
320
- --text-xl:1.0625rem; --leading-xl:1.55rem; /* 17px */
321
- --text-2xl:1.3125rem; --leading-2xl:1.7rem; /* 21px */
322
- --text-3xl:1.625rem; --leading-3xl:1.95rem; /* 26px */
323
- --text-4xl:2rem; --leading-4xl:2.25rem; /* 32px */
324
- --text-5xl:2.5rem; --leading-5xl:1.05; /* 40px */
325
- --text-6xl:3.25rem; --leading-6xl:1; /* 52px */
326
- --text-7xl:4rem; --leading-7xl:1; /* 64px */
327
- --text-8xl:5.25rem; --leading-8xl:1; /* 84px */
328
- --text-9xl:7rem; --leading-9xl:1; /* 112px */
316
+ /* The type knob. 1 is the published scale; every rung below multiplies by
317
+ it, so a surface or a person retunes the WHOLE ramp by setting one
318
+ number and no rung can drift out of relation with the others.
319
+ @hanzo/design's `vars({type})` emits exactly this name. */
320
+ --type-scale:1;
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 */
329
335
 
330
336
  /* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
331
337
  Hanzo Studio address the ramp as --font-size-*; both names are one value, so
@@ -390,7 +396,7 @@
390
396
  form feels broken. pointer:coarse is the real signal — a desktop mouse keeps
391
397
  the compact 13px field. */
392
398
  @media (pointer:coarse){
393
- :root{--text-control:1rem}
399
+ :root{--text-control:calc(1rem * var(--type-scale, 1))}
394
400
  }
395
401
 
396
402
  /* ── tokens/spacing.css ─────────────────────────────────────── */
@@ -398,21 +404,27 @@
398
404
  declared in hanzo.ai's tailwind.config.ts (legacy v3 config, kept for
399
405
  reference) — use it for editorial layouts, not for component padding. */
400
406
  :root{
407
+ /* The density knob. 1 is the published spacing; every --space-* rung below
408
+ multiplies by it, and --grid-gap-* reference those rungs rather than
409
+ restating them, so gaps, padding and section rhythm move together.
410
+ @hanzo/design's `vars({density})` emits exactly this name. */
411
+ --density:1;
412
+
401
413
  --space-0:0;
402
- --space-1:0.25rem;
403
- --space-2:0.5rem;
404
- --space-3:0.75rem;
405
- --space-4:1rem;
406
- --space-5:1.25rem;
407
- --space-6:1.5rem;
408
- --space-8:2rem;
409
- --space-10:2.5rem;
410
- --space-12:3rem;
411
- --space-14:3.5rem;
412
- --space-16:4rem;
413
- --space-20:5rem;
414
- --space-24:6rem;
415
- --space-32:8rem;
414
+ --space-1:calc(0.25rem * var(--density, 1));
415
+ --space-2:calc(0.5rem * var(--density, 1));
416
+ --space-3:calc(0.75rem * var(--density, 1));
417
+ --space-4:calc(1rem * var(--density, 1));
418
+ --space-5:calc(1.25rem * var(--density, 1));
419
+ --space-6:calc(1.5rem * var(--density, 1));
420
+ --space-8:calc(2rem * var(--density, 1));
421
+ --space-10:calc(2.5rem * var(--density, 1));
422
+ --space-12:calc(3rem * var(--density, 1));
423
+ --space-14:calc(3.5rem * var(--density, 1));
424
+ --space-16:calc(4rem * var(--density, 1));
425
+ --space-20:calc(5rem * var(--density, 1));
426
+ --space-24:calc(6rem * var(--density, 1));
427
+ --space-32:calc(8rem * var(--density, 1));
416
428
 
417
429
  /* golden ramp (φ) — hanzo.ai tailwind.config.ts */
418
430
  --golden-1:0.25rem;
@@ -857,19 +869,26 @@
857
869
  padding:0 var(--space-3);
858
870
  }
859
871
  :where(textarea){padding:var(--space-2) var(--space-3);resize:vertical}
860
- /* ——— focus, on a control ———
861
- Not the generic ring. A field already HAS an edge, so focus brightens that
862
- edge (.15 -> .22) and adds a soft halo just outside it — which is what the
863
- reference does (.composer-box:focus-within) and what separates a focused
864
- field from a browser default. The generic `outline` is suppressed here
865
- precisely because it would draw a second, harder box around this one. */
866
- :where(input,select,textarea):focus-visible{
867
- outline:none;
868
- border-color:var(--border-focus);
869
- box-shadow:var(--ring-focus);
870
- }
871
- /* A hovered field lifts its surface a rung. Nudging its EDGE from .15 to .16
872
- is a state nobody can see. */
872
+ /* A field has NO focus rule of its own — see the ring at the bottom of this
873
+ layer, which is the one focus indicator for everything.
874
+
875
+ There used to be one here: `outline:none` plus a brightened edge (.15 ->
876
+ .22) plus a soft halo, the composer look. It was removed because it could
877
+ not do the job in either of the two ways that matter.
878
+
879
+ It was never VISIBLE ENOUGH. Composited on --background the brightened edge
880
+ measures 1.91:1 and the halo 1.25:1, against the 3:1 that WCAG 1.4.11 asks
881
+ of a focus indicator and that this package already gates --ring on. The
882
+ budget was documented as being spent entirely on --ring "because that is
883
+ what a keyboard user navigates by" true for a button, and false for a
884
+ field for exactly as long as this rule told fields not to use it.
885
+
886
+ And it was SUPPRESSIBLE. It carried the indicator on `border-color`, so any
887
+ app that states `border` on its own fields overrode it — @hanzo/id does,
888
+ unlayered, which beats this layer whatever its specificity, and its focused
889
+ fields sat at the resting .15 while both files read as correct. An outline
890
+ is not a border: nothing in an app's field styling reaches it, so the ring
891
+ paints whether or not the app has opinions about edges. */
873
892
  :where(input,select,textarea):hover:not(:focus-visible):not(:disabled){background:var(--surface-3)}
874
893
  :where(input,textarea)::placeholder{color:var(--text-disabled)}
875
894
  :where(input,select,textarea,button):disabled{opacity:.5;cursor:not-allowed}
@@ -909,6 +928,19 @@
909
928
  }
910
929
  }
911
930
 
931
+ /* THE focus indicator. One rule, every focusable thing, no exceptions — a
932
+ button, a link, a summary, a field. 2px at --ring is 3.77:1 on the darkest
933
+ canvas and clears the 2px perimeter WCAG 2.4.13 asks for; the gate in
934
+ check-tokens holds --ring to that and nothing else here may weaken it.
935
+
936
+ There were two rules until 0.4.9, and they collided invisibly. Both computed
937
+ to (0,1,0) — :where() zeroes whatever it wraps, leaving one pseudo-class on
938
+ each side — so the cascade fell through to SOURCE ORDER inside this layer,
939
+ this rule was written later, and it overrode the `outline:none` the field
940
+ rule stated expressly to prevent it. Every focused input on every consumer
941
+ drew BOTH the ring and the edge+halo. Each rule read as correct alone, which
942
+ is why it survived review in both files; the defect existed only in their
943
+ order. One rule cannot disagree with itself. */
912
944
  :focus-visible{outline:2px solid var(--ring);outline-offset:2px}
913
945
  /* --white-20 is white-on-white in the light theme, so selection reads through
914
946
  --selection, which BOTH themes define. */
package/tokens/base.css CHANGED
@@ -99,19 +99,26 @@
99
99
  padding:0 var(--space-3);
100
100
  }
101
101
  :where(textarea){padding:var(--space-2) var(--space-3);resize:vertical}
102
- /* ——— focus, on a control ———
103
- Not the generic ring. A field already HAS an edge, so focus brightens that
104
- edge (.15 -> .22) and adds a soft halo just outside it — which is what the
105
- reference does (.composer-box:focus-within) and what separates a focused
106
- field from a browser default. The generic `outline` is suppressed here
107
- precisely because it would draw a second, harder box around this one. */
108
- :where(input,select,textarea):focus-visible{
109
- outline:none;
110
- border-color:var(--border-focus);
111
- box-shadow:var(--ring-focus);
112
- }
113
- /* A hovered field lifts its surface a rung. Nudging its EDGE from .15 to .16
114
- is a state nobody can see. */
102
+ /* A field has NO focus rule of its own — see the ring at the bottom of this
103
+ layer, which is the one focus indicator for everything.
104
+
105
+ There used to be one here: `outline:none` plus a brightened edge (.15 ->
106
+ .22) plus a soft halo, the composer look. It was removed because it could
107
+ not do the job in either of the two ways that matter.
108
+
109
+ It was never VISIBLE ENOUGH. Composited on --background the brightened edge
110
+ measures 1.91:1 and the halo 1.25:1, against the 3:1 that WCAG 1.4.11 asks
111
+ of a focus indicator and that this package already gates --ring on. The
112
+ budget was documented as being spent entirely on --ring "because that is
113
+ what a keyboard user navigates by" true for a button, and false for a
114
+ field for exactly as long as this rule told fields not to use it.
115
+
116
+ And it was SUPPRESSIBLE. It carried the indicator on `border-color`, so any
117
+ app that states `border` on its own fields overrode it — @hanzo/id does,
118
+ unlayered, which beats this layer whatever its specificity, and its focused
119
+ fields sat at the resting .15 while both files read as correct. An outline
120
+ is not a border: nothing in an app's field styling reaches it, so the ring
121
+ paints whether or not the app has opinions about edges. */
115
122
  :where(input,select,textarea):hover:not(:focus-visible):not(:disabled){background:var(--surface-3)}
116
123
  :where(input,textarea)::placeholder{color:var(--text-disabled)}
117
124
  :where(input,select,textarea,button):disabled{opacity:.5;cursor:not-allowed}
@@ -151,6 +158,19 @@
151
158
  }
152
159
  }
153
160
 
161
+ /* THE focus indicator. One rule, every focusable thing, no exceptions — a
162
+ button, a link, a summary, a field. 2px at --ring is 3.77:1 on the darkest
163
+ canvas and clears the 2px perimeter WCAG 2.4.13 asks for; the gate in
164
+ check-tokens holds --ring to that and nothing else here may weaken it.
165
+
166
+ There were two rules until 0.4.9, and they collided invisibly. Both computed
167
+ to (0,1,0) — :where() zeroes whatever it wraps, leaving one pseudo-class on
168
+ each side — so the cascade fell through to SOURCE ORDER inside this layer,
169
+ this rule was written later, and it overrode the `outline:none` the field
170
+ rule stated expressly to prevent it. Every focused input on every consumer
171
+ drew BOTH the ring and the edge+halo. Each rule read as correct alone, which
172
+ is why it survived review in both files; the defect existed only in their
173
+ order. One rule cannot disagree with itself. */
154
174
  :focus-visible{outline:2px solid var(--ring);outline-offset:2px}
155
175
  /* --white-20 is white-on-white in the light theme, so selection reads through
156
176
  --selection, which BOTH themes define. */
package/tokens/colors.css CHANGED
@@ -214,23 +214,23 @@
214
214
  silent (the border simply stops existing) and has shipped before. */
215
215
  .light{
216
216
  color-scheme:light;
217
- --background:#ffffff;
217
+ --background:#f7f7f7;
218
218
  --foreground:#0a0a0a;
219
219
  /* A ladder, not four names for #f5f5f5. Light lifts by the same tiny steps
220
220
  dark does — ~2% per rung — so --surface-0..3 mean something in both themes. */
221
- --card:#fafafa;
221
+ --card:#f2f2f2;
222
222
  --card-foreground:#0a0a0a;
223
- --popover:#ffffff;
223
+ --popover:#fbfbfb;
224
224
  --popover-foreground:#0a0a0a;
225
225
  --primary:#0a0a0a;
226
226
  --primary-hover:#262626;
227
227
  --primary-foreground:#fafafa;
228
- --secondary:#ededed;
228
+ --secondary:#e4e4e4;
229
229
  --secondary-hover:#e0e0e0;
230
230
  --secondary-foreground:#0a0a0a;
231
- --muted:#f5f5f5;
231
+ --muted:#ededed;
232
232
  --muted-foreground:#525252;
233
- --accent:#ededed;
233
+ --accent:#e4e4e4;
234
234
  --accent-foreground:#0a0a0a;
235
235
  --destructive:var(--state-error);
236
236
  --destructive-hover:#dc2626;
@@ -255,9 +255,9 @@
255
255
  --selection:rgb(0 0 0 / .16);
256
256
  --glass:rgb(0 0 0 / .04);
257
257
  --glass-strong:rgb(0 0 0 / .07);
258
- --surface-card:#fafafa;
259
- --surface-card-emphasis:#ffffff;
260
- --surface-card-quiet:#fcfcfc;
258
+ --surface-card:#f2f2f2;
259
+ --surface-card-emphasis:#fdfdfd;
260
+ --surface-card-quiet:#f5f5f5;
261
261
  --surface-overlay:rgb(255 255 255 / .95);
262
262
  --surface-header:rgb(255 255 255 / .8);
263
263
  --surface-scrim:rgb(0 0 0 / .5);
@@ -2,21 +2,27 @@
2
2
  declared in hanzo.ai's tailwind.config.ts (legacy v3 config, kept for
3
3
  reference) — use it for editorial layouts, not for component padding. */
4
4
  :root{
5
+ /* The density knob. 1 is the published spacing; every --space-* rung below
6
+ multiplies by it, and --grid-gap-* reference those rungs rather than
7
+ restating them, so gaps, padding and section rhythm move together.
8
+ @hanzo/design's `vars({density})` emits exactly this name. */
9
+ --density:1;
10
+
5
11
  --space-0:0;
6
- --space-1:0.25rem;
7
- --space-2:0.5rem;
8
- --space-3:0.75rem;
9
- --space-4:1rem;
10
- --space-5:1.25rem;
11
- --space-6:1.5rem;
12
- --space-8:2rem;
13
- --space-10:2.5rem;
14
- --space-12:3rem;
15
- --space-14:3.5rem;
16
- --space-16:4rem;
17
- --space-20:5rem;
18
- --space-24:6rem;
19
- --space-32:8rem;
12
+ --space-1:calc(0.25rem * var(--density, 1));
13
+ --space-2:calc(0.5rem * var(--density, 1));
14
+ --space-3:calc(0.75rem * var(--density, 1));
15
+ --space-4:calc(1rem * var(--density, 1));
16
+ --space-5:calc(1.25rem * var(--density, 1));
17
+ --space-6:calc(1.5rem * var(--density, 1));
18
+ --space-8:calc(2rem * var(--density, 1));
19
+ --space-10:calc(2.5rem * var(--density, 1));
20
+ --space-12:calc(3rem * var(--density, 1));
21
+ --space-14:calc(3.5rem * var(--density, 1));
22
+ --space-16:calc(4rem * var(--density, 1));
23
+ --space-20:calc(5rem * var(--density, 1));
24
+ --space-24:calc(6rem * var(--density, 1));
25
+ --space-32:calc(8rem * var(--density, 1));
20
26
 
21
27
  /* golden ramp (φ) — hanzo.ai tailwind.config.ts */
22
28
  --golden-1:0.25rem;
@@ -5,19 +5,25 @@
5
5
  — the two are the SAME scale, mirrored. A surface/tenant overrides any --text-*
6
6
  on :root to retune density on demand. */
7
7
  :root{
8
- --text-xs:0.6875rem; --leading-xs:1rem; /* 11px eyebrows / section labels */
9
- --text-sm:0.8125rem; --leading-sm:1.15rem; /* 13px nav labels, dense body */
10
- --text-base:0.875rem; --leading-base:1.35rem; /* 14px base app text (was 16px) */
11
- --text-lg:0.9375rem; --leading-lg:1.4rem; /* 15px */
12
- --text-xl:1.0625rem; --leading-xl:1.55rem; /* 17px */
13
- --text-2xl:1.3125rem; --leading-2xl:1.7rem; /* 21px */
14
- --text-3xl:1.625rem; --leading-3xl:1.95rem; /* 26px */
15
- --text-4xl:2rem; --leading-4xl:2.25rem; /* 32px */
16
- --text-5xl:2.5rem; --leading-5xl:1.05; /* 40px */
17
- --text-6xl:3.25rem; --leading-6xl:1; /* 52px */
18
- --text-7xl:4rem; --leading-7xl:1; /* 64px */
19
- --text-8xl:5.25rem; --leading-8xl:1; /* 84px */
20
- --text-9xl:7rem; --leading-9xl:1; /* 112px */
8
+ /* The type knob. 1 is the published scale; every rung below multiplies by
9
+ it, so a surface or a person retunes the WHOLE ramp by setting one
10
+ number and no rung can drift out of relation with the others.
11
+ @hanzo/design's `vars({type})` emits exactly this name. */
12
+ --type-scale:1;
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 */
21
27
 
22
28
  /* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
23
29
  Hanzo Studio address the ramp as --font-size-*; both names are one value, so
@@ -82,5 +88,5 @@
82
88
  form feels broken. pointer:coarse is the real signal — a desktop mouse keeps
83
89
  the compact 13px field. */
84
90
  @media (pointer:coarse){
85
- :root{--text-control:1rem}
91
+ :root{--text-control:calc(1rem * var(--type-scale, 1))}
86
92
  }