@cloudgrid-io/ui 0.27.1 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/blocks/avatar.d.ts +10 -4
  2. package/dist/blocks/avatar.d.ts.map +1 -1
  3. package/dist/blocks/avatar.js +8 -2
  4. package/dist/blocks/avatar.js.map +1 -1
  5. package/dist/blocks/icons.d.ts +1 -1
  6. package/dist/blocks/icons.d.ts.map +1 -1
  7. package/dist/blocks/icons.js +13 -1
  8. package/dist/blocks/icons.js.map +1 -1
  9. package/dist/blocks/side-nav.d.ts +49 -0
  10. package/dist/blocks/side-nav.d.ts.map +1 -0
  11. package/dist/blocks/side-nav.js +217 -0
  12. package/dist/blocks/side-nav.js.map +1 -0
  13. package/dist/blocks/stat-tile.d.ts +69 -0
  14. package/dist/blocks/stat-tile.d.ts.map +1 -0
  15. package/dist/blocks/stat-tile.js +54 -0
  16. package/dist/blocks/stat-tile.js.map +1 -0
  17. package/dist/blocks/tool-setup.d.ts +105 -4
  18. package/dist/blocks/tool-setup.d.ts.map +1 -1
  19. package/dist/blocks/tool-setup.js +238 -13
  20. package/dist/blocks/tool-setup.js.map +1 -1
  21. package/dist/fonts.css +38 -9
  22. package/dist/index.d.ts +5 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +5 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/lib/cloudgrid-cn.d.ts +1 -1
  27. package/dist/lib/cloudgrid-cn.d.ts.map +1 -1
  28. package/dist/lib/cloudgrid-cn.js +7 -0
  29. package/dist/lib/cloudgrid-cn.js.map +1 -1
  30. package/dist/theme.css +95 -0
  31. package/dist/tokens/base.json +389 -3
  32. package/dist/tokens/surface-ink.json +10 -0
  33. package/dist/tokens/surface-panel.json +10 -0
  34. package/dist/tokens.css +520 -10
  35. package/dist/tokens.json +43 -2
  36. package/dist/ui/button.d.ts +1 -1
  37. package/dist/ui/button.d.ts.map +1 -1
  38. package/dist/ui/button.js +28 -0
  39. package/dist/ui/button.js.map +1 -1
  40. package/dist/ui/dialog.d.ts +17 -1
  41. package/dist/ui/dialog.d.ts.map +1 -1
  42. package/dist/ui/dialog.js +18 -2
  43. package/dist/ui/dialog.js.map +1 -1
  44. package/dist/ui/drawer.d.ts +208 -0
  45. package/dist/ui/drawer.d.ts.map +1 -0
  46. package/dist/ui/drawer.js +265 -0
  47. package/dist/ui/drawer.js.map +1 -0
  48. package/dist/ui/popover.d.ts +83 -0
  49. package/dist/ui/popover.d.ts.map +1 -0
  50. package/dist/ui/popover.js +102 -0
  51. package/dist/ui/popover.js.map +1 -0
  52. package/dist/ui/radio-group.d.ts +21 -0
  53. package/dist/ui/radio-group.d.ts.map +1 -0
  54. package/dist/ui/radio-group.js +101 -0
  55. package/dist/ui/radio-group.js.map +1 -0
  56. package/package.json +2 -1
package/dist/tokens.css CHANGED
@@ -443,10 +443,230 @@
443
443
  value held on white, so the ground moved and the dot kept its contrast. The
444
444
  muted text rung comes with it, 3.27:1 -> 3.69:1 on white. */
445
445
  --cg-neutral-500: #858585;
446
+ /*
447
+ SOURCE: derived, 2026-09-18 (#232). The missing rung between neutral-500 and
448
+ neutral-700, and the only reason it exists is that --cg-text-muted has no value
449
+ on this ramp that clears the 4.5:1 body-text floor.
450
+
451
+ neutral-500 measures 3.69:1 on the card, 3.28:1 on the page ground and 3.29:1 on
452
+ the lavender wash. All three clear 3:1 for large text and none clears 4.5:1, so
453
+ every caption, every eyebrow and every tile hint in this library is under AA for
454
+ body copy. neutral-700 clears it everywhere (7.00:1 on the card) but it IS
455
+ --cg-text-secondary, so aliasing a "strong muted" onto it would not add a rung,
456
+ it would rename one.
457
+
458
+ Derived, with the sweep, so the next person does not repeat it:
459
+
460
+ #6e6e6e on the page ground : 4.53:1 clears by 0.03 — a margin a later
461
+ ground retune would erase in silence
462
+ #6c6c6c : 4.66:1
463
+ #6a6a6a : 4.80:1 <-- this
464
+ #686868 : 4.95:1
465
+
466
+ #6a6a6a on the card #ffffff : 5.41:1
467
+ on the page #f1f1f6 : 4.80:1
468
+ on raised #f4f6f8 : 4.99:1
469
+ on subtle #f2f1f7 : 4.82:1
470
+
471
+ The extra step past the first passing value is --cg-accent's practice, and for
472
+ its reason: 4.53:1 is a number that a page-ground retune turns into a failure
473
+ with nothing going red.
474
+
475
+ KNOWN, and not fixed here: on the violet-200 wash the frames draw (#d9d4fb) this
476
+ measures 3.80:1, still under the body floor. Nothing on a neutral ramp clears
477
+ 4.5:1 on that ground and stays muted; see --cg-violet-200's own note. */
478
+ --cg-neutral-600: #6a6a6a;
446
479
  /* SOURCE: legal / secondary text */
447
480
  --cg-neutral-700: #595959;
448
481
  /* SOURCE: ink */
449
482
  --cg-neutral-900: #222222;
483
+ /*
484
+ SOURCE: Figma variable color/neutral/300, read first-hand on the thegrid frame
485
+ (8364:4396, "On the grid & to the world" on the ink band).
486
+
487
+ THE NAME IS NOT AN ACCIDENT. Figma calls this family color/neutral/300 / 500 /
488
+ 600. This package's neutral ramp is already occupied at every one of those
489
+ numbers by a PURE grey (#acacac, #858585, #595959) that two published consumers
490
+ ship today, and these three are blue-greys: #828c98 is B152 against R130, which
491
+ is not a grey at all. Taking the Figma numbers would mean either changing three
492
+ shipped values (breaking) or having one ramp hold two hue families under one
493
+ name (silent). So the cool family takes its own root and the greys keep theirs.
494
+
495
+ 1.45:1 on the card and 1.29:1 on the page ground: this is a value for text on a
496
+ dark ground, never on a light one. */
497
+ --cg-slate-300: #d2d7de;
498
+ /*
499
+ SOURCE: Figma variable color/neutral/500, also carried under the legacy name
500
+ "N3 new". Read first-hand on four of the six canonical frames — thegrid
501
+ (8364:3966, footer links), teams (7172:7607), pricing (7779:9284), builders
502
+ (6916:8104, the terminal's "$"). It is the design's quiet-text tone and the most
503
+ frequently missing value in this package (ui#239).
504
+
505
+ WHAT IT MEASURES, because the frames spend it on words:
506
+
507
+ on the card #ffffff : 3.41:1
508
+ on the page ground #f1f1f6 : 3.03:1
509
+ on raised #f4f6f8 : 3.15:1
510
+ on the violet wash #d9d4fb : 2.40:1 <-- under even the 3:1 indicator bar
511
+ on ink #222222 : 4.66:1
512
+ on the purple panel #5a47f0 : 1.71:1 invisible
513
+
514
+ So it clears 3:1 for large text on the three light grounds, clears 4.5:1 on ink
515
+ alone, and fails everywhere the frames actually use it for 15px footer links.
516
+ It is shipped because the frames use it and this package is meant to be able to
517
+ draw them; it is deliberately NOT wired to a --cg-text-* semantic, because those
518
+ carry a contrast contract and this value cannot meet it. --cg-slate-600 is the
519
+ companion that can. See ui#232 for the same shape of problem on --cg-text-muted.
520
+
521
+ DECLARED AT :root ONLY. A brand ramp rung is not a semantic and does not flip. */
522
+ --cg-slate-500: #828c98;
523
+ /*
524
+ SOURCE: Figma variable color/neutral/600, read first-hand on thegrid
525
+ (8364:4327, the stats rows; 8364:4255, "your CREATIONS").
526
+
527
+ This is the accessible companion to --cg-slate-500 and the design supplied it
528
+ itself, which is why nothing had to be invented:
529
+
530
+ on the card #ffffff : 5.65:1
531
+ on the page ground #f1f1f6 : 5.02:1
532
+ on raised #f4f6f8 : 5.21:1
533
+ on subtle #f2f1f7 : 5.03:1
534
+ on the cyan tint #dffbfc : 5.20:1
535
+ on the violet wash #d9d4fb : 3.97:1 large text only
536
+ on the lime #b6de68 : 3.67:1 large text only
537
+ on ink #222222 : 2.82:1 do not
538
+ on the purple panel #5a47f0 : 1.03:1 invisible
539
+
540
+ So: safe for body copy on any of the light page and card grounds, and on the
541
+ cyan tint. NOT safe on the violet wash, on the lime, or on either dark surface.
542
+ Where the frames put slate-500 on a light ground and mean it as running text,
543
+ this is the value that keeps the design and passes. */
544
+ --cg-slate-600: #5f6874;
545
+ /*
546
+ SOURCE: Figma variable "stone", declared on the home frame (8204:2397). ui#239
547
+ asks for it by name.
548
+
549
+ HONEST PROVENANCE, because this is the one value here that is weaker than the
550
+ rest: it is a DECLARATION. The home frame declares it as a variable, and the
551
+ Figma quota ran out before the audit could read the ~90 nodes of that frame that
552
+ might paint with it. No node on any of the other five frames uses it. So its
553
+ role is recorded as unknown rather than guessed, and nothing in this package
554
+ consumes it yet.
555
+
556
+ It is a warm near-black, not a grey: R52 G51 B48 against --cg-ink's flat #222222.
557
+ 12.63:1 on the card and 11.22:1 on the page ground, so wherever it lands it is a
558
+ text-or-ink value and not a surface tint.
559
+
560
+ The rung number is by lightness against this package's own ramp, which ends at
561
+ neutral-900 #222222. Figma gives it no number. */
562
+ --cg-stone-800: #343330;
563
+
564
+ /* ---- hue-ramps ----
565
+ The three brand hues as RAMPS, not as single values (#239, 2026-09-18).
566
+
567
+ Until now this package carried one violet, one lime and one cyan, plus a single
568
+ --cg-primary-tint. The six canonical Figma frames carry a full scale of each,
569
+ declared as Figma variables under color/violet/*, color/lime/* and color/cyan/*,
570
+ and they spend those rungs on real surfaces: chips, role badges, avatar strokes,
571
+ step diagrams, the enterprise button. With no rung to reach for, a consumer
572
+ either hardcodes the hex, which CLAUDE.md forbids, or approximates with
573
+ color-mix(). cloudgrid-landing is doing the second right now — #133 renders
574
+ `color-mix(in srgb, var(--cg-primary) 22%, var(--cg-neutral-0))` to get within
575
+ 2/255 of --cg-violet-200 — and that is the drift this group removes.
576
+
577
+ RUNG NUMBERS ARE FIGMA'S, not this package's, wherever Figma names them. Two
578
+ rungs (violet-150, violet-400, cyan-400) are values observed applied on a node
579
+ with NO Figma variable behind them; each says so in its own note and is placed
580
+ by lightness between the bound rungs either side. Nothing here was invented.
581
+
582
+ WHY THE UTILITY NAMES DIVERGE FROM THE TOKEN NAMES. The violet ramp maps onto
583
+ --color-brand-100..900, because "brand" is this package's own name for the hue
584
+ (theme.css, rule 4) and Tailwind ships no `brand` scale to collide with. The
585
+ cyan and lime rungs map onto SUFFIXED names (cyan-tint, lime-edge) rather than
586
+ cyan-200 / lime-700, because Tailwind DOES ship those exact numbered names and
587
+ claiming them would silently repaint every `bg-cyan-200` in a consumer's own
588
+ markup. That is rule 4's failure aimed at Tailwind instead of shadcn, and it is
589
+ worse, because the consumer never opted in.
590
+
591
+ DECLARED AT :root ONLY, like every brand source. A ramp rung is not a semantic
592
+ and does not flip; the semantics that spend it do. */
593
+ /*
594
+ SOURCE: Figma color/violet/100. Applied on thegrid at 8364:4136 (the private-status chip), 8364:4158 (the Owner role badge) and the Add-people button. 1.20:1 on the card, so a ground and never a foreground. */
595
+ --cg-violet-100: #ebe8fd;
596
+ /*
597
+ SOURCE: observed applied, UNBOUND. teams 7172:7759 (the first flow circle) and builders 6919:8782 (the first step circle, "Detect"). No Figma variable carries it.
598
+
599
+ It is the value ui#239 asks for and the one cloudgrid-landing currently reaches with a color-mix of --cg-primary. Placed at 150 because it sits between the two bound rungs either side: violet-100 #ebe8fd and violet-200 #d9d4fb. A half-rung is not new here; the neutral ramp already carries neutral-25 between 0 and 50.
600
+
601
+ Measured as a ground: --cg-text-primary is 12.38:1 on it, --cg-slate-600 4.25:1, --cg-slate-500 2.56:1. */
602
+ --cg-violet-150: #e3e0fd;
603
+ /*
604
+ SOURCE: Figma color/violet/200, declared on the home and thegrid frames.
605
+
606
+ READ ui#239'S CORRECTION BEFORE USING --cg-offline FOR THIS. That issue's first table maps #D9D4FB onto --cg-offline, and the mapping only holds inside [data-cg-surface='panel']. At :root --cg-offline is var(--cg-neutral-500), a mid grey, so a consumer taking that token at face value on the page ground gets #858585 and nothing goes red. This rung is the value; --cg-offline is a status tone that happens to reach it on one surface.
607
+
608
+ The pairing the frames draw on it is a documented AA failure and the reason --cg-slate-600 exists: --cg-slate-500 on this ground measures 2.40:1, under even the 3:1 indicator bar. --cg-text-primary is 11.18:1, --cg-slate-600 3.97:1 (large text only), --cg-neutral-600 3.80:1. */
609
+ --cg-violet-200: #d9d4fb;
610
+ /*
611
+ SOURCE: Figma color/violet/300, declared on thegrid. Rendered there only inside flattened SVG illustration, so it is a declared rung with no CSS-reachable node behind it yet. Carried so the ramp a designer edits in Figma is the ramp this package ships. */
612
+ --cg-violet-300: #c8c2fa;
613
+ /*
614
+ SOURCE: observed applied, UNBOUND. builders 6919:8785, the second step circle.
615
+
616
+ Placed at 400 because it is the only observed violet with no bound rung within reach: it sits between color/violet/300 #c8c2fa and color/violet/500 #a69bf7.
617
+
618
+ Worth knowing before retuning: the four builders step circles are #E3E0FD / #BCB4F9 / #A49AF7 / #8072F3, which are --cg-primary composited on white at 17 / 40 / 55 / 77 percent. The last two land within 2 and 4 of bound rungs 500 and 700 and are taken as those; this one does not. */
619
+ --cg-violet-400: #bcb4f9;
620
+ /*
621
+ SOURCE: Figma color/violet/500, declared on thegrid. builders 6919:8788 paints #A49AF7, which is this value within 2/255 per channel and is treated as this rung rather than as a ninth violet. */
622
+ --cg-violet-500: #a69bf7;
623
+ /*
624
+ SOURCE: Figma color/violet/600, declared on thegrid; rendered only inside flattened SVG illustration. Carried for the same reason as violet-300. */
625
+ --cg-violet-600: #9488f5;
626
+ /*
627
+ SOURCE: Figma color/violet/700. Applied on thegrid 8364:4388, the stroke around an avatar chip in the Plug section. builders 6919:8791 paints #8072F3, within 4/255 per channel, and is taken as this rung.
628
+
629
+ 3.57:1 on the card: an indicator or a border, never a word. */
630
+ --cg-violet-700: #8476f4;
631
+ /*
632
+ Figma names --cg-primary color/violet/900. Deliberately an ALIAS rather than the hex written a second time: the brand hue has one home, at the top of this file, and a designer retuning it must not have to find two. */
633
+ --cg-violet-900: var(--cg-primary);
634
+ /*
635
+ SOURCE: Figma color/cyan/200, also carried under the legacy name "Teriary 20" (the typo is Figma's). Applied on three frames: thegrid 8364:4166 (the Editor role badge), teams 7172:7643 and pricing 7779:9320 (the footer's secondary buttons, with a --cg-tertiary hairline around them).
636
+
637
+ It IS a tint of the tertiary, and exactly: #60e9f0 composited on white at 20% is #dffbfc to the byte. Shipped as a value anyway rather than as a color-mix, because a consumer should not have to know the recipe to draw a button.
638
+
639
+ --cg-slate-600 measures 5.20:1 on it and --cg-text-primary 14.65:1. */
640
+ --cg-cyan-200: #dffbfc;
641
+ /*
642
+ SOURCE: Figma color/cyan/300, declared on thegrid; rendered only inside flattened SVG illustration. Carried so the ramp is whole. */
643
+ --cg-cyan-300: #cff8fa;
644
+ /*
645
+ SOURCE: observed applied, UNBOUND. thegrid 8364:4002 and 8364:4056 — the footer and enterprise buttons, drawn as this fill with a --cg-tertiary hairline. ui#239's second comment asks for it.
646
+
647
+ IT IS NOT A TINT OF --cg-tertiary, and that is worth the arithmetic, because every other rung in this family is. #60E9F0 has G 233, so reaching G 252 needs 86% white, and that mix gives R 233 — this value has R 177. Solving the same mix from the blue channel gives 0%. No single alpha produces it. So the color-mix() workaround available for the violet wash is not available here, which is precisely why the token is.
648
+
649
+ Placed at 400 by lightness, between bound rungs cyan-300 and cyan-900.
650
+
651
+ 5.07:1 on the purple panel and 13.82:1 on ink: as a fill it carries text on both dark surfaces. 1.15:1 on the card, so never a foreground on white. */
652
+ --cg-cyan-400: #b1fcff;
653
+ /*
654
+ Figma names --cg-tertiary color/cyan/900. An alias, for the reason violet-900 is one. */
655
+ --cg-cyan-900: var(--cg-tertiary);
656
+ /*
657
+ SOURCE: Figma color/lime/200, declared on thegrid; rendered only inside flattened SVG illustration. Carried so the ramp is whole. */
658
+ --cg-lime-200: #eef7dc;
659
+ /*
660
+ SOURCE: Figma color/lime/600, declared on thegrid; rendered only inside flattened SVG illustration. */
661
+ --cg-lime-600: #d0ea9e;
662
+ /*
663
+ SOURCE: Figma color/lime/700. Applied on thegrid 8364:4387, the stroke around the audience avatars in the Plug section.
664
+
665
+ 1.38:1 on the card and 4.22:1 on the purple panel: a border on a dark ground, not a line on a light one. */
666
+ --cg-lime-700: #c8e68e;
667
+ /*
668
+ Figma names --cg-secondary color/lime/900. An alias, for the reason violet-900 is one. */
669
+ --cg-lime-900: var(--cg-secondary);
450
670
 
451
671
  /* ---- semantic-surfaces ---- */
452
672
  --cg-surface-page: var(--cg-neutral-25);
@@ -476,6 +696,37 @@
476
696
  --cg-text-primary: var(--cg-neutral-900);
477
697
  --cg-text-secondary: var(--cg-neutral-700);
478
698
  --cg-text-muted: var(--cg-neutral-500);
699
+ /*
700
+ The muted tone for text that is real content: a tile hint, an eyebrow that
701
+ carries meaning, a caption a reader has to read. ui#232, 2026-09-18.
702
+
703
+ --cg-text-muted is under the 4.5:1 body-text floor on three of the four grounds
704
+ (3.69:1 card, 3.28:1 page, 3.44:1 panel, 5.92:1 ink), which means every caption
705
+ in this library is AA-legal as large text and illegal as body copy. #232 asked
706
+ for a stronger rung rather than a retune, on the reasoning #149 used for
707
+ state's -strong: a tone tuned to recede and a tone tuned to be read are two
708
+ jobs, and collapsing them retunes both.
709
+
710
+ It is NOT --cg-text-secondary under a second name. Secondary is neutral-700
711
+ (7.00:1 on the card) and reads as a second voice; this is neutral-600, a new
712
+ rung derived for the floor and nothing else. Its note carries the sweep.
713
+
714
+ base #6a6a6a card 5.41:1 page 4.80:1 raised 4.99:1 subtle 4.82:1
715
+ panel rgba(255,255,255,0.86) on #5a47f0 : 4.75:1
716
+ ink rgba(255,255,255,0.68) on #222222 : 8.02:1
717
+
718
+ ON THE PANEL THIS RUNG IS BRIGHTER THAN --cg-text-secondary, and that is not a
719
+ mistake. Measured, the panel's own secondary (0.82 white) is 4.47:1, which is
720
+ under the floor by 0.03. Nothing dimmer than 0.84 white clears 4.5:1 on that
721
+ ground, so the panel cannot deliver a "muted that reads as body copy" below its
722
+ own secondary. Same shape as the note further up about a panel chip's word:
723
+ where the surface cannot deliver the ratio, the surface has to move, not the
724
+ foreground. A designer who dislikes the inversion changes --cg-surface-panel,
725
+ not this.
726
+
727
+ DECORATIVE CAPTIONS MAY KEEP --cg-text-muted. This rung does not deprecate it;
728
+ #232's own done-when says so. */
729
+ --cg-text-muted-strong: var(--cg-neutral-600);
479
730
  --cg-text-placeholder: var(--cg-neutral-400);
480
731
  --cg-text-accent: var(--cg-primary);
481
732
  --cg-text-inverse: var(--cg-neutral-0);
@@ -523,16 +774,94 @@
523
774
  The mono face is untouched: it is used only inside prompt and terminal surfaces
524
775
  and the draft says nothing about it. */
525
776
  /*
526
- The display rung, now Manrope (#197). Identical to --cg-font-body today and
527
- deliberately still a separate token.
528
-
529
- A consumer that wants the licensed Futura back puts it in front of this stack in
530
- its own :root and changes nothing else. Note that the blocks reading this rung
531
- also set `uppercase` and --cg-display-tracking, which were tuned for a condensed
532
- 800; they still render, in Manrope, and whether the lockups keep the uppercase is
533
- a block decision rather than a token one (#203). */
534
- --cg-font-display: 'Manrope Variable', 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
535
- --cg-font-body: 'Manrope Variable', 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
777
+ The display face, and the one token in this file that names a font this package
778
+ does NOT ship. ui#240, 2026-09-18.
779
+
780
+ All six canonical Figma frames set every headline in TANKER: the hero (92px on
781
+ five frames, 120px on home), every section heading (56px), every card title.
782
+ Nothing else in the design uses it and it is used for nothing else.
783
+
784
+ THE BINARY CANNOT SHIP HERE, and the reason is a licence, not an oversight.
785
+ Tanker is under the ITF Free Font License v2.0, whose section 02 forbids making
786
+ the Font Software available "through another font website, font library,
787
+ marketplace, repository, download service, application or platform". This
788
+ package is public on registry.npmjs.org, installable by anyone with no
789
+ credentials, which is exactly what that clause names. Section 03 states the
790
+ remedy: "Any third party wishing to use the Font Software must obtain their own
791
+ copy directly from Fontshare and be independently bound by this License."
792
+
793
+ That is why Manrope is fine and Tanker is not. Manrope arrives as
794
+ @fontsource-variable/manrope under the Open Font License, which permits
795
+ redistribution. Fontshare publishes no fontsource package.
796
+
797
+ So the package owns the STACK and the consumer owns the FILE. Adding 'Tanker' at
798
+ the front is strictly additive in effect: a consumer that has not self-hosted it
799
+ falls straight through to Manrope and renders exactly what 0.27.2 rendered.
800
+ Before this, a consumer wanting the display face had to copy this whole fallback
801
+ chain verbatim into its own :root (cloudgrid-landing PR #132 did), and that copy
802
+ went stale the moment this package retired the condensed rung.
803
+
804
+ PENDING, and stated so rather than assumed: the 2026-09-10 re-skin (#197)
805
+ deliberately removed the display rung and set the whole system in Manrope.
806
+ Whether naming Tanker here reverses that decision is the founder's call, and it
807
+ is the open question at the end of #240. This change is written to be reversible
808
+ in one line if the answer is no.
809
+
810
+ NOT RETUNED, deliberately: --cg-display-line (1.06) and --cg-display-tracking
811
+ (0.012em) were tuned against Saira Semi Condensed 800. Every Tanker node in
812
+ every frame is leading-none with no tracking. See --cg-display-line-tight. */
813
+ --cg-font-display: 'Tanker', 'Manrope Variable', 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
814
+ /*
815
+ The body face. cloudgrid-landing#145, 2026-09-18.
816
+
817
+ Every body string in the canonical frames is SPACE GROTESK, and it was checked
818
+ on five of the six independently before it was believed: home 8204:2561 and
819
+ 8204:2563, thegrid 8364:4213, builders 6913:7654, teams 7172:7571, enterprise
820
+ 7037:779, pricing 7779:9261 — all 20px / 1.46, all the lede under a headline,
821
+ all in unrelated sections. The nav links and the card copy are the same face at
822
+ 15px. This is the design's text face, not a stray on one frame.
823
+
824
+ IT SHIPS HERE, unlike Tanker. @fontsource-variable/space-grotesk is OFL-1.1,
825
+ which permits redistribution, so it arrives as a dependency and an @import in
826
+ fonts.css exactly the way Manrope does. See fonts.css.
827
+
828
+ THIS IS A VALUE CHANGE, not an addition, and it is the only one in this release.
829
+ Manrope is kept in the chain immediately behind it, so a consumer whose pipeline
830
+ somehow does not resolve the new dependency renders precisely what it rendered
831
+ before. Blast radius: every string in every consumer that has not overridden
832
+ this token changes face. Metrics differ — Space Grotesk is wider at the same
833
+ size — so a consumer with a tightly measured layout should look at it before
834
+ taking the release. cloudgrid-landing is the consumer that asked for it.
835
+
836
+ MANROPE DID NOT LEAVE. The frames keep it, and only for two jobs: the eyebrow
837
+ and the button label. Those now read --cg-font-ui, which is this token's
838
+ previous value unchanged, so neither of them moves. */
839
+ --cg-font-body: 'Space Grotesk Variable', 'Space Grotesk', 'Manrope Variable', 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
840
+ /*
841
+ The face for the small fixed chrome of a page: the eyebrow above a heading, and
842
+ the label inside a button. 2026-09-18.
843
+
844
+ A third family stack is not a flourish. The canonical frames use exactly three
845
+ faces in three jobs — Tanker for display, Space Grotesk for everything a reader
846
+ reads, and MANROPE for the eyebrow and the button label — and they are
847
+ consistent about it. Every button label on all six frames is Manrope Bold 15
848
+ leading-none (home I8204:2580;732:46, thegrid I8364:4215;732:46, builders
849
+ I6913:7656;732:46, teams I7172:7573;732:46, enterprise I7037:781;732:46,
850
+ pricing I7779:9336;732:46). Every applied eyebrow is Manrope SemiBold 14, and
851
+ Figma binds it as a named text variable, "Eyebrow".
852
+
853
+ Its VALUE is --cg-font-body's previous value, byte for byte. That is what makes
854
+ the body-face change safe to read: the eyebrow and the button did not move, they
855
+ just stopped being spelled with the body token.
856
+
857
+ KNOWN INCONSISTENCY IN THE SOURCE, recorded so nobody "fixes" it from the wrong
858
+ end: on the home frame the four nav links (8204:2573) are Manrope Medium 15 and
859
+ "Sign In" beside them (8204:2579) is Space Grotesk Medium 15 — same slot, same
860
+ size, same weight, two faces. On thegrid the same split appears at 8364:4053
861
+ against 8364:4052/4054/4055. Five frames agree the nav is Space Grotesk, so the
862
+ Manrope instances read as a migration caught mid-flight rather than as intent,
863
+ and this token is NOT the nav's face. */
864
+ --cg-font-ui: 'Manrope Variable', 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
536
865
  --cg-font-mono: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
537
866
 
538
867
  /* ---- weights ----
@@ -561,6 +890,25 @@
561
890
 
562
891
  Stepped down one notch 2026-08-06 (operator: main headings read too
563
892
  dominant). The SOURCE values were xl 64 / lg 48 / md 42 / sm 32. */
893
+ /*
894
+ SOURCE: home 8204:2556, the hero headline, Tanker at leading-none.
895
+
896
+ The largest rung in the system and the only node that asks for it. It is kept
897
+ separate from display-2xl rather than collapsed onto it because the two are 28px
898
+ apart and both are heroes: home is 120 and the other five frames are 92. */
899
+ --cg-display-3xl: 120px;
900
+ /*
901
+ SOURCE: the hero headline on five of the six canonical frames — thegrid
902
+ 8364:4211, builders 6913:7652, teams 7172:7569, enterprise 7037:777, pricing
903
+ 7779:9259. Tanker, leading-none, no tracking.
904
+
905
+ This is the ramp's real top. Before it, --cg-display-xl (56px) was the largest
906
+ value in the package and every hero in the design was 36px above it, so each
907
+ consumer clamped its own. thegrid also draws a 96px closing CTA (8364:3957) and
908
+ a 64px section heading (8364:4363); both are single nodes and are deliberately
909
+ not rungs. A value invented to complete a grid is the thing this file keeps
910
+ refusing to add. */
911
+ --cg-display-2xl: 92px;
564
912
  --cg-display-xl: 56px;
565
913
  --cg-display-lg: 44px;
566
914
  /* SOURCE: onboarding H1 (was 42px) */
@@ -568,6 +916,65 @@
568
916
  --cg-display-sm: 30px;
569
917
  --cg-display-line: 1.06;
570
918
  --cg-display-tracking: 0.012em;
919
+ /*
920
+ The leading every Tanker node in every canonical frame actually uses: 1, i.e.
921
+ leading-none. Not one display node in six frames emits a letter-spacing either.
922
+
923
+ --cg-display-line stays 1.06 and --cg-display-tracking stays 0.012em. Both were
924
+ tuned against Saira Semi Condensed 800 (ui#240 records this), neither has been
925
+ retuned for Tanker, and both are shipped values that four display rungs and
926
+ every consumer's display utility already read. Retuning them is a one-line edit
927
+ and a visible change to text that exists today, so it is proposed rather than
928
+ made. This rung is what the new sizes are built on in the meantime. */
929
+ --cg-display-line-tight: 1;
930
+
931
+ /* ---- tanker-title-ramp ----
932
+ The display face at CARD scale, 2026-09-18.
933
+
934
+ Four rungs the canonical frames use constantly and this package could spell
935
+ none of. They are separated from h1-display-ramp rather than wedged into it
936
+ because the two ramps disagree everywhere they overlap, and the disagreement is
937
+ not resolvable by addition:
938
+
939
+ frames 120 96 92 64 56 40 38 32 30 26 20
940
+ package 56 44 38 30
941
+
942
+ 56, 38 and 30 coincide. 44 is used by nothing in any of the six frames. Every
943
+ other size the frames ask for falls BETWEEN two shipped rungs — 40 between 44
944
+ and 38, 26 between 30 and heading-lg's 22 — so there is no name left on the
945
+ t-shirt ramp that is both free and in the right place, and renaming a shipped
946
+ rung is a breaking change for two published consumers.
947
+
948
+ They are also not headings. --cg-heading-* is the Manrope ramp at 1.2 leading;
949
+ this is the display FACE at leading-none, and --cg-title-sm and
950
+ --cg-heading-md are both 20px precisely because a face and a leading are what
951
+ separate them, not a size.
952
+
953
+ SIZES, with where they were read:
954
+ 40 home 8204:2589, enterprise 7038:1244, pricing 7785:9809, thegrid 8364:4382
955
+ 26 teams 7297:8606, builders 6916:8648, enterprise 7038:1192,
956
+ pricing 7788:10264, thegrid 8364:3882
957
+ 20 teams 7172:7760, builders 6919:8783, thegrid petal labels
958
+
959
+ pricing 7785:10059 draws one 32px Tanker number and thegrid draws 15px Tanker
960
+ eyebrows. Single-frame, so recorded and not given a rung. */
961
+ /*
962
+ Four frames. The governance card title, the plan price, the sub-heading under a section statement. */
963
+ --cg-title-lg: 40px;
964
+ /*
965
+ Five frames. The card title and the FAQ question — the most common display size in the design after the section heading.
966
+
967
+ ui#239's type table calls this rung "24px card title". Measured on five frames it is 26, not 24, and nothing in any frame is 24. The table was written from a consumer's approximation rather than from the node. */
968
+ --cg-title-md: 26px;
969
+ /*
970
+ Three frames. The label inside a step circle or a diagram petal. Same size as --cg-heading-md and a different face and leading; see the group note. */
971
+ --cg-title-sm: 20px;
972
+ /*
973
+ leading-none, aliased rather than repeated so a retune of the display leading reaches both ramps. An alias compiles to var(), so it survives a scope flip. */
974
+ --cg-title-line: var(--cg-display-line-tight);
975
+ /*
976
+ No Tanker node in any of the six frames emits a letter-spacing. Spelled out rather than omitted so a utility built on this ramp cannot inherit --cg-display-tracking's 0.012em by accident. */
977
+ --cg-title-tracking: 0em;
571
978
 
572
979
  /* ---- manrope-ramp ---- */
573
980
  /*
@@ -589,6 +996,22 @@
589
996
  extrapolation, and a heading is short enough that 1.2 does not cost a line its
590
997
  descenders. */
591
998
  --cg-heading-line: 1.2;
999
+ /*
1000
+ SOURCE: the lede paragraph on all six canonical frames — home 8204:2561,
1001
+ thegrid 8364:4213, builders 6913:7654, teams 7172:7571, enterprise 7037:779,
1002
+ pricing 7779:9261. Space Grotesk regular, line-height 1.46, no tracking.
1003
+
1004
+ The single most-used text size in the whole design and the one this package
1005
+ could not spell. --cg-body-lg is 19px and --cg-heading-md is 20px, so a
1006
+ consumer building a lede had to pick between a body token one pixel short and a
1007
+ heading token that is the right size with the wrong role and the wrong leading
1008
+ (1.2 against 1.46). cloudgrid-landing#145 records sections doing the first.
1009
+
1010
+ It reads --cg-body-line, so it is a body rung and not a fifth display rung.
1011
+
1012
+ pricing 7785:9832 draws one 18px bold plan name. Single node, one frame, 1px
1013
+ off --cg-body-lg: recorded, not given a rung. */
1014
+ --cg-body-xl: 20px;
592
1015
  /* SOURCE: H1 subline */
593
1016
  --cg-body-lg: 19px;
594
1017
  /* SOURCE: default UI text */
@@ -609,6 +1032,43 @@
609
1032
  --cg-mono-line: 1.7;
610
1033
  --cg-label-size: 15px;
611
1034
  --cg-label-weight: var(--cg-weight-medium);
1035
+ /*
1036
+ SOURCE: Figma's own bound text variable, "Eyebrow" —
1037
+ Font(family: "Manrope", style: SemiBold, size: 14, weight: 600, lineHeight: 1.2,
1038
+ letterSpacing: 10). Applied first-hand on teams 7172:8463, builders 6916:8053
1039
+ and enterprise 7038:1184, each rendering tracking-[1.4px] uppercase.
1040
+
1041
+ The only rung in this package derived from a Figma TEXT variable rather than
1042
+ from a measured node, and it agrees with the nodes, which is why it is trusted.
1043
+
1044
+ It is equidistant from the two tokens a consumer would otherwise reach for:
1045
+ --cg-caption is 13px and --cg-label-size is 15px, and Eyebrow currently builds
1046
+ on the second. One pixel is not the point — the eyebrow also carries a weight,
1047
+ a leading and a tracking that neither of those tokens has, and spelling three
1048
+ of the four inline is how a component drifts.
1049
+
1050
+ KNOWN: thegrid draws its eyebrows differently — Tanker regular 15px, leading
1051
+ 16px, tracking 1.1px (8364:3905, 8364:4409) — and both of pricing's eyebrows are
1052
+ switched off in the file. So this is the eyebrow of three frames, not six, and
1053
+ thegrid's is an open question for the designer rather than a second rung here. */
1054
+ --cg-eyebrow-size: 14px;
1055
+ /*
1056
+ From the Eyebrow variable. The same number as --cg-heading-line, spelled out rather than aliased: an eyebrow is not a heading, and a heading retune must not silently move it. */
1057
+ --cg-eyebrow-line: 1.2;
1058
+ /*
1059
+ From the Eyebrow variable, whose letterSpacing is 10 — Figma's percent, not pixels. At 14px that is 1.4px, which is what all three frames render. Stored in em so the ratio survives a size change. */
1060
+ --cg-eyebrow-tracking: 0.1em;
1061
+ /*
1062
+ 600, from the Eyebrow variable's weight: 600.
1063
+
1064
+ Aliased onto --cg-weight-bold rather than written as a number, because 600 is
1065
+ already this package's "bold" (the weights group's own note explains why that
1066
+ name and not "semibold"). Two spellings of one weight is how a ramp starts
1067
+ disagreeing with itself.
1068
+
1069
+ Note this is NOT --cg-label-weight, which is 500. The eyebrow and the label are
1070
+ different rungs; see the group note. */
1071
+ --cg-eyebrow-weight: var(--cg-weight-bold);
612
1072
 
613
1073
  /* ---- spacing ----
614
1074
  spacing: 4px base */
@@ -657,8 +1117,44 @@
657
1117
  --cg-radius-lg: 14px;
658
1118
  /* SOURCE: option card */
659
1119
  --cg-radius-xl: 16px;
1120
+ /*
1121
+ SOURCE: the corner of every section slab and every card in the design, read on
1122
+ all six canonical frames — home 8204:2440, thegrid 8364:3928, builders
1123
+ 6913:8019, teams 7172:7567, enterprise 7037:775, pricing 7785:10051.
1124
+
1125
+ It is the most-used radius in the whole design and the ramp had no name for it.
1126
+ --cg-radius-xl is 16 and --cg-radius-2xl is 24, and both are shipped, so 20 has
1127
+ no free t-shirt name in the right place. Renaming a rung two published
1128
+ consumers already read is a breaking change for a corner.
1129
+
1130
+ So it takes a ROLE name, which the corners group already does once for
1131
+ --cg-radius-pill. The role is exact and worth stating: in these frames 16 is
1132
+ what a CONTROL is rounded to (every button on every frame is 16) and 20 is what
1133
+ a CONTAINER is rounded to. The design draws that distinction consistently, which
1134
+ is more information than a number between two numbers.
1135
+
1136
+ NOT --cg-radius-panel, deliberately: "panel" in this package is the purple
1137
+ surface (--cg-surface-panel, data-cg-surface="panel"), and a radius named after
1138
+ it would read as that surface's corner rather than as every container's. */
1139
+ --cg-radius-container: 20px;
660
1140
  /* SOURCE: full-bleed purple panel */
661
1141
  --cg-radius-2xl: 24px;
1142
+ /*
1143
+ SOURCE: home 8204:2557 and 8204:2569, the two 92px lime image cards in the hero.
1144
+
1145
+ One frame, two nodes, and it is here rather than recorded-and-skipped for one
1146
+ reason: it extends the t-shirt ramp at the TOP, where there is a free name and
1147
+ no ambiguity, instead of asking for one between two shipped rungs.
1148
+
1149
+ WHAT IS NOT HERE. The brief this work came from expected a rung between
1150
+ --cg-radius-2xl and --cg-radius-pill for a frame nesting 108 / 93 / 80px
1151
+ corners. No radius above 60 and below the pill appears on any of the six frames
1152
+ first-hand. builders draws 60px on three decorative glow rectangles
1153
+ (7172:8460-8462) and thegrid draws 22 / 18 / 15 / 11 / 9 inside a flattened
1154
+ product-UI mock. None of those is a rung; all are recorded here so the next
1155
+ audit does not re-find them. The home frame is only ~10% audited (the Figma
1156
+ quota ran out), so if that expectation has a node behind it, it is there. */
1157
+ --cg-radius-3xl: 32px;
662
1158
  --cg-radius-pill: 999px;
663
1159
  --cg-border-width: 1px;
664
1160
  --cg-border-width-strong: 1.5px;
@@ -844,6 +1340,12 @@
844
1340
  --cg-text-primary: #ffffff;
845
1341
  --cg-text-secondary: rgba(255, 255, 255, 0.82);
846
1342
  --cg-text-muted: rgba(255, 255, 255, 0.66);
1343
+ /*
1344
+ 4.75:1 on --cg-surface-panel. See the base note: on this ground nothing dimmer
1345
+ than 0.84 white clears the 4.5:1 floor, so this rung is necessarily brighter
1346
+ than the panel's own --cg-text-secondary (0.82, 4.47:1). The surface is what
1347
+ would have to move to change that, not this value. */
1348
+ --cg-text-muted-strong: rgba(255, 255, 255, 0.86);
847
1349
  --cg-text-accent: #ffffff;
848
1350
 
849
1351
  /* ---- semantic-lines ---- */
@@ -945,6 +1447,14 @@
945
1447
  --cg-text-primary: #ffffff;
946
1448
  --cg-text-secondary: rgba(255, 255, 255, 0.78);
947
1449
  --cg-text-muted: rgba(255, 255, 255, 0.56);
1450
+ /*
1451
+ 8.02:1 on --cg-surface-code. Ink is the one ground where --cg-text-muted
1452
+ already clears the body floor (5.92:1), so this rung exists there for
1453
+ consistency of meaning rather than to fix a ratio: a component that reaches for
1454
+ text-faint-strong must resolve to something on all three surfaces, and
1455
+ resolving to the base #6a6a6a inside a dark scope would be the CLAUDE.md rule-3
1456
+ failure. It sits between the ink scope's muted (0.56) and its secondary (0.78). */
1457
+ --cg-text-muted-strong: rgba(255, 255, 255, 0.68);
948
1458
  --cg-text-accent: var(--cg-secondary);
949
1459
 
950
1460
  /* ---- semantic-surfaces ---- */