@incodetech/prizma-core-screens 0.2.3 → 0.2.7

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 (116) hide show
  1. package/dist/CurpEnterScreen.d.ts +7 -0
  2. package/dist/CurpEnterScreen.js +17 -3
  3. package/dist/CurpGenerateScreen.d.ts +8 -0
  4. package/dist/CurpGenerateScreen.js +28 -13
  5. package/dist/CurpLayout.css +8 -34
  6. package/dist/CurpStrings.d.ts +16 -2
  7. package/dist/EkybAddressDetailsScreen.d.ts +7 -0
  8. package/dist/EkybAddressDetailsScreen.js +25 -7
  9. package/dist/EkybBusinessDetailsScreen.d.ts +6 -0
  10. package/dist/EkybBusinessDetailsScreen.js +23 -6
  11. package/dist/EkybCountryScreen.d.ts +13 -5
  12. package/dist/EkybCountryScreen.js +27 -9
  13. package/dist/EkybOwnersScreen.d.ts +8 -0
  14. package/dist/EkybOwnersScreen.js +26 -3
  15. package/dist/EkybStrings.d.ts +27 -10
  16. package/dist/EkycCountryCatalog.d.ts +4 -1
  17. package/dist/EkycCountryCatalog.js +5 -3
  18. package/dist/EkycCountryFormScreen.d.ts +12 -3
  19. package/dist/EkycCountryFormScreen.js +28 -10
  20. package/dist/EkycDetailsScreen.d.ts +7 -0
  21. package/dist/EkycDetailsScreen.js +21 -4
  22. package/dist/EkycStrings.d.ts +14 -2
  23. package/dist/ElectronicSignatureLayout.css +6 -0
  24. package/dist/EmailInputStrings.d.ts +11 -1
  25. package/dist/EmailInputVerifyScreen.d.ts +7 -0
  26. package/dist/EmailInputVerifyScreen.js +17 -3
  27. package/dist/FaceMatchComparisonScreen.d.ts +3 -3
  28. package/dist/FaceMatchLayout.css +21 -12
  29. package/dist/FormScreen.css +16 -0
  30. package/dist/FormsLongFormScreen.d.ts +8 -0
  31. package/dist/FormsLongFormScreen.js +34 -13
  32. package/dist/FormsStrings.d.ts +11 -1
  33. package/dist/GeolocationBrowserStepsScreen.d.ts +6 -2
  34. package/dist/GeolocationBrowserStepsScreen.js +7 -3
  35. package/dist/GeolocationStrings.d.ts +3 -0
  36. package/dist/IdCaptureLayout.css +40 -0
  37. package/dist/ListScreen.d.ts +2 -0
  38. package/dist/ListScreen.js +1 -1
  39. package/dist/PhoneInputLayout.css +10 -9
  40. package/dist/PhoneInputStrings.d.ts +11 -1
  41. package/dist/PhoneInputVerifyScreen.d.ts +8 -0
  42. package/dist/PhoneInputVerifyScreen.js +19 -6
  43. package/dist/QualifiedSignatureAcceptScreen.js +6 -1
  44. package/dist/QualifiedSignatureDocumentScreen.d.ts +13 -9
  45. package/dist/QualifiedSignatureDocumentScreen.js +18 -16
  46. package/dist/QualifiedSignatureLayout.css +76 -0
  47. package/dist/QualifiedSignatureStrings.d.ts +10 -0
  48. package/dist/ScreenNav.css +8 -0
  49. package/dist/ScreenNav.d.ts +15 -2
  50. package/dist/ScreenNav.js +10 -3
  51. package/dist/VideoSelfieIdCaptureScreen.d.ts +10 -3
  52. package/dist/VideoSelfieIdCaptureScreen.js +44 -13
  53. package/dist/VideoSelfieLayout.css +164 -33
  54. package/dist/VideoSelfieSelfieCaptureScreen.d.ts +44 -27
  55. package/dist/VideoSelfieSelfieCaptureScreen.js +65 -39
  56. package/dist/VideoSelfieSpeakScreen.js +18 -1
  57. package/dist/VideoSelfieStrings.d.ts +78 -12
  58. package/dist/VideoSelfieTurnHeadScreen.d.ts +51 -0
  59. package/dist/VideoSelfieTurnHeadScreen.js +63 -0
  60. package/dist/VideoSelfieTutorialScreen.js +13 -5
  61. package/dist/VideoSelfieWaveScreen.d.ts +53 -0
  62. package/dist/VideoSelfieWaveScreen.js +62 -0
  63. package/dist/WatchlistBusinessFormScreen.d.ts +11 -7
  64. package/dist/WatchlistBusinessFormScreen.js +27 -13
  65. package/dist/WatchlistBusinessStrings.d.ts +21 -7
  66. package/dist/index.d.ts +3 -1
  67. package/dist/index.js +2 -0
  68. package/dist/types.d.ts +3 -1
  69. package/package.json +14 -3
  70. package/samples/video-selfie-room.png +0 -0
  71. package/src/CurpEnterScreen.tsx +21 -4
  72. package/src/CurpGenerateScreen.tsx +46 -22
  73. package/src/CurpLayout.css +8 -34
  74. package/src/CurpStrings.ts +22 -2
  75. package/src/EkybAddressDetailsScreen.tsx +32 -4
  76. package/src/EkybBusinessDetailsScreen.tsx +23 -5
  77. package/src/EkybCountryScreen.tsx +28 -7
  78. package/src/EkybOwnersScreen.tsx +34 -3
  79. package/src/EkybStrings.ts +29 -10
  80. package/src/EkycCountryCatalog.ts +9 -4
  81. package/src/EkycCountryFormScreen.tsx +51 -12
  82. package/src/EkycDetailsScreen.tsx +22 -1
  83. package/src/EkycStrings.ts +14 -2
  84. package/src/ElectronicSignatureLayout.css +6 -0
  85. package/src/EmailInputStrings.ts +12 -1
  86. package/src/EmailInputVerifyScreen.tsx +21 -4
  87. package/src/FaceMatchComparisonScreen.tsx +3 -3
  88. package/src/FaceMatchLayout.css +21 -12
  89. package/src/FormScreen.css +16 -0
  90. package/src/FormsLongFormScreen.tsx +41 -13
  91. package/src/FormsStrings.ts +12 -1
  92. package/src/GeolocationBrowserStepsScreen.tsx +6 -3
  93. package/src/GeolocationStrings.ts +3 -0
  94. package/src/IdCaptureLayout.css +40 -0
  95. package/src/ListScreen.tsx +10 -1
  96. package/src/PhoneInputLayout.css +10 -9
  97. package/src/PhoneInputStrings.ts +12 -1
  98. package/src/PhoneInputVerifyScreen.tsx +21 -5
  99. package/src/QualifiedSignatureAcceptScreen.tsx +12 -1
  100. package/src/QualifiedSignatureDocumentScreen.tsx +27 -17
  101. package/src/QualifiedSignatureLayout.css +76 -0
  102. package/src/QualifiedSignatureStrings.ts +10 -0
  103. package/src/ScreenNav.css +8 -0
  104. package/src/ScreenNav.tsx +35 -5
  105. package/src/VideoSelfieIdCaptureScreen.tsx +116 -30
  106. package/src/VideoSelfieLayout.css +164 -33
  107. package/src/VideoSelfieSelfieCaptureScreen.tsx +104 -59
  108. package/src/VideoSelfieSpeakScreen.tsx +25 -14
  109. package/src/VideoSelfieStrings.ts +85 -12
  110. package/src/VideoSelfieTurnHeadScreen.tsx +151 -0
  111. package/src/VideoSelfieTutorialScreen.tsx +34 -23
  112. package/src/VideoSelfieWaveScreen.tsx +135 -0
  113. package/src/WatchlistBusinessFormScreen.tsx +35 -16
  114. package/src/WatchlistBusinessStrings.ts +21 -7
  115. package/src/index.ts +14 -0
  116. package/src/types.ts +3 -1
@@ -33,7 +33,9 @@ const PRESET_ROWS = {
33
33
  { key: 'idNumber', kind: 'text', requirement: 'required' },
34
34
  { key: 'email', kind: 'email', requirement: 'required' },
35
35
  { key: 'residence', kind: 'country', requirement: 'required' },
36
- { key: 'dob', kind: 'date', requirement: 'required' },
36
+ // Date of birth carries NO asterisk (owner Elena, 2026-09-30), so the
37
+ // `emptyError` state flags the other three rows and leaves it at rest.
38
+ { key: 'dob', kind: 'date', requirement: 'none' },
37
39
  ],
38
40
  };
39
41
  /**
@@ -85,10 +87,19 @@ const OPTIONAL_VARIANT = {
85
87
  * into the CTA band — the half a module stylesheet kept losing. It is a no-op
86
88
  * on the short `name` preset, whose rows never overflow. DEVIATIONS #DSC-2,
87
89
  * restating #CURP-1.
90
+ *
91
+ * `emptyError` is the GUIDELINES §6-E empty-submit rule (Elena 2026-09-30):
92
+ * the user pressed the CTA with the form still empty. Every REQUIRED row
93
+ * renders empty, with no placeholder, in Core's own error state carrying
94
+ * `strings.requiredError`; optional and unmarked rows stay on their resting
95
+ * surface with no error. The CTA greys there and ONLY there: the resting
96
+ * `default` keeps it enabled, so the user can press it to reach the error —
97
+ * the `FormsFieldScreen` `emptyError` precedent applied to a stack.
88
98
  */
89
99
  export const FormsLongFormScreen = forwardRef(({ strings, preset = 'eKyc', state = 'default', values, focusedField, options, countries, screenId = `forms-long-form-${preset}-${state}`, className, overrides, onAction, ...shell }, ref) => {
90
100
  const copy = strings.longForm[preset];
91
101
  const busy = state === 'loading';
102
+ const emptyError = state === 'emptyError';
92
103
  const countryList = countries ?? [];
93
104
  // The Optional / Required pair documents the eKYC form specifically; no
94
105
  // other preset draws it, so the override never leaks to a preset whose
@@ -106,20 +117,30 @@ export const FormsLongFormScreen = forwardRef(({ strings, preset = 'eKyc', state
106
117
  const fieldCopy = copy.fields[row.key];
107
118
  const label = labelFor(row);
108
119
  const required = row.requirement === 'required';
109
- const value = values?.[row.key] ?? '';
110
- const focused = !busy && focusedField === row.key;
120
+ // `emptyError` is an EMPTY submit: every row renders empty, whatever
121
+ // values the host passed.
122
+ const value = emptyError ? '' : (values?.[row.key] ?? '');
123
+ const focused = !busy && !emptyError && focusedField === row.key;
124
+ // Only REQUIRED rows flag the empty submit. Their placeholder goes, as
125
+ // on the drawn `forms.IDnumber.error`; optional rows stay at rest.
126
+ const invalid = emptyError && required;
127
+ const errorText = invalid ? strings.requiredError : undefined;
128
+ const placeholder = invalid ? undefined : fieldCopy?.placeholder;
111
129
  switch (row.kind) {
112
130
  case 'country':
113
- return (_jsx(PrizmaDropdown, { label: label, required: required, requiredHint: strings.requiredHint, placeholder: fieldCopy?.placeholder, options: countryList.map((c) => ({ value: c.code, label: c.name })), value: value || null, searchable: true, searchPlaceholder: copy.searchPlaceholder, disabled: busy, forceState: focused ? 'focused' : undefined, renderLeading: (option) => {
131
+ return (_jsx(PrizmaDropdown, { label: label, required: required, requiredHint: strings.requiredHint, placeholder: placeholder, options: countryList.map((c) => ({ value: c.code, label: c.name })), value: value || null, searchable: true, searchPlaceholder: copy.searchPlaceholder, disabled: busy, error: invalid, errorText: errorText, forceState: focused ? 'focused' : undefined, renderLeading: (option) => {
114
132
  const match = countryList.find((c) => c.code === option.value);
115
133
  return match ? _jsx(PrizmaCountryFlag, { country: match }) : null;
116
134
  } }, row.key));
117
135
  case 'select':
118
- return (_jsx(PrizmaDropdown, { label: label, required: required, requiredHint: strings.requiredHint, placeholder: fieldCopy?.placeholder, options: options?.[row.key] ?? [], value: value || null, disabled: busy, forceState: focused ? 'focused' : undefined }, row.key));
136
+ return (_jsx(PrizmaDropdown, { label: label, required: required, requiredHint: strings.requiredHint, placeholder: placeholder, options: options?.[row.key] ?? [], value: value || null, disabled: busy, error: invalid, errorText: errorText, forceState: focused ? 'focused' : undefined }, row.key));
119
137
  case 'date':
120
- return (_jsx(PrizmaDateField, { className: focused ? 'prizma-input-datefield--focused' : undefined, label: label, required: required, requiredHint: strings.requiredHint, placeholder: fieldCopy?.placeholder, defaultValue: value, disabled: busy, calendarLabel: copy.calendarLabel }, row.key));
138
+ return (_jsx(PrizmaDateField, { className: focused ? 'prizma-input-datefield--focused' : undefined, label: label, required: required, requiredHint: strings.requiredHint,
139
+ /* Core falls back to its DD/MM/YYYY mask on an undefined
140
+ placeholder, so the error row passes an explicit empty one. */
141
+ placeholder: invalid ? '' : fieldCopy?.placeholder, defaultValue: value, disabled: busy, error: invalid, errorText: errorText, calendarLabel: copy.calendarLabel }, row.key));
121
142
  default:
122
- return (_jsx(PrizmaTextField, { className: focused ? 'prizma-input-textfield--focused' : undefined, label: label, required: required, requiredHint: strings.requiredHint, placeholder: fieldCopy?.placeholder, defaultValue: value, type: row.kind === 'email' ? 'email' : 'text', inputMode: row.kind === 'email' ? 'email' : undefined, autoComplete: row.kind === 'email' ? 'email' : 'off', autoCapitalize: row.kind === 'email' ? 'none' : undefined, spellCheck: false, disabled: busy,
143
+ return (_jsx(PrizmaTextField, { className: focused ? 'prizma-input-textfield--focused' : undefined, label: label, required: required, requiredHint: strings.requiredHint, placeholder: placeholder, defaultValue: value, type: row.kind === 'email' ? 'email' : 'text', inputMode: row.kind === 'email' ? 'email' : undefined, autoComplete: row.kind === 'email' ? 'email' : 'off', autoCapitalize: row.kind === 'email' ? 'none' : undefined, spellCheck: false, disabled: busy, error: invalid, errorText: errorText,
123
144
  /* Figma draws the clear ✕ only on the focused row of
124
145
  `longform.focused`; every other row draws none. */
125
146
  clearable: focused, clearLabel: copy.clearLabel }, row.key));
@@ -134,12 +155,12 @@ export const FormsLongFormScreen = forwardRef(({ strings, preset = 'eKyc', state
134
155
  ]
135
156
  .filter(Boolean)
136
157
  .join(' '), title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel,
137
- /* `longform.default` and `longform.customizable`'s default section grey
138
- the CTA until something is entered; `forms.name` draws it ENABLED on
139
- the same empty form. Figma contradicts its own "optimistic rule"
140
- sticky here, so each section is followed literally — DEVIATIONS
141
- #FDE-3. */
142
- ctaDisabled: state === 'default' && preset !== 'name', ctaLoading: busy, ctaLoadingLabel: copy.ctaLoadingLabel,
158
+ /* GUIDELINES §6-E empty-submit rule (Elena 2026-09-30): the CTA stays
159
+ ENABLED on the resting empty form, every preset alike, so the user
160
+ can press it; it greys only on `emptyError`, the state that press
161
+ leads to. This supersedes the per-section greying #FDE-3 followed
162
+ on `longform.default` and `longform.customizable`. */
163
+ ctaDisabled: emptyError, ctaLoading: busy, ctaLoadingLabel: copy.ctaLoadingLabel,
143
164
  /* The four- and five-row stacks are taller than the 844 frame; the
144
165
  archetype's variant pans them and keeps Spacing/16 of clear surface
145
166
  above the CTA. No module CSS re-declares this. */
@@ -80,8 +80,12 @@ export type FormsLongFormPreset = 'name' | 'profile' | 'eKyc';
80
80
  * - `loading` — `longform.loading`: the whole form greyed, spinner in the CTA
81
81
  * - `optional` — the "Optional / Required" documentation pair: the same form
82
82
  * with mixed required and optional markers
83
+ * - `emptyError` — the user pressed the CTA with the form still empty: every
84
+ * REQUIRED row renders empty in Core's error state with
85
+ * `requiredError`, optional and unmarked rows stay clean, CTA
86
+ * greyed (GUIDELINES §6-E empty-submit rule, Elena 2026-09-30)
83
87
  */
84
- export type FormsLongFormState = 'default' | 'focused' | 'filled' | 'loading' | 'optional';
88
+ export type FormsLongFormState = 'default' | 'focused' | 'filled' | 'loading' | 'optional' | 'emptyError';
85
89
  /** Copy for one single-field section. */
86
90
  export interface FormsFieldCopy {
87
91
  /** H2 ("Enter your title", "Enter your email", "Enter your phone number"). */
@@ -161,6 +165,12 @@ export interface FormsStrings {
161
165
  * the label string by the screen. See DEVIATIONS #FDE-4.
162
166
  */
163
167
  optionalHint: string;
168
+ /**
169
+ * The error row every REQUIRED field shows on the `emptyError` long-form
170
+ * state ("This field is required"). Maps to the SDK's
171
+ * `dynamicForms.fieldRequired` key. GUIDELINES §6-E empty-submit rule.
172
+ */
173
+ requiredError: string;
164
174
  /** The single-field sections. */
165
175
  fields: Record<FormsFieldKey, FormsFieldCopy>;
166
176
  /** The choice sections. */
@@ -31,9 +31,13 @@ export interface GeolocationBrowserStepsScreenProps extends Omit<PrizmaListScree
31
31
  * The rows are Core's `PrizmaCardListCommonIssues` verbatim; only the leading
32
32
  * glyph is chosen here, from the canonical bank (see `STEP_ICONS`).
33
33
  *
34
+ * No subtitle: the browser name (`label`) is NOT drawn under the title (owner
35
+ * call, Elena 2026-09-30). The label stays in the strings as the section's
36
+ * identifier for hosts and catalogs; the screen itself shows title + steps.
37
+ *
34
38
  * The title block is LEFT-aligned for this whole family. Every
35
- * `Geolocation.allowLocation.<browser>` section starts the two-line headline —
36
- * and the browser label under it — flush at the gutter, over left-led step
39
+ * `Geolocation.allowLocation.<browser>` section starts the two-line headline
40
+ * flush at the gutter, over left-led step
37
41
  * rows; a centred headline above a left-aligned list reads as a mistake. The
38
42
  * screen therefore DEFAULTS the archetype's own `align` override to `left`
39
43
  * rather than declaring any alignment of its own, and a host that passes
@@ -55,9 +55,13 @@ const STEP_ICONS = {
55
55
  * The rows are Core's `PrizmaCardListCommonIssues` verbatim; only the leading
56
56
  * glyph is chosen here, from the canonical bank (see `STEP_ICONS`).
57
57
  *
58
+ * No subtitle: the browser name (`label`) is NOT drawn under the title (owner
59
+ * call, Elena 2026-09-30). The label stays in the strings as the section's
60
+ * identifier for hosts and catalogs; the screen itself shows title + steps.
61
+ *
58
62
  * The title block is LEFT-aligned for this whole family. Every
59
- * `Geolocation.allowLocation.<browser>` section starts the two-line headline —
60
- * and the browser label under it — flush at the gutter, over left-led step
63
+ * `Geolocation.allowLocation.<browser>` section starts the two-line headline
64
+ * flush at the gutter, over left-led step
61
65
  * rows; a centred headline above a left-aligned list reads as a mistake. The
62
66
  * screen therefore DEFAULTS the archetype's own `align` override to `left`
63
67
  * rather than declaring any alignment of its own, and a host that passes
@@ -85,7 +89,7 @@ export const GeolocationBrowserStepsScreen = forwardRef(({ strings, browser = 's
85
89
  ...overrides,
86
90
  align: overrides?.align ?? 'left',
87
91
  };
88
- return (_jsx(PrizmaListScreen, { ref: ref, title: strings.browserSteps.title, subtitle: slice?.label, ctaLabel: strings.browserSteps.ctaLabel, secondaryCtaLabel: showSkip ? strings.browserSteps.skipLabel : undefined, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: alignedOverrides, onAction: onAction, ...shell, children: _jsx(PrizmaCardListCommonIssues, { items: (slice?.steps ?? []).map((step) => ({
92
+ return (_jsx(PrizmaListScreen, { ref: ref, title: strings.browserSteps.title, ctaLabel: strings.browserSteps.ctaLabel, secondaryCtaLabel: showSkip ? strings.browserSteps.skipLabel : undefined, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: alignedOverrides, onAction: onAction, ...shell, children: _jsx(PrizmaCardListCommonIssues, { items: (slice?.steps ?? []).map((step) => ({
89
93
  icon: STEP_ICONS[step.kind],
90
94
  title: step.label,
91
95
  showSubtitle: false,
@@ -76,6 +76,9 @@ export interface GeolocationBrowserSteps {
76
76
  * rows and would render as one screen three times. Every browser therefore
77
77
  * SHOULD carry a label, extending the precedent Figma itself sets on Safari.
78
78
  * Still optional so a host that ships its own logo artwork can drop it.
79
+ *
80
+ * 2026-09-30 (Elena): the screen no longer RENDERS it as a subtitle. It is
81
+ * kept as the section's name for catalogs and hosts, not as on-screen copy.
79
82
  */
80
83
  label?: string;
81
84
  /** The steps, in order. */
@@ -59,6 +59,46 @@
59
59
  gap: var(--prizma-spacing-8); /* Title → instruction, GUIDELINES §5 */
60
60
  }
61
61
 
62
+ /* ── Short screens: the vertical frame gives up height, never the title ──────
63
+ * The vertical ID (288:456) and passport (306:420) frames are tall. On a
64
+ * 320 × 693 phone the column is 272 wide, so the frame alone is ~431 tall and
65
+ * the stage ran 168px past the bottom, under the shutter strip (Camila,
66
+ * 2026-09-28). Here the frame is the ONE part of the column that shrinks: it
67
+ * starts at its full-width height (the column less its two Spacing/24
68
+ * gutters, over the aspect), and when the screen is too short the flex column
69
+ * takes the shortfall out of its height; the aspect ratio then narrows its
70
+ * width to match, so it stays an ID-shaped frame, centred. The title band
71
+ * keeps its content height. With room to spare (390 × 844) the frame never
72
+ * needs to shrink, so it sits exactly where it did.
73
+ *
74
+ * Scoped to ID + passport with `:not(--poa)`: Document Capture renders the
75
+ * same stage as `type="poa" isVertical`, so it carries `--vertical` too, but
76
+ * the PoA frame has NO aspect ratio (it fills the host's height). `width:
77
+ * auto` there collapsed it to a 0-wide sliver, a grey vertical bar down the
78
+ * camera screen (Elena, 2026-09-30). Only showed where this file and Document
79
+ * Capture's CSS ship in the same bundle, i.e. the production build. */
80
+ .prizma-viewfinder__content:has(> .prizma-id-capture--vertical:not(.prizma-id-capture--poa)) {
81
+ min-height: 0;
82
+ }
83
+ .prizma-viewfinder__content:has(> .prizma-id-capture--vertical:not(.prizma-id-capture--poa)) > .id-capture-band--top {
84
+ flex-shrink: 0;
85
+ min-height: min-content;
86
+ }
87
+ .prizma-viewfinder__content > .prizma-id-capture--vertical:not(.prizma-id-capture--poa) {
88
+ flex: 0 1 auto;
89
+ min-height: 0;
90
+ }
91
+ .prizma-viewfinder__content > .prizma-id-capture--vertical:not(.prizma-id-capture--poa) .prizma-id-capture__frame {
92
+ flex: 0 1 auto;
93
+ min-height: 0;
94
+ width: auto;
95
+ max-width: 100%;
96
+ flex-basis: calc((100cqw - 2 * var(--prizma-spacing-24)) * 456 / 288);
97
+ }
98
+ .prizma-viewfinder__content > .prizma-id-capture--passport.prizma-id-capture--vertical .prizma-id-capture__frame {
99
+ flex-basis: calc((100cqw - 2 * var(--prizma-spacing-24)) * 420 / 306);
100
+ }
101
+
62
102
  /* ── Camera stage (dark) ───────────────────────────────────────────────────── */
63
103
 
64
104
  /* The stage's nav row carries only the close X (no logo — the camera owns the
@@ -64,6 +64,8 @@ export interface PrizmaListScreenProps extends Omit<PrizmaScreenShellProps, 'dar
64
64
  /** "verified by" attribution copy for the footer tag. */
65
65
  verifiedLabel: string;
66
66
  navLabel?: string;
67
+ /** Opt-in back chevron in the nav (its accessible name). Emits `back`. */
68
+ backLabel?: string;
67
69
  screenId?: string;
68
70
  overrides?: PrizmaScreenOverrides;
69
71
  onAction?: PrizmaScreenActionHandler;
@@ -31,7 +31,7 @@ import './ScreenLayout.css';
31
31
  * Reach for `PrizmaResultScreen` instead when the middle is a centred verdict
32
32
  * rather than a list.
33
33
  */
34
- export const PrizmaListScreen = forwardRef(({ progress, status = 'none', title, subtitle, glyph, children, listAlign = 'start', scrollList = false, disclaimer, ctaLabel, secondaryCtaLabel, linkLabel, verifiedLabel, navLabel, screenId, showHeader = true, showFooter = true, overrides, onAction, ...shell }, ref) => (_jsxs(PrizmaScreenShell, { ref: ref, ...shell, children: [_jsx(PrizmaScreenNav, { reserved: !showHeader, logo: overrides?.logo, navLabel: navLabel }), progress ? (_jsx("div", { className: "prizma-screen-layout__progress", children: progress })) : null, _jsxs("div", { className: "prizma-screen-layout__content", children: [glyph, _jsx(PrizmaTitle, { variant: overrides?.align === 'left' ? 'left' : 'centered', title: title, showSubtitle: Boolean(subtitle), subtitle: subtitle, icon: status === 'loading'
34
+ export const PrizmaListScreen = forwardRef(({ progress, status = 'none', title, subtitle, glyph, children, listAlign = 'start', scrollList = false, disclaimer, ctaLabel, secondaryCtaLabel, linkLabel, verifiedLabel, navLabel, backLabel, screenId, showHeader = true, showFooter = true, overrides, onAction, ...shell }, ref) => (_jsxs(PrizmaScreenShell, { ref: ref, ...shell, children: [_jsx(PrizmaScreenNav, { reserved: !showHeader, logo: overrides?.logo, navLabel: navLabel, backLabel: backLabel, onBack: () => onAction?.({ type: 'back', screenId }) }), progress ? (_jsx("div", { className: "prizma-screen-layout__progress", children: progress })) : null, _jsxs("div", { className: "prizma-screen-layout__content", children: [glyph, _jsx(PrizmaTitle, { variant: overrides?.align === 'left' ? 'left' : 'centered', title: title, showSubtitle: Boolean(subtitle), subtitle: subtitle, icon: status === 'loading'
35
35
  ? { type: 'spinner' }
36
36
  : status === 'none'
37
37
  ? { type: 'none' }
@@ -21,6 +21,7 @@
21
21
  align-items: flex-start;
22
22
  gap: var(--prizma-spacing-8);
23
23
  width: 100%;
24
+ position: relative; /* the country menu's containing block — see below */
24
25
  }
25
26
 
26
27
  /* The dial-code dropdown hugs its content ("🇷🇸 +381"), as Figma draws it.
@@ -47,15 +48,15 @@
47
48
  min-width: 0;
48
49
  }
49
50
 
50
- /* The open menu must NOT inherit the hugged anchor's width: Core anchors the
51
- * menu to the trigger, so once the trigger hugs "🇷🇸 +381" the option rows
52
- * truncate ("+…"). The menu keeps its own readable width — it lists country
53
- * names and dial codes, which the closed field never shows. Sized to the
54
- * content and capped at the 342 column so it never escapes the screen.
51
+ /* The open menu spans the WHOLE row (dial code + number field), as Figma's
52
+ * `Input / Combined` draws it: Core anchors the menu overlay to the trigger's
53
+ * `__anchor` (`position: relative`, Dropdown.css), so once the trigger hugs
54
+ * "🇷🇸 +381" the menu came out as narrow as the dial code. Releasing the
55
+ * anchor's positioning hands the overlay's `left: 0; right: 0` to the row
56
+ * above (`position: relative`), so the menu matches the row's width exactly.
57
+ * Core's own overlay offset (`top: 100% + Spacing/8`) is left untouched.
55
58
  * DEVIATIONS #PI-1 (same root cause as the hug).
56
59
  */
57
- .phone-input-combined__country .prizma-dropdown__menu {
58
- width: max-content;
59
- min-width: 100%;
60
- max-width: 342px;
60
+ .phone-input-combined__country .prizma-dropdown__anchor {
61
+ position: static;
61
62
  }
@@ -7,8 +7,12 @@
7
7
  * - `typing` — partial value, focused field
8
8
  * - `filled` — complete value
9
9
  * - `loading` — CTA in its loading treatment while the code is requested
10
+ * - `emptyError` — "Continue" pressed with no number typed: the phone field
11
+ * stays empty (no placeholder) in Core's error state with `requiredError`,
12
+ * and the CTA is disabled. The dial code always carries a selection, so it
13
+ * takes no error. GUIDELINES §6-E empty-submit rule (Elena, 2026-09-30).
10
14
  */
11
- export type PhoneInputVerifyState = 'default' | 'countryCode' | 'typing' | 'filled' | 'loading';
15
+ export type PhoneInputVerifyState = 'default' | 'countryCode' | 'typing' | 'filled' | 'loading' | 'emptyError';
12
16
  /**
13
17
  * The `phoneNumber.OTP.*` sections — same layout, different field state:
14
18
  * - `typing` — partial code
@@ -41,6 +45,12 @@ export interface PhoneInputStrings {
41
45
  verifiedLabel: string;
42
46
  /** Accessible name for the logo nav row. */
43
47
  navLabel?: string;
48
+ /**
49
+ * Error row under the required field when the primary CTA is pressed with it
50
+ * still empty (the verify screen's `emptyError` state): "This field is
51
+ * required". SDK key `dynamicForms.fieldRequired`.
52
+ */
53
+ requiredError: string;
44
54
  /** phoneNumber.verify.* */
45
55
  verify: {
46
56
  /** H2 ("Verify your phone number"). */
@@ -36,6 +36,14 @@ export interface PhoneInputVerifyScreenProps extends Omit<PrizmaFormScreenProps,
36
36
  * primitives (`PrizmaDropdown`, `PrizmaTextField`) at the designed 56px min
37
37
  * height and `Border/Radius/small`, so the module layer contributes only the
38
38
  * row that pairs them (`PhoneInputLayout.css`) — no primitive is re-implemented.
39
+ *
40
+ * `emptyError` is the empty-submit state (rule from Elena, 2026-09-30,
41
+ * GUIDELINES §6-E empty-submit rule): "Continue" was pressed with no number
42
+ * typed. The phone field renders empty with no placeholder, in Core's error
43
+ * state with `requiredError`, and the primary CTA is disabled, following the
44
+ * drawn precedent in `FormsFieldScreen`. The dial code always carries a
45
+ * selection, so it takes no error. The resting `default` keeps the CTA enabled
46
+ * so the user can reach this state.
39
47
  */
40
48
  export declare const PhoneInputVerifyScreen: React.ForwardRefExoticComponent<PhoneInputVerifyScreenProps & React.RefAttributes<HTMLDivElement>>;
41
49
  export default PhoneInputVerifyScreen;
@@ -29,14 +29,24 @@ const DEFAULT_COUNTRIES = [
29
29
  * primitives (`PrizmaDropdown`, `PrizmaTextField`) at the designed 56px min
30
30
  * height and `Border/Radius/small`, so the module layer contributes only the
31
31
  * row that pairs them (`PhoneInputLayout.css`) — no primitive is re-implemented.
32
+ *
33
+ * `emptyError` is the empty-submit state (rule from Elena, 2026-09-30,
34
+ * GUIDELINES §6-E empty-submit rule): "Continue" was pressed with no number
35
+ * typed. The phone field renders empty with no placeholder, in Core's error
36
+ * state with `requiredError`, and the primary CTA is disabled, following the
37
+ * drawn precedent in `FormsFieldScreen`. The dial code always carries a
38
+ * selection, so it takes no error. The resting `default` keeps the CTA enabled
39
+ * so the user can reach this state.
32
40
  */
33
41
  export const PhoneInputVerifyScreen = forwardRef(({ strings, state = 'default', countries = DEFAULT_COUNTRIES, country, value, screenId = `phone-input-verify-${state}`, overrides, onAction, ...shell }, ref) => {
34
42
  const selected = country ?? countries[0]?.value;
35
43
  const options = countries.map((c) => ({
36
44
  value: c.value,
37
- // The closed field shows flag + dial code exactly as Figma draws it
38
- // ("🇷🇸 +381"); the open menu row adds the country name.
39
- label: [c.flag, c.dialCode].filter(Boolean).join(' '),
45
+ // The open menu row reads flag + dial code + country name
46
+ // ("🇷🇸 +381 Serbia"); the closed field keeps only flag + dial code
47
+ // ("🇷🇸 +381") through Core's `fieldLabel`, exactly as Figma draws it.
48
+ label: [c.flag, c.dialCode, c.label].filter(Boolean).join(' '),
49
+ fieldLabel: [c.flag, c.dialCode].filter(Boolean).join(' '),
40
50
  }));
41
51
  // Figma treats the states as three distinct field treatments, not one:
42
52
  // `loading` greys BOTH fields (the request is in flight and the form is
@@ -47,9 +57,12 @@ export const PhoneInputVerifyScreen = forwardRef(({ strings, state = 'default',
47
57
  // for none of them — it would grey the resting states too (DEVIATIONS #PI-6).
48
58
  const busy = state === 'loading';
49
59
  const focused = state === 'typing';
60
+ // Empty submit: the number field drops its placeholder and takes Core's
61
+ // error state with the shared "required" row; the CTA greys.
62
+ const emptyError = state === 'emptyError';
50
63
  return (_jsx(PrizmaFormScreen, { ref: ref, title: strings.verify.title, subtitle: strings.verify.subtitle, ctaLabel: strings.verify.ctaLabel,
51
64
  /* Figma's `loading` section swaps the CTA label for Core's spinner. */
52
- ctaLoading: busy, ctaLoadingLabel: strings.verify.ctaLoadingLabel, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: overrides, onAction: onAction, ...shell, children: _jsxs("div", { className: "phone-input-combined", children: [_jsx(PrizmaDropdown, { className: "phone-input-combined__country", "aria-label": strings.verify.countryLabel, options: options, value: selected, searchable: true, searchPlaceholder: strings.verify.countrySearchPlaceholder,
65
+ ctaLoading: busy, ctaDisabled: emptyError, ctaLoadingLabel: strings.verify.ctaLoadingLabel, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: overrides, onAction: onAction, ...shell, children: _jsxs("div", { className: "phone-input-combined", children: [_jsx(PrizmaDropdown, { className: "phone-input-combined__country", "aria-label": strings.verify.countryLabel, options: options, value: selected, searchable: true, searchPlaceholder: strings.verify.countrySearchPlaceholder,
53
66
  /* The `countryCode` section IS the open-menu spec state. */
54
67
  forceOpen: state === 'countryCode', disabled: busy }), _jsx(PrizmaTextField, { className: [
55
68
  'phone-input-combined__number',
@@ -59,11 +72,11 @@ export const PhoneInputVerifyScreen = forwardRef(({ strings, state = 'default',
59
72
  focused ? 'prizma-input-textfield--focused' : '',
60
73
  ]
61
74
  .filter(Boolean)
62
- .join(' '), type: "tel", inputMode: "tel", "aria-label": strings.verify.fieldLabel, placeholder: strings.verify.placeholder,
75
+ .join(' '), type: "tel", inputMode: "tel", "aria-label": strings.verify.fieldLabel, placeholder: emptyError ? undefined : strings.verify.placeholder,
63
76
  /* Uncontrolled: a screen is one static spec state, and `value`
64
77
  without `onChange` would either warn or force `readOnly`, whose
65
78
  disabled surface is exactly the grey Figma does NOT draw here. */
66
- defaultValue: value ?? '', disabled: busy,
79
+ defaultValue: emptyError ? '' : value ?? '', disabled: busy, error: emptyError, errorText: emptyError ? strings.requiredError : undefined,
67
80
  /* Figma draws the clear ✕ only while the field is focused. */
68
81
  clearable: focused })] }) }));
69
82
  });
@@ -62,6 +62,7 @@ export const QualifiedSignatureAcceptScreen = forwardRef(({ strings, variant = '
62
62
  // `loading` makes the whole form inert.
63
63
  const busy = state === 'loading';
64
64
  const ticked = state !== 'default';
65
+ const requiredHint = strings.accept.requiredHint;
65
66
  return (_jsxs(PrizmaFormScreen, { ref: ref, className: ['qualified-signature-accept', className].filter(Boolean).join(' '), title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel, ctaDisabled: !ticked, ctaLoading: busy, ctaLoadingLabel: copy.ctaLoadingLabel,
66
67
  /* The clause stack pans inside the column — the archetype owns this now
67
68
  (`--scroll`), so the module declares no overflow of its own. #QS-1 */
@@ -80,7 +81,11 @@ export const QualifiedSignatureAcceptScreen = forwardRef(({ strings, variant = '
80
81
  action outright while the partner's `loading` frame greys it. Two
81
82
  Figma readings of one inert state; Core models inert as disabled,
82
83
  so both take the greyed treatment and the removal is logged. */
83
- _jsx(PrizmaCardDocumentContainer, { filename: filename, actionLabel: strings.accept.viewLabel, actionDisabled: busy, onAction: () => onAction?.({ type: 'tertiary', screenId }) })) : null, _jsx("div", { className: "qualified-signature-terms", role: "group", "aria-label": strings.accept.termsLabel, children: terms.map((term) => (_jsx(PrizmaCheckbox, { size: "L", name: term.id, value: term.id, checked: ticked, disabled: busy, segments: term.segments }, term.id))) })] }));
84
+ _jsx(PrizmaCardDocumentContainer, { filename: filename, actionLabel: strings.accept.viewLabel, actionDisabled: busy, onAction: () => onAction?.({ type: 'tertiary', screenId }) })) : null, _jsxs("div", { className: "qualified-signature-terms", role: "group", "aria-label": strings.accept.termsLabel, children: [requiredHint && strings.accept.requiredLegend ? (_jsx("p", { className: "qualified-signature-terms__legend", children: strings.accept.requiredLegend })) : null, terms.map((term) => (_jsx(PrizmaCheckbox, { size: "L", name: term.id, value: term.id, checked: ticked, disabled: busy, required: Boolean(requiredHint),
85
+ /* The `*` rides the END of the clause copy, the Data Sharing
86
+ Consent idiom — the L card paints no marker of its own beside
87
+ the box (owner, 2026-09-02). */
88
+ segments: requiredHint ? [...term.segments, ` ${requiredHint}`] : term.segments }, term.id)))] })] }));
84
89
  });
85
90
  QualifiedSignatureAcceptScreen.displayName = 'QualifiedSignatureAcceptScreen';
86
91
  export default QualifiedSignatureAcceptScreen;
@@ -40,11 +40,13 @@ export interface QualifiedSignatureDocumentScreenProps extends Omit<PrizmaListSc
40
40
  * overflow constraint, declaring no spacing or size, the same idiom as CURP's
41
41
  * taller-than-844 form (DEVIATIONS #CURP-1).
42
42
  *
43
- * THREE deltas from the frames, all reported rather than worked around:
44
- * 1. Both sections draw a back chevron at the nav's left (and `terms` replaces
45
- * the logo row with a chevron + heading bar). GUIDELINES §3 stands: SDK
46
- * screens carry no dismiss or back affordance — the standing ruling of
47
- * DEVIATIONS #PI-9, which is not re-litigated per module.
43
+ * THREE deltas from the frames:
44
+ * 1. Both sections draw a back chevron at the nav's left. It is BUILT (owner
45
+ * call 2026-09-29): these are viewers the user opened from the accept
46
+ * screen, so they need a way back. It is Core's chevron through the opt-in
47
+ * `PrizmaScreenNav.backLabel`, emitting `back`; the logo stays centred.
48
+ * `terms` replacing the logo row with a chevron + heading bar is still not
49
+ * followed — the title stays the archetype's H2 below the nav.
48
50
  * 2. `viewdoc` draws NO heading. GUIDELINES §4 is explicit that every screen has
49
51
  * exactly one H2, and archetype G has no title-less path, so the screen
50
52
  * carries one and the Figma frame is the side to change. (An archetype
@@ -53,10 +55,12 @@ export interface QualifiedSignatureDocumentScreenProps extends Omit<PrizmaListSc
53
55
  * for attribution chrome (#PI-8, #F-30, #F-34), so both variants keep the
54
56
  * header and the footer and nothing shifts between states.
55
57
  *
56
- * `terms` is LEFT-aligned, as Figma draws it: a centred heading over a
57
- * left-aligned wall of legal text reads as a mistake. The alignment rides the
58
- * sanctioned `overrides.align` path (types.ts) — nothing is re-styled here — and
59
- * an explicit host override still wins. The #GEO-2 precedent.
58
+ * Both variants frame the page in the box Electronic Signature's review screen
59
+ * uses, so the title is CENTRED on both, like that screen (owner call
60
+ * 2026-09-29, "take reference from other screens"). Figma's `QSign.T&C` draws
61
+ * the title left over an unframed wall of text; once the text sits in its own
62
+ * left-aligned frame the centred title no longer reads as a mistake. An
63
+ * explicit `overrides.align` from the host still wins.
60
64
  */
61
65
  export declare const QualifiedSignatureDocumentScreen: React.ForwardRefExoticComponent<QualifiedSignatureDocumentScreenProps & React.RefAttributes<HTMLDivElement>>;
62
66
  export default QualifiedSignatureDocumentScreen;
@@ -24,11 +24,13 @@ import './QualifiedSignatureLayout.css';
24
24
  * overflow constraint, declaring no spacing or size, the same idiom as CURP's
25
25
  * taller-than-844 form (DEVIATIONS #CURP-1).
26
26
  *
27
- * THREE deltas from the frames, all reported rather than worked around:
28
- * 1. Both sections draw a back chevron at the nav's left (and `terms` replaces
29
- * the logo row with a chevron + heading bar). GUIDELINES §3 stands: SDK
30
- * screens carry no dismiss or back affordance — the standing ruling of
31
- * DEVIATIONS #PI-9, which is not re-litigated per module.
27
+ * THREE deltas from the frames:
28
+ * 1. Both sections draw a back chevron at the nav's left. It is BUILT (owner
29
+ * call 2026-09-29): these are viewers the user opened from the accept
30
+ * screen, so they need a way back. It is Core's chevron through the opt-in
31
+ * `PrizmaScreenNav.backLabel`, emitting `back`; the logo stays centred.
32
+ * `terms` replacing the logo row with a chevron + heading bar is still not
33
+ * followed — the title stays the archetype's H2 below the nav.
32
34
  * 2. `viewdoc` draws NO heading. GUIDELINES §4 is explicit that every screen has
33
35
  * exactly one H2, and archetype G has no title-less path, so the screen
34
36
  * carries one and the Figma frame is the side to change. (An archetype
@@ -37,20 +39,20 @@ import './QualifiedSignatureLayout.css';
37
39
  * for attribution chrome (#PI-8, #F-30, #F-34), so both variants keep the
38
40
  * header and the footer and nothing shifts between states.
39
41
  *
40
- * `terms` is LEFT-aligned, as Figma draws it: a centred heading over a
41
- * left-aligned wall of legal text reads as a mistake. The alignment rides the
42
- * sanctioned `overrides.align` path (types.ts) — nothing is re-styled here — and
43
- * an explicit host override still wins. The #GEO-2 precedent.
42
+ * Both variants frame the page in the box Electronic Signature's review screen
43
+ * uses, so the title is CENTRED on both, like that screen (owner call
44
+ * 2026-09-29, "take reference from other screens"). Figma's `QSign.T&C` draws
45
+ * the title left over an unframed wall of text; once the text sits in its own
46
+ * left-aligned frame the centred title no longer reads as a mistake. An
47
+ * explicit `overrides.align` from the host still wins.
44
48
  */
45
49
  export const QualifiedSignatureDocumentScreen = forwardRef(({ strings, variant = 'document', body, screenId = `qualified-signature-document-${variant}`, overrides, onAction, ...shell }, ref) => {
46
50
  const copy = variant === 'terms' ? strings.document.terms : strings.document.document;
47
- // The legal wall is left-aligned in Figma; the contract page keeps the
48
- // archetype's centred default. An explicit `overrides.align` still wins.
49
- const alignedOverrides = {
50
- ...overrides,
51
- align: overrides?.align ?? (variant === 'terms' ? 'left' : 'centered'),
52
- };
53
- return (_jsx(PrizmaListScreen, { ref: ref, title: copy.title, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: alignedOverrides, onAction: onAction, ...shell, children: _jsx("div", { className: "qualified-signature-document", children: body }) }));
51
+ return (_jsx(PrizmaListScreen, { ref: ref, title: copy.title, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, backLabel: strings.document.backLabel, screenId: screenId, overrides: overrides, onAction: onAction, ...shell, children: _jsx("div", { className: [
52
+ 'qualified-signature-document',
53
+ 'qualified-signature-document--framed',
54
+ `qualified-signature-document--${variant}`,
55
+ ].join(' '), role: "region", "aria-label": copy.title, tabIndex: 0, children: body }) }));
54
56
  });
55
57
  QualifiedSignatureDocumentScreen.displayName = 'QualifiedSignatureDocumentScreen';
56
58
  export default QualifiedSignatureDocumentScreen;
@@ -57,6 +57,26 @@
57
57
  display: flex;
58
58
  flex-direction: column;
59
59
  gap: var(--prizma-spacing-4);
60
+ /* The clauses sit at the BOTTOM of the column, on the primary button, not
61
+ * under the document row (Elena, 2026-09-30). The fields are a flex column,
62
+ * so the auto margin takes the free space; when the clauses outgrow the frame
63
+ * (the five-clause reading) there is none and the stack scrolls as before. */
64
+ margin-top: auto;
65
+ }
66
+
67
+ /* The required legend leading the group. Core ships no caption block outside a
68
+ * field, so the line binds the clause copy's own Size-S tokens in the secondary
69
+ * ink — no new size, no new colour. Spacing/8 more below it than the clauses
70
+ * keep between themselves, so it reads as the group's header. */
71
+ .qualified-signature-terms__legend {
72
+ margin: 0 0 var(--prizma-spacing-8);
73
+ font-family: var(--prizma-checkbox-radio-family);
74
+ font-weight: var(--prizma-checkbox-radio-s-font-weight);
75
+ font-size: var(--prizma-checkbox-radio-s-font-size);
76
+ line-height: var(--prizma-checkbox-radio-s-line-height);
77
+ letter-spacing: var(--prizma-checkbox-radio-s-letter-spacing);
78
+ color: var(--prizma-text-body-500-secondary);
79
+ text-align: left;
60
80
  }
61
81
 
62
82
  /* ── (c) Uploaded-file preview card ──────────────────────────────────────────
@@ -181,6 +201,62 @@
181
201
  overscroll-behavior: contain;
182
202
  }
183
203
 
204
+ /* The document FRAME — the two full-surface document screens draw the page
205
+ * inside the same box Electronic Signature's review screen uses
206
+ * (`.electronic-signature-document`): `Border/Neutral/100` hairline,
207
+ * `Border/Radius/small`, `Surface/Neutral/0`, clipped so the page scrolls INSIDE
208
+ * it. Without it the contract image floated loose on the screen and the legal
209
+ * text ran edge to edge as one grey wall (owner-reported 2026-09-29: "take
210
+ * reference from other screens"). A modifier, not the base class, because the
211
+ * gated-form exploration reuses `.qualified-signature-document` inside its own
212
+ * popup, which already carries a frame.
213
+ *
214
+ * Spacing/16 bottom margin: the gap Electronic Signature's frame keeps above the
215
+ * CTA band, here kept above the anchored tag so the frame never touches it.
216
+ */
217
+ .qualified-signature-document--framed {
218
+ box-sizing: border-box;
219
+ background-color: var(--prizma-surface-neutral-0);
220
+ border: var(--prizma-border-width-regular) solid var(--prizma-border-neutral-100);
221
+ border-radius: var(--prizma-border-radius-small);
222
+ margin-bottom: var(--prizma-spacing-16);
223
+ }
224
+
225
+ /* `terms` — a TEXT agreement needs a gutter inside the frame (Spacing/16) and
226
+ * the paragraph rhythm Electronic Signature uses (Spacing/12 between blocks). */
227
+ .qualified-signature-document--terms {
228
+ padding: var(--prizma-spacing-16);
229
+ text-align: start;
230
+ }
231
+
232
+ .qualified-signature-document--terms > * {
233
+ margin: 0;
234
+ }
235
+
236
+ .qualified-signature-document.qualified-signature-document--terms > * + * {
237
+ /* Doubled class: `__prose` resets `margin: 0` later in this file at the
238
+ same specificity, which would otherwise swallow the rhythm. */
239
+ margin-top: var(--prizma-spacing-12);
240
+ }
241
+
242
+ /* Section headings inside the form — Headline/H5 on Text/Body/800, the same
243
+ * pair as `.electronic-signature-document h4`. */
244
+ .qualified-signature-document--terms h4 {
245
+ font-family: var(--prizma-headline-family);
246
+ font-weight: var(--prizma-headline-h5-font-weight);
247
+ font-size: var(--prizma-headline-h5-font-size);
248
+ line-height: var(--prizma-headline-h5-line-height);
249
+ letter-spacing: var(--prizma-headline-h5-letter-spacing);
250
+ color: var(--prizma-text-body-800-primary);
251
+ }
252
+
253
+ /* Inside the frame the prose drops to Text/Body/500, the secondary ink every
254
+ * block of Core prose uses (`CardList.css`, `Card.css`) — the heading carries
255
+ * the hierarchy, as in Electronic Signature. */
256
+ .qualified-signature-document--terms .qualified-signature-document__prose {
257
+ color: var(--prizma-text-body-500-secondary);
258
+ }
259
+
184
260
  /* The contract page is host-supplied media (an <img> / <canvas> of the rendered
185
261
  * PDF, GUIDELINES §10's `cameraSurface` idiom) — it fills the column width and
186
262
  * keeps its own aspect. */
@@ -128,6 +128,12 @@ export interface QualifiedSignatureStrings {
128
128
  viewLabel: string;
129
129
  /** Accessible name for the consent stack. */
130
130
  termsLabel?: string;
131
+ /** Suffix appended to every clause, all of them required ("*"). Localizable,
132
+ * never baked into the clause copy — Data Sharing Consent's `requiredHint`. */
133
+ requiredHint?: string;
134
+ /** The line that leads the clause group and explains the marker ("All terms
135
+ * marked with * are required."). Only drawn alongside `requiredHint`. */
136
+ requiredLegend?: string;
131
137
  /** The generic five-clause reading. */
132
138
  accept: QualifiedSignatureAcceptCopy;
133
139
  /** The partner two-clause reading. */
@@ -148,6 +154,10 @@ export interface QualifiedSignatureStrings {
148
154
  terms: {
149
155
  title: string;
150
156
  };
157
+ /** Accessible name of the nav's back chevron on both document screens
158
+ * ("Back") — they are opened from the accept screen, so they carry a way
159
+ * back to it. Emits `back`. Omit to render the logo-only nav. */
160
+ backLabel?: string;
151
161
  };
152
162
  /** The two in-flight screens. */
153
163
  processing: {