@echelon-foundry/design-system 0.10.0 → 0.11.0-main.260.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/CHANGELOG.md CHANGED
@@ -7,6 +7,43 @@ published as GitHub release tarballs
7
7
  and to npm. Earlier releases are described in `docs/CONSUMING-FORMA.md` and
8
8
  `HANDOFF.md`.
9
9
 
10
+ ## Unreleased
11
+
12
+ **Behaviour change: workflow validation is stricter.** Diagram non-text contrast (R7-REM-009, `.praxis/inbox/documents/2026-10-29-R7-REM-009-diagram-nontext-contrast.md`).
13
+ Documents that validated before can now fail with `A11Y-NONTEXT-CONTRAST`, and `forma-workflow render` refuses them (exit 1). The status tokens are additive.
14
+
15
+ ### Migration
16
+ - Prefer Forma tokens for outlines and connectors: `token:status-*` or `token:border-functional` follow the theme and are not judged by the document rule.
17
+ - If the host pins a theme, pass it: `--theme light|dark` on `validate` and `render`, or `ValidationOptions.Theme` with `Validation.loadWith`. Default-surface contrast is then judged for that theme only.
18
+ - Otherwise adjust literal colours to reach 3:1 against the light (`#f2efe7`) and dark (`#171a18`) surfaces, or against the node's literal fill.
19
+
20
+ ### Added
21
+ - **Status colour tokens in the workflow vocabulary.** `token:status-warning`, `token:status-danger`, `token:status-success` and `token:status-info` are accepted wherever a workflow colour is (`fill`, `stroke`, `accent`, `foreground`, edge `stroke`, palette slots) and render to `var(--ef-color-status-*)`. The workflow schema enum and `contracts/diagram-presentation.json` list them. An application can draw an attention outline with a Forma token instead of a brand literal.
22
+ - **Validation rule `A11Y-NONTEXT-CONTRAST`** (error, accessibility; WCAG 1.4.11, 3:1). Literal (or literal-palette) colours on nodes, groups, legend swatches and edges. Without an authored fill, strokes, accents, legend swatches and edge strokes are judged against both default surfaces, light `#f2efe7` and dark `#171a18`, and the weaker theme is named; a pinned host theme (`--theme`, `ValidationOptions`) limits this to that theme's surface. With a literal fill, the accent must reach 3:1 against the fill; the stroke is reported only when it is below 3:1 against the fill and neither stroke nor fill reaches 3:1 against a default surface, so borderless filled shapes stay valid. Token values and token/literal mixes are not judged by the document rule; `tests/nontext-contrast.test.mjs` checks every status token and `border-functional` at 3:1 on surface-primary and surface-secondary in every shipped theme and brand. Rehearsal 6 regression: Causa's `#e9a23b` outline on `#f5f3ed` measured 1.95:1 and passed validation. See `docs/workflow/VALIDATION.md`, "Non-text contrast".
23
+ - **Contract.** `contracts/diagram-presentation.json` marks `fill`, `stroke`, `accent` and `connectorStroke` `"contrast": "consumer"` (as `foreground` already was): colours set through the CSS custom properties bypass document validation and must be measured by the application.
24
+ - **Browser regression.** `tests/browser/diagram-nontext-contrast.spec.mjs` measures real Forma.Workflow output (golden documents in `tests/fixtures/workflow-nontext/`, checked by the F# suite) in Chromium: status-token outlines, a diamond node and an edge reach 3:1 in light, dark and forced colours; the Rehearsal 6 document is reported by the validator and measures below 3:1.
25
+
26
+ ### Changed
27
+ - **Validation is stricter** (see Migration above).
28
+ - **`Validation.loadWith` / `ofJsonWith` / `semanticWith`** take `ValidationOptions` (`{ Theme }`); `load`, `ofJson` and `semantic` keep their behaviour with no pinned theme. The CLI's `validate` accepts `--theme`, and `render --theme` now also scopes validation.
29
+ - `examples/workflows/forma/workflows/colored.forma-workflow.json`: connector `c2` stroke `#2f5fb3` → `#3d6cc0`. The old value is 2.85:1 on the dark surface and is now an `A11Y-NONTEXT-CONTRAST` error. Existing documents with literal colours may newly fail this rule.
30
+
31
+
32
+ ## 0.11.0
33
+
34
+ Minor release: typography roles (EII-F, `.praxis/inbox/documents/2026-10-28-EII-F-typography-roles.md`).
35
+
36
+ ### Added
37
+ - **Typography roles.** `data-ef-type="application"`, `"dense"` and `"accessible"` select fixed heading and body steps from existing tokens: application h1/h2/h3 2xl/xl/lg with no ch caps; dense one step smaller with snug leading; accessible one step larger with relaxed leading. Marketing stays the default. Roles work in the `.ef-site` shell and on the application layer alone. See `docs/CONSUMING-FORMA.md`, "Typography roles".
38
+ - **Role tokens `--ef-type-measure-h1` / `--ef-type-measure-h2`.** They carry the existing 14ch/18ch marketing heading measures so a role can lift them.
39
+ - **`@echelon-foundry/forma-conformance`: rendered corner check.** `evaluateCornerContainment`, `resolveCornerRadius`, `measureCornerContainment` and `expectTextInsideCorners` port the layout probe's `boundary.corner` (LCE-001) to consumers. Released with the package's next version.
40
+ - **Browser regressions.** `tests/browser/type-roles.spec.mjs` (R5 heading scale, role steps, containment, overlap, clipping, focus, keyboard, 320-1280px, 200% text) and `tests/browser/corner-containment.spec.mjs` (LCE-001 padding cap under pill and 50% brand radii).
41
+
42
+ ### Fixed (opt-in)
43
+ - An application screen on the `.ef-site` shell no longer has to inherit the marketing display scale (h1 up to 7.25rem at 14ch), which the R5 rehearsal overrode locally.
44
+ - On the application layer, wrapped h1/h2 lines under a role use snug leading; with the core 1.02 line-height their glyph boxes overlapped at 200% text.
45
+
46
+
10
47
  ## 0.10.0
11
48
 
12
49
  Minor release: layout constraint enforcement (LCE-001, `experiments/LCE-001/REPORT.md`). `@echelon-foundry/forma-workflow` 1.1.0 ships the matching engine.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "format": "forma.color-contract/1.0.0",
3
- "formaVersion": "0.10.0",
3
+ "formaVersion": "0.11.0",
4
4
  "validationScope": "CSS var() references to --ef-color-* plus reserved internal color-related prefixes; other Forma APIs are governed by their own contracts.",
5
5
  "compatibility": {
6
6
  "policy": "Validate with the contract shipped by the pinned Forma package version.",
@@ -65,11 +65,11 @@
65
65
  }
66
66
  },
67
67
  "appearanceProperties": {
68
- "fill": { "customProperty": "--ef-diagram-fill", "appliesTo": ["node", "group", "legend"], "syntax": "color", "default": { "node": "--ef-color-surface-primary", "group": "transparent", "legend": "--ef-color-surface-primary" } },
69
- "stroke": { "customProperty": "--ef-diagram-stroke", "appliesTo": ["node", "group", "legend"], "syntax": "color", "default": { "node": "--ef-color-border-functional", "group": "--ef-color-border-functional", "legend": "--ef-color-border-functional" }, "print": "Authored stroke prints; otherwise black." },
70
- "accent": { "customProperty": "--ef-diagram-accent", "appliesTo": ["node", "group", "legend"], "syntax": "color", "default": { "node": "stroke", "group": "stroke", "legend": "stroke" } },
68
+ "fill": { "customProperty": "--ef-diagram-fill", "appliesTo": ["node", "group", "legend"], "syntax": "color", "default": { "node": "--ef-color-surface-primary", "group": "transparent", "legend": "--ef-color-surface-primary" }, "contrast": "consumer" },
69
+ "stroke": { "customProperty": "--ef-diagram-stroke", "appliesTo": ["node", "group", "legend"], "syntax": "color", "default": { "node": "--ef-color-border-functional", "group": "--ef-color-border-functional", "legend": "--ef-color-border-functional" }, "print": "Authored stroke prints; otherwise black.", "contrast": "consumer" },
70
+ "accent": { "customProperty": "--ef-diagram-accent", "appliesTo": ["node", "group", "legend"], "syntax": "color", "default": { "node": "stroke", "group": "stroke", "legend": "stroke" }, "contrast": "consumer" },
71
71
  "foreground": { "customProperty": "--ef-diagram-foreground", "appliesTo": ["node", "group"], "syntax": "color", "default": { "node": "--ef-color-text-primary", "group": "--ef-color-text-primary" }, "contrast": "consumer" },
72
- "connectorStroke": { "customProperty": "--ef-diagram-connector-stroke", "appliesTo": ["connector"], "syntax": "color", "default": { "connector": "--ef-color-border-functional" } },
72
+ "connectorStroke": { "customProperty": "--ef-diagram-connector-stroke", "appliesTo": ["connector"], "syntax": "color", "default": { "connector": "--ef-color-border-functional" }, "contrast": "consumer" },
73
73
  "connectorWidth": { "customProperty": "--ef-diagram-connector-width", "appliesTo": ["connector"], "syntax": "number", "range": [1, 8], "default": { "connector": "2" } }
74
74
  },
75
75
  "geometryProperties": {
@@ -97,7 +97,11 @@
97
97
  "--ef-color-surface-secondary",
98
98
  "--ef-color-surface-inverse",
99
99
  "--ef-color-text-primary",
100
- "--ef-color-text-secondary"
100
+ "--ef-color-text-secondary",
101
+ "--ef-color-status-warning",
102
+ "--ef-color-status-danger",
103
+ "--ef-color-status-success",
104
+ "--ef-color-status-info"
101
105
  ],
102
106
  "reset": "Remove the custom property to restore the Forma default (FMD-RESOLVE-006)."
103
107
  },
package/dist/all.css CHANGED
@@ -906,6 +906,108 @@
906
906
  :where(.ef-stack, .ef-grid, .ef-responsive-grid, .ef-priority-stack)[data-density] { gap: var(--ef-density-space); }
907
907
  @media (max-width: 48rem) { .ef-rail, .ef-rail[data-side="end"], .ef-focus-stage { grid-template-columns: minmax(0, 1fr); } .ef-mosaic { grid-template-columns: repeat(2, minmax(0, 1fr)); } .ef-span-2 { grid-column: 1 / -1; } }
908
908
  @media (max-width: 30rem) { .ef-mosaic { grid-template-columns: minmax(0, 1fr); } .ef-mosaic > [data-span="2"], .ef-mosaic > [data-span="full"] { grid-column: auto; } .ef-responsive-grid { --ef-responsive-grid-min: 100%; } .ef-priority-stack { --ef-priority-min: 100%; } }
909
+
910
+ /*
911
+ * Typography roles (EII-F). data-ef-type selects a role on any element,
912
+ * usually <html> or <body>; the nearest role wins:
913
+ * marketing the default (no attribute needed): the marketing layer's
914
+ * fluid display scale and heading measures, unchanged.
915
+ * application fixed steps for tools: h1 2xl, h2 xl, h3 lg, no ch caps.
916
+ * dense administrative density: one step smaller, snug leading.
917
+ * accessible large text: one step larger, relaxed leading.
918
+ * A role restates the type role tokens that the marketing layer
919
+ * (src/marketing/roles.css) and its themes declare, using only primitive
920
+ * steps. This layer follows echelon.tokens, so a role overrides a theme's
921
+ * :root role values; the .ef-site shell consumes the same tokens. The rules
922
+ * below give the application layer, which has no heading scale of its own,
923
+ * the same headings. Component headings (later layers) keep their sizes.
924
+ */
925
+ :where([data-ef-type="application"]) {
926
+ --ef-type-size-h1: var(--ef-primitive-font-size-2xl);
927
+ --ef-type-size-h2: var(--ef-primitive-font-size-xl);
928
+ --ef-type-size-h3: var(--ef-primitive-font-size-lg);
929
+ --ef-type-size-h4: var(--ef-primitive-font-size-md);
930
+ --ef-type-size-body: var(--ef-primitive-font-size-md);
931
+ --ef-type-size-lead: var(--ef-primitive-font-size-lg);
932
+ --ef-type-size-statement: var(--ef-primitive-font-size-xl);
933
+ --ef-type-size-small: var(--ef-primitive-font-size-sm);
934
+ --ef-type-size-label: var(--ef-primitive-font-size-xs);
935
+ --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
936
+ --ef-type-leading-snug: var(--ef-primitive-line-height-snug);
937
+ --ef-type-leading-body: var(--ef-primitive-line-height-normal);
938
+ --ef-type-leading-lead: var(--ef-primitive-line-height-normal);
939
+ --ef-type-measure-h1: none;
940
+ --ef-type-measure-h2: none;
941
+ }
942
+
943
+ :where([data-ef-type="dense"]) {
944
+ --ef-type-size-h1: var(--ef-primitive-font-size-xl);
945
+ --ef-type-size-h2: var(--ef-primitive-font-size-lg);
946
+ --ef-type-size-h3: var(--ef-primitive-font-size-md);
947
+ --ef-type-size-h4: var(--ef-primitive-font-size-sm);
948
+ --ef-type-size-body: var(--ef-primitive-font-size-sm);
949
+ --ef-type-size-lead: var(--ef-primitive-font-size-md);
950
+ --ef-type-size-statement: var(--ef-primitive-font-size-lg);
951
+ --ef-type-size-small: var(--ef-primitive-font-size-xs);
952
+ --ef-type-size-label: var(--ef-primitive-font-size-xs);
953
+ --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
954
+ --ef-type-leading-snug: var(--ef-primitive-line-height-snug);
955
+ --ef-type-leading-body: var(--ef-primitive-line-height-snug);
956
+ --ef-type-leading-lead: var(--ef-primitive-line-height-snug);
957
+ --ef-type-measure-h1: none;
958
+ --ef-type-measure-h2: none;
959
+ }
960
+
961
+ :where([data-ef-type="accessible"]) {
962
+ --ef-type-size-h1: var(--ef-primitive-font-size-3xl);
963
+ --ef-type-size-h2: var(--ef-primitive-font-size-2xl);
964
+ --ef-type-size-h3: var(--ef-primitive-font-size-xl);
965
+ --ef-type-size-h4: var(--ef-primitive-font-size-lg);
966
+ --ef-type-size-body: var(--ef-primitive-font-size-lg);
967
+ --ef-type-size-lead: var(--ef-primitive-font-size-xl);
968
+ --ef-type-size-statement: var(--ef-primitive-font-size-2xl);
969
+ --ef-type-size-small: var(--ef-primitive-font-size-md);
970
+ --ef-type-size-label: var(--ef-primitive-font-size-sm);
971
+ --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
972
+ --ef-type-leading-snug: var(--ef-primitive-line-height-normal);
973
+ --ef-type-leading-body: var(--ef-primitive-line-height-relaxed);
974
+ --ef-type-leading-lead: var(--ef-primitive-line-height-relaxed);
975
+ --ef-type-measure-h1: none;
976
+ --ef-type-measure-h2: none;
977
+ }
978
+
979
+ /* Body text takes the role on <body> or the role element, never on <html>
980
+ itself, so rem keeps meaning the user's default text size. */
981
+ :where(
982
+ html[data-ef-type="application"] > body, :not(html)[data-ef-type="application"],
983
+ html[data-ef-type="dense"] > body, :not(html)[data-ef-type="dense"],
984
+ html[data-ef-type="accessible"] > body, :not(html)[data-ef-type="accessible"]
985
+ ) {
986
+ font-size: var(--ef-type-size-body);
987
+ line-height: var(--ef-type-leading-body);
988
+ }
989
+
990
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h1) {
991
+ font-size: var(--ef-type-size-h1);
992
+ line-height: var(--ef-type-leading-heading);
993
+ max-inline-size: var(--ef-type-measure-h1);
994
+ }
995
+
996
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h2) {
997
+ font-size: var(--ef-type-size-h2);
998
+ line-height: var(--ef-type-leading-heading);
999
+ max-inline-size: var(--ef-type-measure-h2);
1000
+ }
1001
+
1002
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h3) {
1003
+ font-size: var(--ef-type-size-h3);
1004
+ line-height: var(--ef-type-leading-snug);
1005
+ }
1006
+
1007
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h4, h5, h6) {
1008
+ font-size: var(--ef-type-size-h4);
1009
+ line-height: var(--ef-type-leading-snug);
1010
+ }
909
1011
  }
910
1012
 
911
1013
  /*
@@ -7658,6 +7760,10 @@
7658
7760
  --ef-type-size-h3: var(--ef-primitive-font-size-lg);
7659
7761
  --ef-type-size-h4: var(--ef-primitive-font-size-md);
7660
7762
  --ef-type-size-statement: var(--ef-primitive-font-size-fluid-display-3);
7763
+ /* Display heading measures. A typography role (data-ef-type, see
7764
+ src/styles/foundations.css) lifts them for application screens. */
7765
+ --ef-type-measure-h1: 14ch;
7766
+ --ef-type-measure-h2: 18ch;
7661
7767
  --ef-type-leading-body: var(--ef-primitive-line-height-normal);
7662
7768
  --ef-type-leading-lead: var(--ef-primitive-line-height-normal);
7663
7769
  --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
@@ -7850,8 +7956,8 @@
7850
7956
  line-height: var(--ef-type-leading-heading);
7851
7957
  }
7852
7958
 
7853
- .ef-site :where(h1) { font-size: var(--ef-type-size-h1); max-inline-size: 14ch; }
7854
- .ef-site :where(h2) { font-size: var(--ef-type-size-h2); max-inline-size: 18ch; }
7959
+ .ef-site :where(h1) { font-size: var(--ef-type-size-h1); max-inline-size: var(--ef-type-measure-h1); }
7960
+ .ef-site :where(h2) { font-size: var(--ef-type-size-h2); max-inline-size: var(--ef-type-measure-h2); }
7855
7961
  .ef-site :where(h3) { font-size: var(--ef-type-size-h3); line-height: var(--ef-type-leading-snug); }
7856
7962
  .ef-site :where(h4, h5, h6) { font-size: var(--ef-type-size-h4); line-height: var(--ef-type-leading-snug); }
7857
7963
 
@@ -342,13 +342,13 @@
342
342
  "tokens.css"
343
343
  ],
344
344
  "bytes": {
345
- "all.css": 336382,
345
+ "all.css": 341701,
346
346
  "assessment.css": 36343,
347
347
  "brands/echelon.css": 4213,
348
348
  "brands/example-harbor.css": 4229,
349
349
  "brands/index.json": 335,
350
350
  "components.css": 213356,
351
- "foundations.css": 15326,
351
+ "foundations.css": 20395,
352
352
  "icons/add.svg": 216,
353
353
  "icons/agent.svg": 354,
354
354
  "icons/archive.svg": 248,
@@ -510,7 +510,7 @@
510
510
  "icons/warning.svg": 324,
511
511
  "icons/wifi-off.svg": 269,
512
512
  "icons/workflow.svg": 345,
513
- "marketing.css": 47618,
513
+ "marketing.css": 47868,
514
514
  "patterns/active-filter-summary.html": 532,
515
515
  "patterns/alert.html": 384,
516
516
  "patterns/allocation.html": 1033,
@@ -295,4 +295,106 @@
295
295
  :where(.ef-stack, .ef-grid, .ef-responsive-grid, .ef-priority-stack)[data-density] { gap: var(--ef-density-space); }
296
296
  @media (max-width: 48rem) { .ef-rail, .ef-rail[data-side="end"], .ef-focus-stage { grid-template-columns: minmax(0, 1fr); } .ef-mosaic { grid-template-columns: repeat(2, minmax(0, 1fr)); } .ef-span-2 { grid-column: 1 / -1; } }
297
297
  @media (max-width: 30rem) { .ef-mosaic { grid-template-columns: minmax(0, 1fr); } .ef-mosaic > [data-span="2"], .ef-mosaic > [data-span="full"] { grid-column: auto; } .ef-responsive-grid { --ef-responsive-grid-min: 100%; } .ef-priority-stack { --ef-priority-min: 100%; } }
298
+
299
+ /*
300
+ * Typography roles (EII-F). data-ef-type selects a role on any element,
301
+ * usually <html> or <body>; the nearest role wins:
302
+ * marketing the default (no attribute needed): the marketing layer's
303
+ * fluid display scale and heading measures, unchanged.
304
+ * application fixed steps for tools: h1 2xl, h2 xl, h3 lg, no ch caps.
305
+ * dense administrative density: one step smaller, snug leading.
306
+ * accessible large text: one step larger, relaxed leading.
307
+ * A role restates the type role tokens that the marketing layer
308
+ * (src/marketing/roles.css) and its themes declare, using only primitive
309
+ * steps. This layer follows echelon.tokens, so a role overrides a theme's
310
+ * :root role values; the .ef-site shell consumes the same tokens. The rules
311
+ * below give the application layer, which has no heading scale of its own,
312
+ * the same headings. Component headings (later layers) keep their sizes.
313
+ */
314
+ :where([data-ef-type="application"]) {
315
+ --ef-type-size-h1: var(--ef-primitive-font-size-2xl);
316
+ --ef-type-size-h2: var(--ef-primitive-font-size-xl);
317
+ --ef-type-size-h3: var(--ef-primitive-font-size-lg);
318
+ --ef-type-size-h4: var(--ef-primitive-font-size-md);
319
+ --ef-type-size-body: var(--ef-primitive-font-size-md);
320
+ --ef-type-size-lead: var(--ef-primitive-font-size-lg);
321
+ --ef-type-size-statement: var(--ef-primitive-font-size-xl);
322
+ --ef-type-size-small: var(--ef-primitive-font-size-sm);
323
+ --ef-type-size-label: var(--ef-primitive-font-size-xs);
324
+ --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
325
+ --ef-type-leading-snug: var(--ef-primitive-line-height-snug);
326
+ --ef-type-leading-body: var(--ef-primitive-line-height-normal);
327
+ --ef-type-leading-lead: var(--ef-primitive-line-height-normal);
328
+ --ef-type-measure-h1: none;
329
+ --ef-type-measure-h2: none;
330
+ }
331
+
332
+ :where([data-ef-type="dense"]) {
333
+ --ef-type-size-h1: var(--ef-primitive-font-size-xl);
334
+ --ef-type-size-h2: var(--ef-primitive-font-size-lg);
335
+ --ef-type-size-h3: var(--ef-primitive-font-size-md);
336
+ --ef-type-size-h4: var(--ef-primitive-font-size-sm);
337
+ --ef-type-size-body: var(--ef-primitive-font-size-sm);
338
+ --ef-type-size-lead: var(--ef-primitive-font-size-md);
339
+ --ef-type-size-statement: var(--ef-primitive-font-size-lg);
340
+ --ef-type-size-small: var(--ef-primitive-font-size-xs);
341
+ --ef-type-size-label: var(--ef-primitive-font-size-xs);
342
+ --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
343
+ --ef-type-leading-snug: var(--ef-primitive-line-height-snug);
344
+ --ef-type-leading-body: var(--ef-primitive-line-height-snug);
345
+ --ef-type-leading-lead: var(--ef-primitive-line-height-snug);
346
+ --ef-type-measure-h1: none;
347
+ --ef-type-measure-h2: none;
348
+ }
349
+
350
+ :where([data-ef-type="accessible"]) {
351
+ --ef-type-size-h1: var(--ef-primitive-font-size-3xl);
352
+ --ef-type-size-h2: var(--ef-primitive-font-size-2xl);
353
+ --ef-type-size-h3: var(--ef-primitive-font-size-xl);
354
+ --ef-type-size-h4: var(--ef-primitive-font-size-lg);
355
+ --ef-type-size-body: var(--ef-primitive-font-size-lg);
356
+ --ef-type-size-lead: var(--ef-primitive-font-size-xl);
357
+ --ef-type-size-statement: var(--ef-primitive-font-size-2xl);
358
+ --ef-type-size-small: var(--ef-primitive-font-size-md);
359
+ --ef-type-size-label: var(--ef-primitive-font-size-sm);
360
+ --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
361
+ --ef-type-leading-snug: var(--ef-primitive-line-height-normal);
362
+ --ef-type-leading-body: var(--ef-primitive-line-height-relaxed);
363
+ --ef-type-leading-lead: var(--ef-primitive-line-height-relaxed);
364
+ --ef-type-measure-h1: none;
365
+ --ef-type-measure-h2: none;
366
+ }
367
+
368
+ /* Body text takes the role on <body> or the role element, never on <html>
369
+ itself, so rem keeps meaning the user's default text size. */
370
+ :where(
371
+ html[data-ef-type="application"] > body, :not(html)[data-ef-type="application"],
372
+ html[data-ef-type="dense"] > body, :not(html)[data-ef-type="dense"],
373
+ html[data-ef-type="accessible"] > body, :not(html)[data-ef-type="accessible"]
374
+ ) {
375
+ font-size: var(--ef-type-size-body);
376
+ line-height: var(--ef-type-leading-body);
377
+ }
378
+
379
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h1) {
380
+ font-size: var(--ef-type-size-h1);
381
+ line-height: var(--ef-type-leading-heading);
382
+ max-inline-size: var(--ef-type-measure-h1);
383
+ }
384
+
385
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h2) {
386
+ font-size: var(--ef-type-size-h2);
387
+ line-height: var(--ef-type-leading-heading);
388
+ max-inline-size: var(--ef-type-measure-h2);
389
+ }
390
+
391
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h3) {
392
+ font-size: var(--ef-type-size-h3);
393
+ line-height: var(--ef-type-leading-snug);
394
+ }
395
+
396
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h4, h5, h6) {
397
+ font-size: var(--ef-type-size-h4);
398
+ line-height: var(--ef-type-leading-snug);
399
+ }
298
400
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "formaVersion": "0.10.0",
3
+ "formaVersion": "0.11.0",
4
4
  "grid": 24,
5
5
  "icons": [
6
6
  {
@@ -1,4 +1,4 @@
1
- /*! Forma 0.10.0 | forma-all.css | bundle echelon-marketing
1
+ /*! Forma 0.11.0 | forma-all.css | bundle echelon-marketing
2
2
  Complete Forma surface (application components, assessment, and marketing layers) for sites that also present Forma application patterns such as data grids or status lozenges.
3
3
  Generated by tools/PresentationBundler. Do not edit; pin by version and sha256. */
4
4
 
@@ -911,6 +911,108 @@
911
911
  :where(.ef-stack, .ef-grid, .ef-responsive-grid, .ef-priority-stack)[data-density] { gap: var(--ef-density-space); }
912
912
  @media (max-width: 48rem) { .ef-rail, .ef-rail[data-side="end"], .ef-focus-stage { grid-template-columns: minmax(0, 1fr); } .ef-mosaic { grid-template-columns: repeat(2, minmax(0, 1fr)); } .ef-span-2 { grid-column: 1 / -1; } }
913
913
  @media (max-width: 30rem) { .ef-mosaic { grid-template-columns: minmax(0, 1fr); } .ef-mosaic > [data-span="2"], .ef-mosaic > [data-span="full"] { grid-column: auto; } .ef-responsive-grid { --ef-responsive-grid-min: 100%; } .ef-priority-stack { --ef-priority-min: 100%; } }
914
+
915
+ /*
916
+ * Typography roles (EII-F). data-ef-type selects a role on any element,
917
+ * usually <html> or <body>; the nearest role wins:
918
+ * marketing the default (no attribute needed): the marketing layer's
919
+ * fluid display scale and heading measures, unchanged.
920
+ * application fixed steps for tools: h1 2xl, h2 xl, h3 lg, no ch caps.
921
+ * dense administrative density: one step smaller, snug leading.
922
+ * accessible large text: one step larger, relaxed leading.
923
+ * A role restates the type role tokens that the marketing layer
924
+ * (src/marketing/roles.css) and its themes declare, using only primitive
925
+ * steps. This layer follows echelon.tokens, so a role overrides a theme's
926
+ * :root role values; the .ef-site shell consumes the same tokens. The rules
927
+ * below give the application layer, which has no heading scale of its own,
928
+ * the same headings. Component headings (later layers) keep their sizes.
929
+ */
930
+ :where([data-ef-type="application"]) {
931
+ --ef-type-size-h1: var(--ef-primitive-font-size-2xl);
932
+ --ef-type-size-h2: var(--ef-primitive-font-size-xl);
933
+ --ef-type-size-h3: var(--ef-primitive-font-size-lg);
934
+ --ef-type-size-h4: var(--ef-primitive-font-size-md);
935
+ --ef-type-size-body: var(--ef-primitive-font-size-md);
936
+ --ef-type-size-lead: var(--ef-primitive-font-size-lg);
937
+ --ef-type-size-statement: var(--ef-primitive-font-size-xl);
938
+ --ef-type-size-small: var(--ef-primitive-font-size-sm);
939
+ --ef-type-size-label: var(--ef-primitive-font-size-xs);
940
+ --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
941
+ --ef-type-leading-snug: var(--ef-primitive-line-height-snug);
942
+ --ef-type-leading-body: var(--ef-primitive-line-height-normal);
943
+ --ef-type-leading-lead: var(--ef-primitive-line-height-normal);
944
+ --ef-type-measure-h1: none;
945
+ --ef-type-measure-h2: none;
946
+ }
947
+
948
+ :where([data-ef-type="dense"]) {
949
+ --ef-type-size-h1: var(--ef-primitive-font-size-xl);
950
+ --ef-type-size-h2: var(--ef-primitive-font-size-lg);
951
+ --ef-type-size-h3: var(--ef-primitive-font-size-md);
952
+ --ef-type-size-h4: var(--ef-primitive-font-size-sm);
953
+ --ef-type-size-body: var(--ef-primitive-font-size-sm);
954
+ --ef-type-size-lead: var(--ef-primitive-font-size-md);
955
+ --ef-type-size-statement: var(--ef-primitive-font-size-lg);
956
+ --ef-type-size-small: var(--ef-primitive-font-size-xs);
957
+ --ef-type-size-label: var(--ef-primitive-font-size-xs);
958
+ --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
959
+ --ef-type-leading-snug: var(--ef-primitive-line-height-snug);
960
+ --ef-type-leading-body: var(--ef-primitive-line-height-snug);
961
+ --ef-type-leading-lead: var(--ef-primitive-line-height-snug);
962
+ --ef-type-measure-h1: none;
963
+ --ef-type-measure-h2: none;
964
+ }
965
+
966
+ :where([data-ef-type="accessible"]) {
967
+ --ef-type-size-h1: var(--ef-primitive-font-size-3xl);
968
+ --ef-type-size-h2: var(--ef-primitive-font-size-2xl);
969
+ --ef-type-size-h3: var(--ef-primitive-font-size-xl);
970
+ --ef-type-size-h4: var(--ef-primitive-font-size-lg);
971
+ --ef-type-size-body: var(--ef-primitive-font-size-lg);
972
+ --ef-type-size-lead: var(--ef-primitive-font-size-xl);
973
+ --ef-type-size-statement: var(--ef-primitive-font-size-2xl);
974
+ --ef-type-size-small: var(--ef-primitive-font-size-md);
975
+ --ef-type-size-label: var(--ef-primitive-font-size-sm);
976
+ --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
977
+ --ef-type-leading-snug: var(--ef-primitive-line-height-normal);
978
+ --ef-type-leading-body: var(--ef-primitive-line-height-relaxed);
979
+ --ef-type-leading-lead: var(--ef-primitive-line-height-relaxed);
980
+ --ef-type-measure-h1: none;
981
+ --ef-type-measure-h2: none;
982
+ }
983
+
984
+ /* Body text takes the role on <body> or the role element, never on <html>
985
+ itself, so rem keeps meaning the user's default text size. */
986
+ :where(
987
+ html[data-ef-type="application"] > body, :not(html)[data-ef-type="application"],
988
+ html[data-ef-type="dense"] > body, :not(html)[data-ef-type="dense"],
989
+ html[data-ef-type="accessible"] > body, :not(html)[data-ef-type="accessible"]
990
+ ) {
991
+ font-size: var(--ef-type-size-body);
992
+ line-height: var(--ef-type-leading-body);
993
+ }
994
+
995
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h1) {
996
+ font-size: var(--ef-type-size-h1);
997
+ line-height: var(--ef-type-leading-heading);
998
+ max-inline-size: var(--ef-type-measure-h1);
999
+ }
1000
+
1001
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h2) {
1002
+ font-size: var(--ef-type-size-h2);
1003
+ line-height: var(--ef-type-leading-heading);
1004
+ max-inline-size: var(--ef-type-measure-h2);
1005
+ }
1006
+
1007
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h3) {
1008
+ font-size: var(--ef-type-size-h3);
1009
+ line-height: var(--ef-type-leading-snug);
1010
+ }
1011
+
1012
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h4, h5, h6) {
1013
+ font-size: var(--ef-type-size-h4);
1014
+ line-height: var(--ef-type-leading-snug);
1015
+ }
914
1016
  }
915
1017
 
916
1018
  /*
@@ -7663,6 +7765,10 @@
7663
7765
  --ef-type-size-h3: var(--ef-primitive-font-size-lg);
7664
7766
  --ef-type-size-h4: var(--ef-primitive-font-size-md);
7665
7767
  --ef-type-size-statement: var(--ef-primitive-font-size-fluid-display-3);
7768
+ /* Display heading measures. A typography role (data-ef-type, see
7769
+ src/styles/foundations.css) lifts them for application screens. */
7770
+ --ef-type-measure-h1: 14ch;
7771
+ --ef-type-measure-h2: 18ch;
7666
7772
  --ef-type-leading-body: var(--ef-primitive-line-height-normal);
7667
7773
  --ef-type-leading-lead: var(--ef-primitive-line-height-normal);
7668
7774
  --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
@@ -7855,8 +7961,8 @@
7855
7961
  line-height: var(--ef-type-leading-heading);
7856
7962
  }
7857
7963
 
7858
- .ef-site :where(h1) { font-size: var(--ef-type-size-h1); max-inline-size: 14ch; }
7859
- .ef-site :where(h2) { font-size: var(--ef-type-size-h2); max-inline-size: 18ch; }
7964
+ .ef-site :where(h1) { font-size: var(--ef-type-size-h1); max-inline-size: var(--ef-type-measure-h1); }
7965
+ .ef-site :where(h2) { font-size: var(--ef-type-size-h2); max-inline-size: var(--ef-type-measure-h2); }
7860
7966
  .ef-site :where(h3) { font-size: var(--ef-type-size-h3); line-height: var(--ef-type-leading-snug); }
7861
7967
  .ef-site :where(h4, h5, h6) { font-size: var(--ef-type-size-h4); line-height: var(--ef-type-leading-snug); }
7862
7968
 
@@ -1,4 +1,4 @@
1
- /*! Forma 0.10.0 | forma-echelon-marketing.css | bundle echelon-marketing
1
+ /*! Forma 0.11.0 | forma-echelon-marketing.css | bundle echelon-marketing
2
2
  Single-file Echelon marketing presentation: forma-marketing.css followed by the Echelon theme. Most sites load only this file.
3
3
  Generated by tools/PresentationBundler. Do not edit; pin by version and sha256. */
4
4
 
@@ -665,6 +665,10 @@
665
665
  --ef-type-size-h3: var(--ef-primitive-font-size-lg);
666
666
  --ef-type-size-h4: var(--ef-primitive-font-size-md);
667
667
  --ef-type-size-statement: var(--ef-primitive-font-size-fluid-display-3);
668
+ /* Display heading measures. A typography role (data-ef-type, see
669
+ src/styles/foundations.css) lifts them for application screens. */
670
+ --ef-type-measure-h1: 14ch;
671
+ --ef-type-measure-h2: 18ch;
668
672
  --ef-type-leading-body: var(--ef-primitive-line-height-normal);
669
673
  --ef-type-leading-lead: var(--ef-primitive-line-height-normal);
670
674
  --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
@@ -1011,6 +1015,108 @@
1011
1015
  :where(.ef-stack, .ef-grid, .ef-responsive-grid, .ef-priority-stack)[data-density] { gap: var(--ef-density-space); }
1012
1016
  @media (max-width: 48rem) { .ef-rail, .ef-rail[data-side="end"], .ef-focus-stage { grid-template-columns: minmax(0, 1fr); } .ef-mosaic { grid-template-columns: repeat(2, minmax(0, 1fr)); } .ef-span-2 { grid-column: 1 / -1; } }
1013
1017
  @media (max-width: 30rem) { .ef-mosaic { grid-template-columns: minmax(0, 1fr); } .ef-mosaic > [data-span="2"], .ef-mosaic > [data-span="full"] { grid-column: auto; } .ef-responsive-grid { --ef-responsive-grid-min: 100%; } .ef-priority-stack { --ef-priority-min: 100%; } }
1018
+
1019
+ /*
1020
+ * Typography roles (EII-F). data-ef-type selects a role on any element,
1021
+ * usually <html> or <body>; the nearest role wins:
1022
+ * marketing the default (no attribute needed): the marketing layer's
1023
+ * fluid display scale and heading measures, unchanged.
1024
+ * application fixed steps for tools: h1 2xl, h2 xl, h3 lg, no ch caps.
1025
+ * dense administrative density: one step smaller, snug leading.
1026
+ * accessible large text: one step larger, relaxed leading.
1027
+ * A role restates the type role tokens that the marketing layer
1028
+ * (src/marketing/roles.css) and its themes declare, using only primitive
1029
+ * steps. This layer follows echelon.tokens, so a role overrides a theme's
1030
+ * :root role values; the .ef-site shell consumes the same tokens. The rules
1031
+ * below give the application layer, which has no heading scale of its own,
1032
+ * the same headings. Component headings (later layers) keep their sizes.
1033
+ */
1034
+ :where([data-ef-type="application"]) {
1035
+ --ef-type-size-h1: var(--ef-primitive-font-size-2xl);
1036
+ --ef-type-size-h2: var(--ef-primitive-font-size-xl);
1037
+ --ef-type-size-h3: var(--ef-primitive-font-size-lg);
1038
+ --ef-type-size-h4: var(--ef-primitive-font-size-md);
1039
+ --ef-type-size-body: var(--ef-primitive-font-size-md);
1040
+ --ef-type-size-lead: var(--ef-primitive-font-size-lg);
1041
+ --ef-type-size-statement: var(--ef-primitive-font-size-xl);
1042
+ --ef-type-size-small: var(--ef-primitive-font-size-sm);
1043
+ --ef-type-size-label: var(--ef-primitive-font-size-xs);
1044
+ --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
1045
+ --ef-type-leading-snug: var(--ef-primitive-line-height-snug);
1046
+ --ef-type-leading-body: var(--ef-primitive-line-height-normal);
1047
+ --ef-type-leading-lead: var(--ef-primitive-line-height-normal);
1048
+ --ef-type-measure-h1: none;
1049
+ --ef-type-measure-h2: none;
1050
+ }
1051
+
1052
+ :where([data-ef-type="dense"]) {
1053
+ --ef-type-size-h1: var(--ef-primitive-font-size-xl);
1054
+ --ef-type-size-h2: var(--ef-primitive-font-size-lg);
1055
+ --ef-type-size-h3: var(--ef-primitive-font-size-md);
1056
+ --ef-type-size-h4: var(--ef-primitive-font-size-sm);
1057
+ --ef-type-size-body: var(--ef-primitive-font-size-sm);
1058
+ --ef-type-size-lead: var(--ef-primitive-font-size-md);
1059
+ --ef-type-size-statement: var(--ef-primitive-font-size-lg);
1060
+ --ef-type-size-small: var(--ef-primitive-font-size-xs);
1061
+ --ef-type-size-label: var(--ef-primitive-font-size-xs);
1062
+ --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
1063
+ --ef-type-leading-snug: var(--ef-primitive-line-height-snug);
1064
+ --ef-type-leading-body: var(--ef-primitive-line-height-snug);
1065
+ --ef-type-leading-lead: var(--ef-primitive-line-height-snug);
1066
+ --ef-type-measure-h1: none;
1067
+ --ef-type-measure-h2: none;
1068
+ }
1069
+
1070
+ :where([data-ef-type="accessible"]) {
1071
+ --ef-type-size-h1: var(--ef-primitive-font-size-3xl);
1072
+ --ef-type-size-h2: var(--ef-primitive-font-size-2xl);
1073
+ --ef-type-size-h3: var(--ef-primitive-font-size-xl);
1074
+ --ef-type-size-h4: var(--ef-primitive-font-size-lg);
1075
+ --ef-type-size-body: var(--ef-primitive-font-size-lg);
1076
+ --ef-type-size-lead: var(--ef-primitive-font-size-xl);
1077
+ --ef-type-size-statement: var(--ef-primitive-font-size-2xl);
1078
+ --ef-type-size-small: var(--ef-primitive-font-size-md);
1079
+ --ef-type-size-label: var(--ef-primitive-font-size-sm);
1080
+ --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
1081
+ --ef-type-leading-snug: var(--ef-primitive-line-height-normal);
1082
+ --ef-type-leading-body: var(--ef-primitive-line-height-relaxed);
1083
+ --ef-type-leading-lead: var(--ef-primitive-line-height-relaxed);
1084
+ --ef-type-measure-h1: none;
1085
+ --ef-type-measure-h2: none;
1086
+ }
1087
+
1088
+ /* Body text takes the role on <body> or the role element, never on <html>
1089
+ itself, so rem keeps meaning the user's default text size. */
1090
+ :where(
1091
+ html[data-ef-type="application"] > body, :not(html)[data-ef-type="application"],
1092
+ html[data-ef-type="dense"] > body, :not(html)[data-ef-type="dense"],
1093
+ html[data-ef-type="accessible"] > body, :not(html)[data-ef-type="accessible"]
1094
+ ) {
1095
+ font-size: var(--ef-type-size-body);
1096
+ line-height: var(--ef-type-leading-body);
1097
+ }
1098
+
1099
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h1) {
1100
+ font-size: var(--ef-type-size-h1);
1101
+ line-height: var(--ef-type-leading-heading);
1102
+ max-inline-size: var(--ef-type-measure-h1);
1103
+ }
1104
+
1105
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h2) {
1106
+ font-size: var(--ef-type-size-h2);
1107
+ line-height: var(--ef-type-leading-heading);
1108
+ max-inline-size: var(--ef-type-measure-h2);
1109
+ }
1110
+
1111
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h3) {
1112
+ font-size: var(--ef-type-size-h3);
1113
+ line-height: var(--ef-type-leading-snug);
1114
+ }
1115
+
1116
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h4, h5, h6) {
1117
+ font-size: var(--ef-type-size-h4);
1118
+ line-height: var(--ef-type-leading-snug);
1119
+ }
1014
1120
  }
1015
1121
 
1016
1122
  /* --- source: src/marketing/foundations.css --- */
@@ -1158,8 +1264,8 @@
1158
1264
  line-height: var(--ef-type-leading-heading);
1159
1265
  }
1160
1266
 
1161
- .ef-site :where(h1) { font-size: var(--ef-type-size-h1); max-inline-size: 14ch; }
1162
- .ef-site :where(h2) { font-size: var(--ef-type-size-h2); max-inline-size: 18ch; }
1267
+ .ef-site :where(h1) { font-size: var(--ef-type-size-h1); max-inline-size: var(--ef-type-measure-h1); }
1268
+ .ef-site :where(h2) { font-size: var(--ef-type-size-h2); max-inline-size: var(--ef-type-measure-h2); }
1163
1269
  .ef-site :where(h3) { font-size: var(--ef-type-size-h3); line-height: var(--ef-type-leading-snug); }
1164
1270
  .ef-site :where(h4, h5, h6) { font-size: var(--ef-type-size-h4); line-height: var(--ef-type-leading-snug); }
1165
1271
 
@@ -1,4 +1,4 @@
1
- /*! Forma 0.10.0 | forma-marketing-theme-echelon.css | bundle echelon-marketing
1
+ /*! Forma 0.11.0 | forma-marketing-theme-echelon.css | bundle echelon-marketing
2
2
  Echelon Marketing Theme: compiled theme tokens and theme hooks. Load after forma-marketing.css.
3
3
  Generated by tools/PresentationBundler. Do not edit; pin by version and sha256. */
4
4
 
@@ -1,4 +1,4 @@
1
- /*! Forma 0.10.0 | forma-marketing.css | bundle echelon-marketing
1
+ /*! Forma 0.11.0 | forma-marketing.css | bundle echelon-marketing
2
2
  Brand-neutral marketing layer: core tokens, role defaults, Forma foundations, marketing foundations, components, and layouts.
3
3
  Generated by tools/PresentationBundler. Do not edit; pin by version and sha256. */
4
4
 
@@ -664,6 +664,10 @@
664
664
  --ef-type-size-h3: var(--ef-primitive-font-size-lg);
665
665
  --ef-type-size-h4: var(--ef-primitive-font-size-md);
666
666
  --ef-type-size-statement: var(--ef-primitive-font-size-fluid-display-3);
667
+ /* Display heading measures. A typography role (data-ef-type, see
668
+ src/styles/foundations.css) lifts them for application screens. */
669
+ --ef-type-measure-h1: 14ch;
670
+ --ef-type-measure-h2: 18ch;
667
671
  --ef-type-leading-body: var(--ef-primitive-line-height-normal);
668
672
  --ef-type-leading-lead: var(--ef-primitive-line-height-normal);
669
673
  --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
@@ -1010,6 +1014,108 @@
1010
1014
  :where(.ef-stack, .ef-grid, .ef-responsive-grid, .ef-priority-stack)[data-density] { gap: var(--ef-density-space); }
1011
1015
  @media (max-width: 48rem) { .ef-rail, .ef-rail[data-side="end"], .ef-focus-stage { grid-template-columns: minmax(0, 1fr); } .ef-mosaic { grid-template-columns: repeat(2, minmax(0, 1fr)); } .ef-span-2 { grid-column: 1 / -1; } }
1012
1016
  @media (max-width: 30rem) { .ef-mosaic { grid-template-columns: minmax(0, 1fr); } .ef-mosaic > [data-span="2"], .ef-mosaic > [data-span="full"] { grid-column: auto; } .ef-responsive-grid { --ef-responsive-grid-min: 100%; } .ef-priority-stack { --ef-priority-min: 100%; } }
1017
+
1018
+ /*
1019
+ * Typography roles (EII-F). data-ef-type selects a role on any element,
1020
+ * usually <html> or <body>; the nearest role wins:
1021
+ * marketing the default (no attribute needed): the marketing layer's
1022
+ * fluid display scale and heading measures, unchanged.
1023
+ * application fixed steps for tools: h1 2xl, h2 xl, h3 lg, no ch caps.
1024
+ * dense administrative density: one step smaller, snug leading.
1025
+ * accessible large text: one step larger, relaxed leading.
1026
+ * A role restates the type role tokens that the marketing layer
1027
+ * (src/marketing/roles.css) and its themes declare, using only primitive
1028
+ * steps. This layer follows echelon.tokens, so a role overrides a theme's
1029
+ * :root role values; the .ef-site shell consumes the same tokens. The rules
1030
+ * below give the application layer, which has no heading scale of its own,
1031
+ * the same headings. Component headings (later layers) keep their sizes.
1032
+ */
1033
+ :where([data-ef-type="application"]) {
1034
+ --ef-type-size-h1: var(--ef-primitive-font-size-2xl);
1035
+ --ef-type-size-h2: var(--ef-primitive-font-size-xl);
1036
+ --ef-type-size-h3: var(--ef-primitive-font-size-lg);
1037
+ --ef-type-size-h4: var(--ef-primitive-font-size-md);
1038
+ --ef-type-size-body: var(--ef-primitive-font-size-md);
1039
+ --ef-type-size-lead: var(--ef-primitive-font-size-lg);
1040
+ --ef-type-size-statement: var(--ef-primitive-font-size-xl);
1041
+ --ef-type-size-small: var(--ef-primitive-font-size-sm);
1042
+ --ef-type-size-label: var(--ef-primitive-font-size-xs);
1043
+ --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
1044
+ --ef-type-leading-snug: var(--ef-primitive-line-height-snug);
1045
+ --ef-type-leading-body: var(--ef-primitive-line-height-normal);
1046
+ --ef-type-leading-lead: var(--ef-primitive-line-height-normal);
1047
+ --ef-type-measure-h1: none;
1048
+ --ef-type-measure-h2: none;
1049
+ }
1050
+
1051
+ :where([data-ef-type="dense"]) {
1052
+ --ef-type-size-h1: var(--ef-primitive-font-size-xl);
1053
+ --ef-type-size-h2: var(--ef-primitive-font-size-lg);
1054
+ --ef-type-size-h3: var(--ef-primitive-font-size-md);
1055
+ --ef-type-size-h4: var(--ef-primitive-font-size-sm);
1056
+ --ef-type-size-body: var(--ef-primitive-font-size-sm);
1057
+ --ef-type-size-lead: var(--ef-primitive-font-size-md);
1058
+ --ef-type-size-statement: var(--ef-primitive-font-size-lg);
1059
+ --ef-type-size-small: var(--ef-primitive-font-size-xs);
1060
+ --ef-type-size-label: var(--ef-primitive-font-size-xs);
1061
+ --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
1062
+ --ef-type-leading-snug: var(--ef-primitive-line-height-snug);
1063
+ --ef-type-leading-body: var(--ef-primitive-line-height-snug);
1064
+ --ef-type-leading-lead: var(--ef-primitive-line-height-snug);
1065
+ --ef-type-measure-h1: none;
1066
+ --ef-type-measure-h2: none;
1067
+ }
1068
+
1069
+ :where([data-ef-type="accessible"]) {
1070
+ --ef-type-size-h1: var(--ef-primitive-font-size-3xl);
1071
+ --ef-type-size-h2: var(--ef-primitive-font-size-2xl);
1072
+ --ef-type-size-h3: var(--ef-primitive-font-size-xl);
1073
+ --ef-type-size-h4: var(--ef-primitive-font-size-lg);
1074
+ --ef-type-size-body: var(--ef-primitive-font-size-lg);
1075
+ --ef-type-size-lead: var(--ef-primitive-font-size-xl);
1076
+ --ef-type-size-statement: var(--ef-primitive-font-size-2xl);
1077
+ --ef-type-size-small: var(--ef-primitive-font-size-md);
1078
+ --ef-type-size-label: var(--ef-primitive-font-size-sm);
1079
+ --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
1080
+ --ef-type-leading-snug: var(--ef-primitive-line-height-normal);
1081
+ --ef-type-leading-body: var(--ef-primitive-line-height-relaxed);
1082
+ --ef-type-leading-lead: var(--ef-primitive-line-height-relaxed);
1083
+ --ef-type-measure-h1: none;
1084
+ --ef-type-measure-h2: none;
1085
+ }
1086
+
1087
+ /* Body text takes the role on <body> or the role element, never on <html>
1088
+ itself, so rem keeps meaning the user's default text size. */
1089
+ :where(
1090
+ html[data-ef-type="application"] > body, :not(html)[data-ef-type="application"],
1091
+ html[data-ef-type="dense"] > body, :not(html)[data-ef-type="dense"],
1092
+ html[data-ef-type="accessible"] > body, :not(html)[data-ef-type="accessible"]
1093
+ ) {
1094
+ font-size: var(--ef-type-size-body);
1095
+ line-height: var(--ef-type-leading-body);
1096
+ }
1097
+
1098
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h1) {
1099
+ font-size: var(--ef-type-size-h1);
1100
+ line-height: var(--ef-type-leading-heading);
1101
+ max-inline-size: var(--ef-type-measure-h1);
1102
+ }
1103
+
1104
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h2) {
1105
+ font-size: var(--ef-type-size-h2);
1106
+ line-height: var(--ef-type-leading-heading);
1107
+ max-inline-size: var(--ef-type-measure-h2);
1108
+ }
1109
+
1110
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h3) {
1111
+ font-size: var(--ef-type-size-h3);
1112
+ line-height: var(--ef-type-leading-snug);
1113
+ }
1114
+
1115
+ :where([data-ef-type="application"], [data-ef-type="dense"], [data-ef-type="accessible"]) :where(h4, h5, h6) {
1116
+ font-size: var(--ef-type-size-h4);
1117
+ line-height: var(--ef-type-leading-snug);
1118
+ }
1013
1119
  }
1014
1120
 
1015
1121
  /* --- source: src/marketing/foundations.css --- */
@@ -1157,8 +1263,8 @@
1157
1263
  line-height: var(--ef-type-leading-heading);
1158
1264
  }
1159
1265
 
1160
- .ef-site :where(h1) { font-size: var(--ef-type-size-h1); max-inline-size: 14ch; }
1161
- .ef-site :where(h2) { font-size: var(--ef-type-size-h2); max-inline-size: 18ch; }
1266
+ .ef-site :where(h1) { font-size: var(--ef-type-size-h1); max-inline-size: var(--ef-type-measure-h1); }
1267
+ .ef-site :where(h2) { font-size: var(--ef-type-size-h2); max-inline-size: var(--ef-type-measure-h2); }
1162
1268
  .ef-site :where(h3) { font-size: var(--ef-type-size-h3); line-height: var(--ef-type-leading-snug); }
1163
1269
  .ef-site :where(h4, h5, h6) { font-size: var(--ef-type-size-h4); line-height: var(--ef-type-leading-snug); }
1164
1270
 
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "forma": "0.10.0",
3
+ "forma": "0.11.0",
4
4
  "bundle": "echelon-marketing",
5
5
  "description": "Forma presentation artifacts for Echelon marketing sites. Every artifact is a single self-contained CSS file with no imports or relative URLs.",
6
- "releaseTag": "v0.10.0",
6
+ "releaseTag": "v0.11.0",
7
7
  "artifacts": [
8
8
  {
9
9
  "file": "forma-marketing.css",
10
10
  "description": "Brand-neutral marketing layer: core tokens, role defaults, Forma foundations, marketing foundations, components, and layouts.",
11
- "bytes": 87243,
12
- "sha256": "d8a900aecd8aa4a72cbda731d3bbcfadbf872bc8aed1a8ef74f5ef3169af4cc6",
11
+ "bytes": 92562,
12
+ "sha256": "d15eb11a605676935a683fd1382bdecf2fd9c070893c80974766b4c22ab63b75",
13
13
  "sources": [
14
14
  "dist/tokens.css",
15
15
  "src/marketing/roles.css",
@@ -23,7 +23,7 @@
23
23
  "file": "forma-marketing-theme-echelon.css",
24
24
  "description": "Echelon Marketing Theme: compiled theme tokens and theme hooks. Load after forma-marketing.css.",
25
25
  "bytes": 6296,
26
- "sha256": "e3e05cf66b87ac6be686896c353ab87f47ad1be589c1fc2895238d1bd987d9e6",
26
+ "sha256": "66fcf350b0d36535df6fcb6fbca32bc1236b31398c49eeccf3670342f38e5ac9",
27
27
  "sources": [
28
28
  "build/marketing/echelon-marketing.tokens.css",
29
29
  "themes/echelon/marketing.css"
@@ -32,8 +32,8 @@
32
32
  {
33
33
  "file": "forma-echelon-marketing.css",
34
34
  "description": "Single-file Echelon marketing presentation: forma-marketing.css followed by the Echelon theme. Most sites load only this file.",
35
- "bytes": 93383,
36
- "sha256": "837ddc667b3eccbc6a0512fe767708212a2aeffe18f9c2a27cbedad9be2937f3",
35
+ "bytes": 98702,
36
+ "sha256": "08975a531a7c4c8d2ec8c67e2c3998c4bbceaa25b8d1e5b1097a8420b85e6f12",
37
37
  "sources": [
38
38
  "artifact:forma-marketing.css",
39
39
  "artifact:forma-marketing-theme-echelon.css"
@@ -42,8 +42,8 @@
42
42
  {
43
43
  "file": "forma-all.css",
44
44
  "description": "Complete Forma surface (application components, assessment, and marketing layers) for sites that also present Forma application patterns such as data grids or status lozenges.",
45
- "bytes": 336745,
46
- "sha256": "6666ea17b441934dd66807f9aefae86213ca228c92eed8da17faed2b2d816b18",
45
+ "bytes": 342064,
46
+ "sha256": "8355d917009ade27384b73f43e29c756ae9298e499e26b18cee7ba198edf4fcf",
47
47
  "sources": [
48
48
  "dist/all.css"
49
49
  ]
@@ -1,4 +1,4 @@
1
- d8a900aecd8aa4a72cbda731d3bbcfadbf872bc8aed1a8ef74f5ef3169af4cc6 forma-marketing.css
2
- e3e05cf66b87ac6be686896c353ab87f47ad1be589c1fc2895238d1bd987d9e6 forma-marketing-theme-echelon.css
3
- 837ddc667b3eccbc6a0512fe767708212a2aeffe18f9c2a27cbedad9be2937f3 forma-echelon-marketing.css
4
- 6666ea17b441934dd66807f9aefae86213ca228c92eed8da17faed2b2d816b18 forma-all.css
1
+ d15eb11a605676935a683fd1382bdecf2fd9c070893c80974766b4c22ab63b75 forma-marketing.css
2
+ 66fcf350b0d36535df6fcb6fbca32bc1236b31398c49eeccf3670342f38e5ac9 forma-marketing-theme-echelon.css
3
+ 08975a531a7c4c8d2ec8c67e2c3998c4bbceaa25b8d1e5b1097a8420b85e6f12 forma-echelon-marketing.css
4
+ 8355d917009ade27384b73f43e29c756ae9298e499e26b18cee7ba198edf4fcf forma-all.css
@@ -47,6 +47,10 @@
47
47
  --ef-type-size-h3: var(--ef-primitive-font-size-lg);
48
48
  --ef-type-size-h4: var(--ef-primitive-font-size-md);
49
49
  --ef-type-size-statement: var(--ef-primitive-font-size-fluid-display-3);
50
+ /* Display heading measures. A typography role (data-ef-type, see
51
+ src/styles/foundations.css) lifts them for application screens. */
52
+ --ef-type-measure-h1: 14ch;
53
+ --ef-type-measure-h2: 18ch;
50
54
  --ef-type-leading-body: var(--ef-primitive-line-height-normal);
51
55
  --ef-type-leading-lead: var(--ef-primitive-line-height-normal);
52
56
  --ef-type-leading-heading: var(--ef-primitive-line-height-snug);
@@ -239,8 +243,8 @@
239
243
  line-height: var(--ef-type-leading-heading);
240
244
  }
241
245
 
242
- .ef-site :where(h1) { font-size: var(--ef-type-size-h1); max-inline-size: 14ch; }
243
- .ef-site :where(h2) { font-size: var(--ef-type-size-h2); max-inline-size: 18ch; }
246
+ .ef-site :where(h1) { font-size: var(--ef-type-size-h1); max-inline-size: var(--ef-type-measure-h1); }
247
+ .ef-site :where(h2) { font-size: var(--ef-type-size-h2); max-inline-size: var(--ef-type-measure-h2); }
244
248
  .ef-site :where(h3) { font-size: var(--ef-type-size-h3); line-height: var(--ef-type-leading-snug); }
245
249
  .ef-site :where(h4, h5, h6) { font-size: var(--ef-type-size-h4); line-height: var(--ef-type-leading-snug); }
246
250
 
package/dist/runtime.js CHANGED
@@ -52,6 +52,6 @@ export function registerEchelonRuntime(name, metadata, target = globalThis) {
52
52
  return installEchelonRuntimeRegistrar(target)?.(name, metadata);
53
53
  }
54
54
 
55
- export const FORMA_VERSION = "0.10.0";
55
+ export const FORMA_VERSION = "0.11.0";
56
56
 
57
57
  if (typeof window !== "undefined") registerEchelonRuntime("forma", { version: FORMA_VERSION }, window);
@@ -104,7 +104,7 @@ saving.
104
104
  | `kindLabel` | Display text for the kind. Optional for core kinds. |
105
105
  | `label`, `description` | Visible text. |
106
106
  | `variant` | Forma shape: `rectangle`, `rounded`, `pill`, `ellipse` or `diamond`. Presentation only; a shape never implies a kind. |
107
- | `color` | `fill`, `stroke`, `accent` and `foreground`. Each is a `#rrggbb` literal, a Forma token (`token:accent-primary`) or a palette slot (`palette:finance`). |
107
+ | `color` | `fill`, `stroke`, `accent` and `foreground`. Each is a `#rrggbb` literal, a Forma token or a palette slot (`palette:finance`). Tokens: `token:accent-primary`, `token:accent-secondary`, `token:border-functional`, `token:border-subtle`, `token:surface-primary`, `token:surface-secondary`, `token:surface-inverse`, `token:text-primary`, `token:text-secondary`, and the status roles `token:status-warning`, `token:status-danger`, `token:status-success` and `token:status-info` (render to `var(--ef-color-status-*)`; use them for attention outlines instead of a brand literal). |
108
108
  | `status` | `{ "state": "active", "label": "In progress", "detail": "...", "updatedAt": "..." }`. Core states are `none`, `pending`, `ready`, `active`, `waiting`, `complete`, `blocked`, `failed`, `skipped`, `cancelled` and `unknown`. A namespaced state needs a `label`. |
109
109
  | `ports` | Connection points: `id`, `side` (`top`, `right`, `bottom`, `left`), `direction` (`in`, `out`, `inout`), `label`, `maxConnections`. |
110
110
  | `references` | Domain references: `id`, `system` (a namespace), `type`, `key`, `label` and `href`. Forma displays them and never dereferences them. |
@@ -84,8 +84,57 @@ JSON Schema validator enforces it, so there is no hand-maintained copy.
84
84
  | Groups | `GROUP-MISSING-MEMBER`, `GROUP-MISSING-PARENT`, `GROUP-CYCLE`, `GROUP-LANE-CONFLICT` |
85
85
  | Extensions | `EXT-RESERVED` (and the schema's namespace and object rules) |
86
86
  | Compatibility | `COMPAT-UNAVAILABLE`, `COMPAT-UNKNOWN-CAPABILITY` |
87
- | Accessibility (static) | `A11Y-TITLE`, `A11Y-NAME`, `A11Y-CONTRAST` (authored text colour against fill below 4.5:1), `A11Y-BRANCH-LABEL`, `A11Y-LEGEND` |
87
+ | Accessibility (static) | `A11Y-TITLE`, `A11Y-NAME`, `A11Y-CONTRAST` (authored text colour against fill below 4.5:1), `A11Y-NONTEXT-CONTRAST` (non-text indicators below 3:1, WCAG 1.4.11; see below), `A11Y-BRANCH-LABEL`, `A11Y-LEGEND` |
88
88
  | Security | `SEC-UNSAFE-URL` (anything other than http, https, mailto or relative; also control characters and leading spaces), `SEC-BIDI-CONTROL` |
89
89
  | Semantics (warnings) | `SEM-DECISION-BRANCHES`, `SEM-START-INCOMING`, `SEM-END-OUTGOING`, `LAYOUT-PARTIAL` |
90
90
 
91
91
  Errors decide the class. Warnings and notes do not.
92
+
93
+ ### Non-text contrast (`A11Y-NONTEXT-CONTRAST`)
94
+
95
+ The rule judges literal colours (`#rrggbb`, or a palette slot holding one) on
96
+ nodes, groups, legend swatches and edges against the 3:1 minimum of WCAG 1.4.11:
97
+
98
+ - **No authored fill.** The shape is drawn on the default surface, so its
99
+ stroke and accent are judged against both default surfaces: light `#f2efe7`
100
+ and dark `#171a18` (`--ef-color-surface-primary` in the compiled light theme
101
+ and in the dark theme, including `prefers-color-scheme: dark`). The weaker
102
+ theme is reported and named. Legend swatches and edge strokes are judged the
103
+ same way.
104
+ - **Pinned host theme.** When the host pins a theme (`--theme light|dark` on
105
+ `forma-workflow validate` and `render`, or `ValidationOptions.Theme` with
106
+ `Validation.loadWith`), default-surface comparisons use that theme's surface
107
+ only. Without a pinned theme the host follows `prefers-color-scheme`, so
108
+ both surfaces apply. Any other theme name is judged against both.
109
+ - **Literal fill.** The accent is an indicator and must reach 3:1 against the
110
+ fill. The stroke is the shape's boundary: it is reported when it is below
111
+ 3:1 against the fill *and* neither the stroke nor the fill reaches 3:1
112
+ against a default surface. A borderless filled shape (stroke equal to fill)
113
+ is valid wherever its fill stands out from the surface.
114
+
115
+ **Limits.** The rule cannot know which theme a token resolves in, so it does
116
+ not judge token values, token-backed palette slots, token fills, or
117
+ token/literal mixes (a token stroke on a literal fill, a literal stroke on a
118
+ token fill). Forma's own token pairs (every `status-*` token and
119
+ `border-functional` on `surface-primary` and `surface-secondary`, in every
120
+ shipped theme and brand) are covered by `tests/nontext-contrast.test.mjs`.
121
+ Only `#f2efe7` and `#171a18` are used as default surfaces. Literal colours
122
+ are judged against them, not against the surface of another Forma theme
123
+ (for example `high-contrast`, `warm-earth` or `dark-technical`) or a brand an
124
+ application applies; those surfaces are approximated by the light and dark
125
+ defaults.
126
+
127
+ The browser regression for this rule measures real Forma.Workflow output: the
128
+ sources and golden HTML and findings files live in
129
+ `tests/fixtures/workflow-nontext/`, and the F# suite checks them byte for
130
+ byte. After an intended change to the renderer or the rule, regenerate them
131
+ with `FORMA_UPDATE_GOLDEN=1 dotnet run --project tests/workflow/Forma.Workflow.Tests -c Release`.
132
+
133
+ **CSS custom properties bypass validation.** Colours an application sets
134
+ directly through `--ef-diagram-stroke`, `--ef-diagram-accent`,
135
+ `--ef-diagram-fill`, `--ef-diagram-connector-stroke` or
136
+ `--ef-diagram-foreground` never pass through the document, so no finding is
137
+ produced; `contracts/diagram-presentation.json` marks them
138
+ `"contrast": "consumer"`. An application that colours diagrams that way must
139
+ measure the result in its own browser tests (computed border colour against
140
+ computed background, as `tests/browser/diagram-nontext-contrast.spec.mjs` does).
@@ -80,7 +80,7 @@
80
80
  "node": "downlink"
81
81
  },
82
82
  "color": {
83
- "stroke": "#2f5fb3"
83
+ "stroke": "#3d6cc0"
84
84
  },
85
85
  "width": 3
86
86
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echelon-foundry/design-system",
3
- "version": "0.10.0",
3
+ "version": "0.11.0-main.260.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Zero-component-runtime HTML and CSS design system for Echelon Foundry applications.",
@@ -86,7 +86,7 @@
86
86
  "site:mobile": "playwright test --config=playwright.site.config.mjs",
87
87
  "pack:verify": "npm run build && npm pack --dry-run",
88
88
  "test:package": "node --test tests/package-contract.test.mjs tests/conformance-package.test.mjs",
89
- "test:css": "node --test tests/css-source.test.mjs tests/dist-css.test.mjs tests/contrast-pairs.test.mjs tests/diagram-contract.test.mjs",
89
+ "test:css": "node --test tests/css-source.test.mjs tests/dist-css.test.mjs tests/contrast-pairs.test.mjs tests/nontext-contrast.test.mjs tests/diagram-contract.test.mjs",
90
90
  "test:contrast": "node --test tests/contrast-pairs.test.mjs && playwright test tests/browser/pattern-contrast.spec.mjs --project=chromium",
91
91
  "brands:build": "dotnet run --project tools/BrandCompiler/BrandCompiler.fsproj -- brands dist/brands",
92
92
  "color:build": "node tools/color-contract/build.mjs",
@@ -259,7 +259,11 @@
259
259
  "token:surface-secondary",
260
260
  "token:surface-inverse",
261
261
  "token:text-primary",
262
- "token:text-secondary"
262
+ "token:text-secondary",
263
+ "token:status-warning",
264
+ "token:status-danger",
265
+ "token:status-success",
266
+ "token:status-info"
263
267
  ]
264
268
  }
265
269
  ]