okchroma 0.8.2 → 0.9.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.
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  LINK_GROUP: () => LINK_GROUP,
30
30
  LINK_POSTURES: () => LINK_POSTURES,
31
31
  LINK_STATES: () => LINK_STATES,
32
+ LINK_STEP: () => LINK_STEP,
32
33
  MODE_SPECS: () => MODE_SPECS,
33
34
  OFFSET_ALPHAS: () => OFFSET_ALPHAS,
34
35
  OUTLINE_HOVER_ALPHA: () => OUTLINE_HOVER_ALPHA,
@@ -43,9 +44,7 @@ __export(index_exports, {
43
44
  STAMP_FILL: () => STAMP_FILL,
44
45
  STAMP_FILL_HOVER: () => STAMP_FILL_HOVER,
45
46
  STAMP_FILL_PRESSED: () => STAMP_FILL_PRESSED,
46
- STAMP_LEAF: () => STAMP_LEAF,
47
47
  STAMP_ON: () => STAMP_ON,
48
- STAMP_STATE_LEAVES: () => STAMP_STATE_LEAVES,
49
48
  SUBTLE_PASTEL_K: () => SUBTLE_PASTEL_K,
50
49
  SUBTLE_TINT_MULT: () => SUBTLE_TINT_MULT,
51
50
  absolutePath: () => absolutePath,
@@ -66,6 +65,7 @@ __export(index_exports, {
66
65
  generateScale: () => generateScale,
67
66
  generateSubtleSecondary: () => generateSubtleSecondary,
68
67
  groupEntries: () => groupEntries,
68
+ linkLeaf: () => linkLeaf,
69
69
  linkPath: () => linkPath,
70
70
  neutralCss: () => neutralCss,
71
71
  neutralTintHue: () => neutralTintHue,
@@ -114,13 +114,13 @@ function stateStepL(restL, _mode, k) {
114
114
  }
115
115
  var OVERRIDE_LIGHT_BELOW = ARCHETYPES.find((a) => a.name === "rich").min;
116
116
  var OVERRIDE_DARK_ABOVE = ARCHETYPES.find((a) => a.name === "light").min;
117
- function stateFillL(restL, mode, k) {
117
+ function stateFillL(restL, mode, k, keepsPole) {
118
118
  const natural = mode === "dark" ? 1 : -1;
119
119
  const overridden = mode === "light" ? restL < OVERRIDE_LIGHT_BELOW : restL > OVERRIDE_DARK_ABOVE;
120
120
  const preferred = overridden ? -natural : natural;
121
121
  const fits = (d) => {
122
122
  const p = restL + d * stateStepL(restL, mode, 2);
123
- return p >= STATE_L_MIN && p <= STATE_L_MAX;
123
+ return p >= STATE_L_MIN && p <= STATE_L_MAX && (keepsPole?.(p) ?? true);
124
124
  };
125
125
  const dir = fits(preferred) ? preferred : fits(-preferred) ? -preferred : preferred;
126
126
  const L = restL + dir * stateStepL(restL, mode, k);
@@ -838,32 +838,20 @@ function stopTokenName(stop) {
838
838
  }
839
839
  var PAPER_0 = "paper-0";
840
840
  var PEN_100 = "pen-100";
841
- var STAMP_FILL = "stamp-fill";
842
- var STAMP_FILL_HOVER = "stamp-fill-hover";
843
- var STAMP_FILL_PRESSED = "stamp-fill-pressed";
841
+ var STAMP_FILL = "stamp-0";
842
+ var STAMP_FILL_HOVER = "stamp-1";
843
+ var STAMP_FILL_PRESSED = "stamp-2";
844
844
  var STAMP_EDGE = "stamp-edge";
845
845
  var STAMP_ON = "stamp-on";
846
- var STAMP_STATE_LEAVES = {
847
- [STAMP_FILL]: "stamp/fill",
848
- [STAMP_FILL_HOVER]: "stamp/fill-hover",
849
- [STAMP_FILL_PRESSED]: "stamp/fill-pressed",
850
- [STAMP_EDGE]: "stamp/edge",
851
- [STAMP_ON]: "stamp/on"
852
- };
853
- var STAMP_LEAF = {
854
- FILL: STAMP_STATE_LEAVES[STAMP_FILL],
855
- FILL_HOVER: STAMP_STATE_LEAVES[STAMP_FILL_HOVER],
856
- FILL_PRESSED: STAMP_STATE_LEAVES[STAMP_FILL_PRESSED],
857
- EDGE: STAMP_STATE_LEAVES[STAMP_EDGE],
858
- ON: STAMP_STATE_LEAVES[STAMP_ON]
859
- };
860
846
  var cssVarName = (path) => `--${path.join("-")}`;
861
847
  var figmaPathOf = (path) => path.join("/");
862
- var familyPath = (family, leaf) => [family, ...(STAMP_STATE_LEAVES[leaf] ?? leaf).split("/")];
848
+ var familyPath = (family, leaf) => [family, leaf];
863
849
  var LINK_GROUP = "link";
864
850
  var LINK_POSTURES = ["default", "inverse"];
865
851
  var LINK_STATES = ["enabled", "hover", "pressed"];
866
- var linkPath = (posture, state) => [LINK_GROUP, posture, state];
852
+ var LINK_STEP = { enabled: 0, hover: 1, pressed: 2 };
853
+ var linkLeaf = (posture, state) => `${posture}-${LINK_STEP[state]}`;
854
+ var linkPath = (posture, state) => [LINK_GROUP, linkLeaf(posture, state)];
867
855
  var ABSOLUTE_GROUP = "absolute";
868
856
  var absolutePath = (family) => [ABSOLUTE_GROUP, family];
869
857
  var COLOR_GROUP = "color";
@@ -2236,6 +2224,20 @@ function ctaDarkDualGateL(base, darkH, onFillTextIsWhite, enforce, coLc, capLoL,
2236
2224
  const lLc = findLForBlackTextLc(startL, C2, darkH, coLc + APCA_SOLVE_MARGIN_LC, capHiL);
2237
2225
  return Math.max(l45, Math.min(lLc, capHiL));
2238
2226
  }
2227
+ function stepKeepsPole(mode, white, chromaAt, H2, law) {
2228
+ if (mode === "light") return void 0;
2229
+ if (law.enforceLc !== void 0) {
2230
+ const bar = law.enforceLc;
2231
+ const lcAt = white ? whiteTextLcAt : blackTextLcAt;
2232
+ return (L) => lcAt(L, chromaAt(L), H2) >= bar;
2233
+ }
2234
+ if (law.ratioFloor === void 0) return void 0;
2235
+ const floor = law.ratioFloor, poleY = white ? 1 : 0;
2236
+ return (L) => {
2237
+ const C2 = clampChromaToGamut(L, chromaAt(L), H2);
2238
+ return Math.min(legalRatio(L, C2, H2, poleY), contrastRatio(shippedY(L, C2, H2), poleY)) >= floor;
2239
+ };
2240
+ }
2239
2241
  function onFillIsWhiteDarkAt(L, C2, H2, enforce, ratioFloor) {
2240
2242
  return onTextIsWhite(apcaYAt(L, C2, H2), L, C2, H2, enforce, ratioFloor);
2241
2243
  }
@@ -2518,7 +2520,8 @@ function resolveRamp(hex, mode, spec, opts) {
2518
2520
  const enforceLc = spec.ons.onFill.enforceLc;
2519
2521
  const coLc = ctx.opts?.apcaClearance && enforceLc === void 0 ? ctx.opts.apcaClearanceLc ?? spec.ons.onFill.coEnforceLc : void 0;
2520
2522
  const onFloor = enforceLc !== void 0 ? void 0 : spec.ons.onFill.ratioFloor;
2521
- let cta, ctaHover, ctaPressed, onFillIsWhite;
2523
+ let cta, onFillIsWhite;
2524
+ let stateC, stateH;
2522
2525
  if (mode === "light") {
2523
2526
  onFillIsWhite = onFillIsWhiteLight(ctx, enforceLc !== void 0 ? false : onFillEnforce);
2524
2527
  let light9L = enforceLc !== void 0 ? ctaLightLApca(ctx, onFillIsWhite, onFillEnforce, enforceLc) : ctaLightL(ctx, onFillIsWhite, onFillEnforce);
@@ -2550,21 +2553,14 @@ function resolveRamp(hex, mode, spec, opts) {
2550
2553
  }
2551
2554
  }
2552
2555
  cta = emitRole(STAMP_FILL, light9L, ctaCFor(light9L) * ctaCMul, ctaH);
2553
- const hCFor = (L) => ctx.cAt("light", L, (hoverReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul;
2554
- const pCFor = (L) => ctx.cAt("light", L, (pressedReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul;
2555
- const hL = stateFillL(light9L, "light", 1);
2556
- const pL = stateFillL(light9L, "light", 2);
2557
- ctaHover = emitRole(STAMP_FILL_HOVER, hL, hCFor(hL), ctaH);
2558
- ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, pCFor(pL), ctaH);
2559
- if (light9L !== ctx.scaleL) {
2560
- cta.enforced = true;
2561
- ctaHover.enforced = true;
2562
- ctaPressed.enforced = true;
2563
- }
2556
+ stateC = {
2557
+ hover: (L) => ctx.cAt("light", L, (hoverReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul,
2558
+ pressed: (L) => ctx.cAt("light", L, (pressedReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul
2559
+ };
2560
+ stateH = ctaH;
2561
+ if (light9L !== ctx.scaleL) cta.enforced = true;
2564
2562
  if (repelled) {
2565
2563
  cta.repelled = true;
2566
- ctaHover.repelled = true;
2567
- ctaPressed.repelled = true;
2568
2564
  onFillIsWhite = onFillIsWhiteDarkAt(
2569
2565
  cta.L,
2570
2566
  cta.C,
@@ -2577,50 +2573,29 @@ function resolveRamp(hex, mode, spec, opts) {
2577
2573
  const d = dctx;
2578
2574
  const cta9L = ctx.opts?.darkCtaFlatApp !== void 0 ? flatDarkCtaL(d, (L) => ctx.cAt("dark", L, d.darkC9), ctx.darkCtaH, ctx.opts.darkCtaFlatApp) : d.dark9L;
2579
2575
  const dCFor = (L) => ctx.cAt("dark", L, d.darkC9);
2580
- const dTrio = (baseL) => [stateFillL(baseL, "dark", 1), stateFillL(baseL, "dark", 2)];
2576
+ stateC = { hover: dCFor, pressed: dCFor };
2577
+ stateH = ctx.darkCtaH;
2581
2578
  cta = emitRole(STAMP_FILL, cta9L, dCFor(cta9L), ctx.darkCtaH);
2582
- {
2583
- const [hL, pL] = dTrio(cta9L);
2584
- ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2585
- ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2586
- }
2587
2579
  onFillIsWhite = onFillIsWhiteDarkAt(cta.L, cta.C, cta.H, enforceLc !== void 0 ? false : onFillEnforce);
2588
2580
  const enforcedL = enforceLc !== void 0 ? ctaDarkEnforcedLApca(ctx, cta, onFillIsWhite, onFillEnforce, enforceLc) : ctaDarkEnforcedL(ctx, cta, onFillIsWhite, onFillEnforce);
2589
2581
  if (enforcedL !== null) {
2590
2582
  cta = emitRole(STAMP_FILL, enforcedL, dCFor(enforcedL), ctx.darkCtaH);
2591
- const [hL, pL] = dTrio(enforcedL);
2592
- ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2593
- ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2594
2583
  cta.enforced = true;
2595
- ctaHover.enforced = true;
2596
- ctaPressed.enforced = true;
2597
2584
  }
2598
2585
  if (coLc !== void 0) {
2599
2586
  const [capLoL, capHiL] = CTA_CLEARANCE_CAPS;
2600
2587
  const clearedL = ctaDarkDualGateL(cta, ctx.darkCtaH, onFillIsWhite, onFillEnforce, coLc + APCA_ENFORCE_MARGIN_LC, capLoL, capHiL);
2601
2588
  if (Math.abs(clearedL - cta.L) > 1e-9) {
2602
2589
  cta = emitRole(STAMP_FILL, clearedL, dCFor(clearedL), ctx.darkCtaH);
2603
- const [hL, pL] = dTrio(clearedL);
2604
- ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2605
- ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2606
2590
  cta.enforced = true;
2607
- ctaHover.enforced = true;
2608
- ctaPressed.enforced = true;
2609
2591
  }
2610
2592
  }
2611
2593
  if (ctx.opts?.ctaSolve) {
2612
2594
  const exitL = solveDarkCtaExit(cta, dCFor, ctx.darkCtaH, ctx.opts.ctaSolve.redDark, enforceLc, coLc);
2613
2595
  if (exitL !== null) {
2614
2596
  cta = emitRole(STAMP_FILL, exitL, dCFor(exitL), ctx.darkCtaH);
2615
- const [hL, pL] = dTrio(exitL);
2616
- ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2617
- ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2618
2597
  cta.enforced = true;
2619
- ctaHover.enforced = true;
2620
- ctaPressed.enforced = true;
2621
2598
  cta.repelled = true;
2622
- ctaHover.repelled = true;
2623
- ctaPressed.repelled = true;
2624
2599
  onFillIsWhite = onFillIsWhiteDarkAt(
2625
2600
  cta.L,
2626
2601
  cta.C,
@@ -2636,6 +2611,19 @@ function resolveRamp(hex, mode, spec, opts) {
2636
2611
  const other = legalRatio(cta.L, cta.C, cta.H, onFillIsWhite ? 0 : 1);
2637
2612
  if (chosen < onFloor && other >= onFloor) onFillIsWhite = !onFillIsWhite;
2638
2613
  }
2614
+ const keepsPole = stepKeepsPole(mode, onFillIsWhite, stateC.pressed, stateH, { ratioFloor: onFloor, enforceLc });
2615
+ const hL = stateFillL(cta.L, mode, 1, keepsPole);
2616
+ const pL = stateFillL(cta.L, mode, 2, keepsPole);
2617
+ const ctaHover = emitRole(STAMP_FILL_HOVER, hL, stateC.hover(hL), stateH);
2618
+ const ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, stateC.pressed(pL), stateH);
2619
+ if (cta.enforced) {
2620
+ ctaHover.enforced = true;
2621
+ ctaPressed.enforced = true;
2622
+ }
2623
+ if (cta.repelled) {
2624
+ ctaHover.repelled = true;
2625
+ ctaPressed.repelled = true;
2626
+ }
2639
2627
  return { mode, seed, stops, roles: { cta, ctaHover, ctaPressed }, ons: { onFillIsWhite } };
2640
2628
  }
2641
2629
 
@@ -2718,18 +2706,18 @@ function generateNeutralScale(brandH, level = "default", contrastProfile) {
2718
2706
  if (popLift > 1e-6) {
2719
2707
  scale.ctaDark = makeStop(9, scale.dark[3].L + popLift, scale.dark[3].C, scale.dark[3].H);
2720
2708
  }
2721
- const nState = (stop, mode, rest, k) => {
2722
- const L = stateFillL(rest.L, mode, k);
2723
- return makeStop(stop, L, curve(L, mode), rest.H);
2724
- };
2725
- scale.ctaHover = nState(10, "light", scale.cta, 1);
2726
- scale.ctaPressed = nState(11, "light", scale.cta, 2);
2727
- scale.ctaHoverDark = nState(10, "dark", scale.ctaDark, 1);
2728
- scale.ctaPressedDark = nState(11, "dark", scale.ctaDark, 2);
2729
2709
  const onEnforce = contrastProfile !== "apca";
2730
2710
  const onFloor = 4.5;
2731
2711
  scale.onFillTextIsWhite = onTextIsWhite(apcaY(scale.cta.r, scale.cta.g, scale.cta.b), scale.cta.L, scale.cta.C, scale.cta.H, onEnforce, onFloor);
2732
2712
  scale.onFillTextIsWhiteDark = onTextIsWhite(apcaY(scale.ctaDark.r, scale.ctaDark.g, scale.ctaDark.b), scale.ctaDark.L, scale.ctaDark.C, scale.ctaDark.H, onEnforce, onFloor);
2713
+ const nState = (stop, mode, rest, k, white) => {
2714
+ const L = stateFillL(rest.L, mode, k, stepKeepsPole(mode, white, (l) => curve(l, mode), rest.H, { ratioFloor: onFloor }));
2715
+ return makeStop(stop, L, curve(L, mode), rest.H);
2716
+ };
2717
+ scale.ctaHover = nState(10, "light", scale.cta, 1, scale.onFillTextIsWhite);
2718
+ scale.ctaPressed = nState(11, "light", scale.cta, 2, scale.onFillTextIsWhite);
2719
+ scale.ctaHoverDark = nState(10, "dark", scale.ctaDark, 1, scale.onFillTextIsWhiteDark);
2720
+ scale.ctaPressedDark = nState(11, "dark", scale.ctaDark, 2, scale.onFillTextIsWhiteDark);
2733
2721
  return scale;
2734
2722
  }
2735
2723
  function generateSubtleSecondary(hex, opts) {
@@ -2742,10 +2730,6 @@ function generateSubtleSecondary(hex, opts) {
2742
2730
  });
2743
2731
  const asCta = (stop, src) => makeStop(stop, src.L, src.C, src.H);
2744
2732
  const mk = (stop, L, mode) => makeStop(stop, L, curve(L, mode), scale.brandH);
2745
- const st = (stop, mode, rest, k) => {
2746
- const L = stateFillL(rest.L, mode, k);
2747
- return makeStop(stop, L, curve(L, mode), rest.H);
2748
- };
2749
2733
  if (opts?.ctaL) {
2750
2734
  scale.cta = mk(9, opts.ctaL.light, "light");
2751
2735
  scale.ctaDark = mk(9, opts.ctaL.dark, "dark");
@@ -2753,14 +2737,18 @@ function generateSubtleSecondary(hex, opts) {
2753
2737
  scale.cta = asCta(9, scale.light[3]);
2754
2738
  scale.ctaDark = asCta(9, scale.dark[3]);
2755
2739
  }
2756
- scale.ctaHover = st(10, "light", scale.cta, 1);
2757
- scale.ctaPressed = st(11, "light", scale.cta, 2);
2758
- scale.ctaHoverDark = st(10, "dark", scale.ctaDark, 1);
2759
- scale.ctaPressedDark = st(11, "dark", scale.ctaDark, 2);
2760
2740
  const onEnforce = opts?.contrastProfile !== "apca";
2761
2741
  const onFloor = 4.5;
2762
2742
  scale.onFillTextIsWhite = onTextIsWhite(apcaY(scale.cta.r, scale.cta.g, scale.cta.b), scale.cta.L, scale.cta.C, scale.cta.H, onEnforce, onFloor);
2763
2743
  scale.onFillTextIsWhiteDark = onTextIsWhite(apcaY(scale.ctaDark.r, scale.ctaDark.g, scale.ctaDark.b), scale.ctaDark.L, scale.ctaDark.C, scale.ctaDark.H, onEnforce, onFloor);
2744
+ const st = (stop, mode, rest, k, white) => {
2745
+ const L = stateFillL(rest.L, mode, k, stepKeepsPole(mode, white, (l) => curve(l, mode), rest.H, { ratioFloor: onFloor }));
2746
+ return makeStop(stop, L, curve(L, mode), rest.H);
2747
+ };
2748
+ scale.ctaHover = st(10, "light", scale.cta, 1, scale.onFillTextIsWhite);
2749
+ scale.ctaPressed = st(11, "light", scale.cta, 2, scale.onFillTextIsWhite);
2750
+ scale.ctaHoverDark = st(10, "dark", scale.ctaDark, 1, scale.onFillTextIsWhiteDark);
2751
+ scale.ctaPressedDark = st(11, "dark", scale.ctaDark, 2, scale.onFillTextIsWhiteDark);
2764
2752
  return scale;
2765
2753
  }
2766
2754
 
@@ -2854,20 +2842,21 @@ var SCALE = {
2854
2842
  // ("heavy-emphasis": "high" and "low" are plane words in the community plugin and
2855
2843
  // "strong" carries "on")
2856
2844
  "pen-70": { req: "heavy-emphasis text, inverted backgrounds", contrast: AA_BODY, claim: claimOf("pen"), theming: solved, collides: true },
2857
- // the stamp family. "CTA" stays in these bodies on purpose: it is not a token label, so
2845
+ // the stamp rows, each a position: the fill at rest and at one and two steps, the edge,
2846
+ // the on-text. No body names a state; a consumer's semantic layer names the states and
2847
+ // aliases the steps. "CTA" stays in these bodies on purpose: it is not a token label, so
2858
2848
  // it floods nothing, and a designer's "cta" query lands on these rows.
2859
2849
  // the usage lines are the owner's guidance, for the document rendering: the fill is
2860
2850
  // misused as a standalone color in the wild
2861
- "stamp/fill": {
2862
- req: "CTAs",
2851
+ "stamp-0": {
2852
+ req: "CTAs, the fill at rest",
2863
2853
  theming: (f) => `fully re-solved per theme and family${COLOR_WORD[f]}`,
2864
- use: "primary button fills; optional for avatars, badges and other elements that carry text. Not a standalone color: it has no contrast guarantee of its own, only its on-text has one. Use it alone or with its hover and pressed states. Always bordered with its edge"
2854
+ use: "primary button fills; optional for avatars, badges and other elements that carry text. Not a standalone color: it has no contrast guarantee of its own, only its on-text has one. Use it alone or with the two steps above it. Always bordered with its edge"
2865
2855
  },
2866
- "stamp/fill-hover": { req: "CTA pointer-over state", theming: (f) => `follows its rest fill${COLOR_WORD[f]}`, use: "the pointer-over state of the fill, with the fill; for nothing else. Always bordered with the edge" },
2867
- "stamp/fill-pressed": { req: "CTA pressed state", theming: (f) => `follows its rest fill${COLOR_WORD[f]}`, use: "the pressed state of the fill, with the fill; for nothing else. Always bordered with the edge" },
2868
- "stamp/edge": { req: "min APCA visibility", theming: (f) => `draws for CTAs that sit close to the page; strength per family tier${COLOR_WORD[f]}`, use: "the border of the fill and its states, always rendered with them and never without them (it resolves to transparent unless the fill sits close to the page); for nothing else" },
2869
- // ("fill" is a label word and foreign here)
2870
- "stamp/on": { req: "text over the CTA color", contrast: `${AA_BODY} over its CTA`, theming: (f) => `whichever pole passes; quiet CTAs take the soft pole${COLOR_WORD[f]}`, use: "the only text color over the fill; never elsewhere" }
2856
+ "stamp-1": { req: "CTAs, the fill one step from rest", theming: (f) => `follows the fill at rest${COLOR_WORD[f]}`, use: "the fill one step from rest, with the fill at rest; for nothing else. Always bordered with the edge" },
2857
+ "stamp-2": { req: "CTAs, the fill two steps from rest", theming: (f) => `follows the fill at rest${COLOR_WORD[f]}`, use: "the fill two steps from rest, with the fill at rest; for nothing else. Always bordered with the edge" },
2858
+ "stamp-edge": { req: "min APCA visibility", theming: (f) => `draws for CTAs that sit close to the page; strength per family tier${COLOR_WORD[f]}`, use: "the border of the fill at every step, always rendered with it and never without it (it resolves to transparent unless the fill sits close to the page); for nothing else" },
2859
+ "stamp-on": { req: "text over the CTA color", contrast: `${AA_BODY} over its CTA`, theming: (f) => `whichever pole passes; quiet CTAs take the soft pole${COLOR_WORD[f]}`, use: "the only text color over the fill at every step; never elsewhere" }
2871
2860
  };
2872
2861
  var NEUTRAL_ONLY = {
2873
2862
  // the pole is measured against the neutral's own stops only; as text, it is the on-color
@@ -2876,15 +2865,15 @@ var NEUTRAL_ONLY = {
2876
2865
  // the literal pole carries no tint, so no theming line
2877
2866
  "pen-100": { req: "max-emphasis text", contrast: AA_BODY, claim: () => `${AA_BODY} on every paper and chalk of every family` }
2878
2867
  };
2879
- var LINK = (state, contrast) => ({
2880
- req: "links" + state,
2868
+ var LINK = (step, contrast) => ({
2869
+ req: "links" + step,
2881
2870
  contrast,
2882
2871
  claim: () => `${contrast} on every paper of the neutral`,
2883
2872
  theming: "rides the theme\u2019s link color; custom seed re-solves; overridable per theme",
2884
2873
  use: "hyperlinks; not a text-style CTA, which is the text stops read as states"
2885
2874
  });
2886
- var LINK_INVERSE = (state, contrast) => ({
2887
- req: "links over inverted backgrounds" + state,
2875
+ var LINK_INVERSE = (step, contrast) => ({
2876
+ req: "links over inverted backgrounds" + step,
2888
2877
  contrast,
2889
2878
  claim: () => `${contrast} on any family's pen-70`,
2890
2879
  theming: "same seed as the link, re-solved for inverted backgrounds; overridable per theme",
@@ -2893,12 +2882,12 @@ var LINK_INVERSE = (state, contrast) => ({
2893
2882
  var SYSTEM = {
2894
2883
  "absolute/brand": { req: "identity seed reference", theming: "the theme\u2019s own input, as given", use: "a reference value (logos, swatches of the input); never a UI color, it carries no contrast guarantee" },
2895
2884
  "absolute/brand-alt": { req: "identity seed reference", theming: "the theme\u2019s paired input, as given", use: "a reference value (logos, swatches of the input); never a UI color, it carries no contrast guarantee" },
2896
- "link/default/enabled": LINK("", AA_BODY),
2897
- "link/default/hover": LINK(" pointer-over", AA_BODY),
2898
- "link/default/pressed": LINK(" pressed", AA_BODY),
2899
- "link/inverse/enabled": LINK_INVERSE("", AA_BODY),
2900
- "link/inverse/hover": LINK_INVERSE(" pointer-over", AA_BODY),
2901
- "link/inverse/pressed": LINK_INVERSE(" pressed", AA_BODY)
2885
+ "link/default-0": LINK("", AA_BODY),
2886
+ "link/default-1": LINK(", one step from rest", AA_BODY),
2887
+ "link/default-2": LINK(", two steps from rest", AA_BODY),
2888
+ "link/inverse-0": LINK_INVERSE("", AA_BODY),
2889
+ "link/inverse-1": LINK_INVERSE(", one step from rest", AA_BODY),
2890
+ "link/inverse-2": LINK_INVERSE(", two steps from rest", AA_BODY)
2902
2891
  };
2903
2892
  var PREFIXES = [
2904
2893
  // community spellings first: the family word is a prefix of the community shapes
@@ -3335,11 +3324,12 @@ function escapeCtaFamily(nScale, mode, contrastProfile) {
3335
3324
  if (!strongText) throw new Error("escapeCtaFamily: the neutral scale has no ink-11 stop");
3336
3325
  const mk = (stop, L) => makeStop(stop, L, strongText.C, strongText.H);
3337
3326
  const cta = mk(9, strongText.L);
3338
- const ctaHover = mk(10, stateFillL(strongText.L, mode, 1));
3339
- const ctaPressed = mk(11, stateFillL(strongText.L, mode, 2));
3340
3327
  const onEnforce = contrastProfile !== "apca";
3341
3328
  const onFloor = contrastProfile === "apca" ? void 0 : 4.5;
3342
3329
  const onFillIsWhite = onTextIsWhite(apcaY(cta.r, cta.g, cta.b), cta.L, cta.C, cta.H, onEnforce, onFloor);
3330
+ const keepsPole = stepKeepsPole(mode, onFillIsWhite, () => strongText.C, strongText.H, { ratioFloor: onFloor });
3331
+ const ctaHover = mk(10, stateFillL(strongText.L, mode, 1, keepsPole));
3332
+ const ctaPressed = mk(11, stateFillL(strongText.L, mode, 2, keepsPole));
3343
3333
  return { cta, ctaHover, ctaPressed, onFillIsWhite };
3344
3334
  }
3345
3335
  var SECONDARY_DISTINCT_DELTA_E = 0.12;
@@ -3889,9 +3879,6 @@ function colorFromHexString(hex) {
3889
3879
  const h = hex.replace("#", "");
3890
3880
  return color(parseInt(h.slice(0, 2), 16) / 255, parseInt(h.slice(2, 4), 16) / 255, parseInt(h.slice(4, 6), 16) / 255);
3891
3881
  }
3892
- function bandedLeaf(flat) {
3893
- return STAMP_STATE_LEAVES[flat] ?? flat;
3894
- }
3895
3882
  function groupEntries(g2) {
3896
3883
  const entries = Object.entries(g2);
3897
3884
  const digitLeading = (k) => /^\d/.test(k);
@@ -3899,24 +3886,15 @@ function groupEntries(g2) {
3899
3886
  return entries.sort((a, b) => parseInt(b[0], 10) - parseInt(a[0], 10));
3900
3887
  return entries;
3901
3888
  }
3902
- function putLeaf(g2, flat, tok) {
3903
- const path = bandedLeaf(flat).split("/");
3904
- let cur = g2;
3905
- for (const seg of path.slice(0, -1)) {
3906
- if (!(seg in cur) || "$type" in cur[seg]) cur[seg] = {};
3907
- cur = cur[seg];
3908
- }
3909
- cur[path[path.length - 1]] = tok;
3910
- }
3911
3889
  function rampGroup(stops, onFillWhite, extra) {
3912
3890
  const g2 = {};
3913
3891
  const leaves = stops.map((s) => ({ name: stopTokenName(s.stop), tok: colorFromStop(s) })).sort((a, b) => tokenOrder(a.name) - tokenOrder(b.name));
3914
- for (const l of leaves) putLeaf(g2, l.name, l.tok);
3915
- if (extra?.cta) putLeaf(g2, STAMP_FILL, colorFromStop(extra.cta));
3916
- if (extra?.ctaHover) putLeaf(g2, STAMP_FILL_HOVER, colorFromStop(extra.ctaHover));
3917
- if (extra?.ctaPressed) putLeaf(g2, STAMP_FILL_PRESSED, colorFromStop(extra.ctaPressed));
3918
- if (extra?.cta) putLeaf(g2, STAMP_EDGE, extra.ctaBorder ?? TRANSPARENT_TOKEN);
3919
- putLeaf(g2, STAMP_ON, pole(onFillWhite));
3892
+ for (const l of leaves) g2[l.name] = l.tok;
3893
+ if (extra?.cta) g2[STAMP_FILL] = colorFromStop(extra.cta);
3894
+ if (extra?.ctaHover) g2[STAMP_FILL_HOVER] = colorFromStop(extra.ctaHover);
3895
+ if (extra?.ctaPressed) g2[STAMP_FILL_PRESSED] = colorFromStop(extra.ctaPressed);
3896
+ if (extra?.cta) g2[STAMP_EDGE] = extra.ctaBorder ?? TRANSPARENT_TOKEN;
3897
+ g2[STAMP_ON] = pole(onFillWhite);
3920
3898
  return g2;
3921
3899
  }
3922
3900
  function themeToFigma(r, input) {
@@ -3955,16 +3933,18 @@ function themeToFigma(r, input) {
3955
3933
  if (input.secondaryStyle === "outline" && input.secondary) {
3956
3934
  const s8 = secondary[mode].find((s) => s.stop === 8);
3957
3935
  const s9 = secondary[mode].find((s) => s.stop === 9);
3958
- putLeaf(secondaryGroup, STAMP_FILL, TRANSPARENT_TOKEN);
3936
+ secondaryGroup[STAMP_FILL] = TRANSPARENT_TOKEN;
3959
3937
  if (s8) {
3960
3938
  const e = srgbEmitChannels(s8);
3961
- putLeaf(secondaryGroup, STAMP_FILL_HOVER, color(e.r, e.g, e.b, OUTLINE_HOVER_ALPHA));
3962
- putLeaf(secondaryGroup, STAMP_FILL_PRESSED, color(e.r, e.g, e.b, OUTLINE_PRESSED_ALPHA));
3963
- putLeaf(secondaryGroup, STAMP_EDGE, colorFromStop(s8));
3939
+ secondaryGroup[STAMP_FILL_HOVER] = color(e.r, e.g, e.b, OUTLINE_HOVER_ALPHA);
3940
+ secondaryGroup[STAMP_FILL_PRESSED] = color(e.r, e.g, e.b, OUTLINE_PRESSED_ALPHA);
3941
+ secondaryGroup[STAMP_EDGE] = colorFromStop(s8);
3964
3942
  }
3965
- if (s9) putLeaf(secondaryGroup, STAMP_ON, colorFromStop(s9));
3943
+ if (s9) secondaryGroup[STAMP_ON] = colorFromStop(s9);
3966
3944
  }
3967
- const softOnCta = (g3, white) => putLeaf(g3, STAMP_ON, pole(white, SOFT_ON_CTA_ALPHA[mode]));
3945
+ const softOnCta = (g3, white) => {
3946
+ g3[STAMP_ON] = pole(white, SOFT_ON_CTA_ALPHA[mode]);
3947
+ };
3968
3948
  softOnCta(neutralGroup, mode === "light" ? nScale.onFillTextIsWhite : nScale.onFillTextIsWhiteDark);
3969
3949
  if (input.secondary && input.secondaryStyle !== "outline" && softOnCtaPasses(input.secondary, mode))
3970
3950
  softOnCta(secondaryGroup, mode === "light" ? secondaryOnFillLight : secondaryOnFillDark);
@@ -3972,10 +3952,10 @@ function themeToFigma(r, input) {
3972
3952
  const esc = input.ctaEscape ? escapeCtaFamily(nScale, mode, input.contrastProfile) : null;
3973
3953
  if (esc) {
3974
3954
  for (const g3 of input.secondary ? [brandGroup] : [brandGroup, secondaryGroup]) {
3975
- putLeaf(g3, STAMP_FILL, colorFromStop(esc.cta));
3976
- putLeaf(g3, STAMP_FILL_HOVER, colorFromStop(esc.ctaHover));
3977
- putLeaf(g3, STAMP_FILL_PRESSED, colorFromStop(esc.ctaPressed));
3978
- putLeaf(g3, STAMP_ON, pole(esc.onFillIsWhite));
3955
+ g3[STAMP_FILL] = colorFromStop(esc.cta);
3956
+ g3[STAMP_FILL_HOVER] = colorFromStop(esc.ctaHover);
3957
+ g3[STAMP_FILL_PRESSED] = colorFromStop(esc.ctaPressed);
3958
+ g3[STAMP_ON] = pole(esc.onFillIsWhite);
3979
3959
  }
3980
3960
  }
3981
3961
  const scaleTextAt = (n) => {
@@ -3985,7 +3965,7 @@ function themeToFigma(r, input) {
3985
3965
  };
3986
3966
  const trio = (posture) => {
3987
3967
  const t = posture === "default" ? lt ? mode === "light" ? [lt.link, lt.linkHover, lt.linkPressed] : [lt.linkDark, lt.linkHoverDark, lt.linkPressedDark] : [scaleTextAt(9), scaleTextAt(10), scaleTextAt(11)] : mode === "light" ? [invLt.link, invLt.linkHover, invLt.linkPressed] : [invLt.linkDark, invLt.linkHoverDark, invLt.linkPressedDark];
3988
- return Object.fromEntries(LINK_STATES.map((state, i) => [state, colorFromStop(t[i])]));
3968
+ return Object.fromEntries(LINK_STATES.map((state, i) => [linkLeaf(posture, state), colorFromStop(t[i])]));
3989
3969
  };
3990
3970
  const g2 = {
3991
3971
  [CSS_FAMILY.neutral]: neutralGroup,
@@ -4000,7 +3980,7 @@ function themeToFigma(r, input) {
4000
3980
  ctaFamily(sig.scale, mode, role)
4001
3981
  );
4002
3982
  }
4003
- g2[LINK_GROUP] = { default: trio("default"), inverse: trio("inverse") };
3983
+ g2[LINK_GROUP] = { ...trio("default"), ...trio("inverse") };
4004
3984
  g2[ABSOLUTE_GROUP] = {
4005
3985
  [CSS_FAMILY.brandPrimary]: colorFromHexString(scale.identityHex ?? invSeed),
4006
3986
  [CSS_FAMILY.brandSecondary]: colorFromHexString((input.secondary ?? scale).identityHex ?? invSeed)