@usefillo/react 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,8 @@ npm i @usefillo/react
24
24
 
25
25
  `react` and `react-dom` (18 or 19) are peer dependencies.
26
26
 
27
+ The default stylesheet inherits the host page's CSS `color-scheme` and font. If your theme provider only toggles a class or data attribute, pass its resolved mode as `theme={{ colorScheme: "light" | "dark" }}`. Use `"auto"` only for a deliberately system-driven page. A fixed hex `background` automatically selects a readable palette unless you explicitly choose one.
28
+
27
29
  ## Write forms as components
28
30
 
29
31
  ```tsx
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentType, ReactNode, ReactElement } from 'react';
3
3
  import * as _usefillo_core from '@usefillo/core';
4
- import { FormSchema, FilloClient, ResponseData, FieldValue, FormPage, Block, FormStatus, FieldKind, Field, CustomField, FormTheme, FilloAppearance, FilloRendererStrings, FilloRespondent, ChallengeConfig, ChallengeTheme, FilloError, CodeForm, FormSettings, Condition, TextField, PhoneField, NumberField, ChoiceField, SelectOption, CheckboxField, RatingField, LinearScaleField, RankingField, SignatureField, DateField, FileUploadField as FileUploadField$1, HiddenField, CalculatedField } from '@usefillo/core';
4
+ import { FormSchema, FilloClient, ResponseData, FieldValue, FormPage, Block, FormStatus, FieldKind, Field, CustomField, FormTheme, FilloAppearance, FilloRendererStrings, FilloRespondent, ChallengeConfig, ChallengeTheme, FilloError, CodeForm, FormSettings, Condition, TextField, PhoneField, NumberField, ChoiceField, SelectOption, CheckboxField, RatingField, LinearScaleField, RankingField, SignatureField, DateField, FileUploadField as FileUploadField$1, HiddenField, CalculatedField, RespondentErrorStrings } from '@usefillo/core';
5
5
  export { CodeForm, Field, FieldValue, FileValue, FilloAppearance, FilloClient, FilloError, FilloJsxError, FilloRendererStrings, FilloSlot, FilloStrings, FormSchema, FormStatus, FormTheme, ProvisionWorkspaceResult, PublishedForm, ResponseData, SlotState, createClient, defineForm, provisionWorkspace, when } from '@usefillo/core';
6
6
 
7
7
  /**
@@ -270,7 +270,7 @@ type ContentProps = {
270
270
  /** Inert: rendering one throws; they exist to be read by the compiler. */
271
271
  type Inert<P> = (props: P) => never;
272
272
  interface FilloJsxFormBaseProps extends Omit<FilloFormProps, "form" | "formId" | "client" | "renderOnly" | "skipValidation"> {
273
- /** Workspace handle — the form's identity across syncs. */
273
+ /** Project handle — the form's identity across syncs. */
274
274
  id: string;
275
275
  title?: string;
276
276
  description?: string;
@@ -366,6 +366,8 @@ interface ControllerOptions {
366
366
  * environment) so integration details never reach production visitors.
367
367
  */
368
368
  verboseResolutionErrors?: boolean;
369
+ /** Resolved renderer fallbacks for localized respondent-safe failures. */
370
+ respondentErrorStrings?: Partial<RespondentErrorStrings>;
369
371
  /**
370
372
  * Host-app account context (identify()): who is filling this form, by your
371
373
  * own user id. Recorded with the response as an unverified claim so the
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  normalizeFormTheme,
23
23
  reachableFields,
24
24
  reachablePageSequence,
25
+ respondentErrorStringsFor,
25
26
  resolveThemeAppearance
26
27
  } from "@usefillo/core";
27
28
  import { resolveStrings, slotClass as slotClass3 } from "@usefillo/core";
@@ -104,26 +105,35 @@ function DevNoClientNotice() {
104
105
  " to collect them in Fillo, or forward them with webhooks. (This notice only appears in development and preview.)"
105
106
  ] });
106
107
  }
107
- function DevDraftNotice({ warningUrl }) {
108
- const safeUrl = safeHttpUrl(warningUrl);
108
+ function DevDraftNotice({ warningUrl, formUrl }) {
109
+ const storageUrl = safeHttpUrl(warningUrl);
110
+ const publishUrl = safeHttpUrl(formUrl);
109
111
  return /* @__PURE__ */ jsxs("div", { className: "fillo-devwarning", role: "alert", children: [
110
- /* @__PURE__ */ jsx("strong", { children: "Draft form." }),
111
- " It renders here, but it won\u2019t accept responses until you publish it in your Fillo dashboard.",
112
+ /* @__PURE__ */ jsx("strong", { children: "Draft form preview." }),
113
+ " The form renders for local testing, but Fillo will reject the submission and save no response until you publish it.",
112
114
  " ",
113
- safeUrl && /* @__PURE__ */ jsxs(Fragment, { children: [
114
- "Connect storage to publish:",
115
- " ",
116
- /* @__PURE__ */ jsx("a", { href: safeUrl, target: "_blank", rel: "noopener noreferrer", children: safeUrl }),
115
+ storageUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [
116
+ /* @__PURE__ */ jsx("a", { href: storageUrl, target: "_blank", rel: "noopener noreferrer", children: "Connect storage to publish" }),
117
117
  ".",
118
118
  " "
119
- ] }),
119
+ ] }) : publishUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [
120
+ /* @__PURE__ */ jsx("a", { href: publishUrl, target: "_blank", rel: "noopener noreferrer", children: "Open in Fillo to publish" }),
121
+ ".",
122
+ " "
123
+ ] }) : null,
120
124
  "(This notice only appears in development and preview.)"
121
125
  ] });
122
126
  }
123
- function DevStagedNotice() {
127
+ function DevStagedNotice({ formUrl }) {
128
+ const publishUrl = safeHttpUrl(formUrl);
124
129
  return /* @__PURE__ */ jsxs("div", { className: "fillo-devwarning", role: "alert", children: [
125
- /* @__PURE__ */ jsx("strong", { children: "Code changes are staged." }),
126
- " This local preview is not the live form yet. Publish the changes in Fillo before deploying them for respondents."
130
+ /* @__PURE__ */ jsx("strong", { children: "Code changes are staged, not live." }),
131
+ " This page shows your draft, while respondents still get the live version.",
132
+ " ",
133
+ publishUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [
134
+ /* @__PURE__ */ jsx("a", { href: publishUrl, target: "_blank", rel: "noopener noreferrer", children: "Review and publish in Fillo" }),
135
+ "."
136
+ ] }) : /* @__PURE__ */ jsx(Fragment, { children: "Review and publish the changes in Fillo." })
127
137
  ] });
128
138
  }
129
139
  function DevSyncErrorNotice({ error }) {
@@ -139,7 +149,7 @@ function DevSyncErrorNotice({ error }) {
139
149
  ] });
140
150
  }
141
151
  function DevChrome(props) {
142
- const notice = props.devNotices === false ? null : props.syncError ? /* @__PURE__ */ jsx(DevSyncErrorNotice, { error: props.syncError }) : props.staged ? /* @__PURE__ */ jsx(DevStagedNotice, {}) : props.draft ? /* @__PURE__ */ jsx(DevDraftNotice, { warningUrl: props.warningUrl }) : props.noClient ? /* @__PURE__ */ jsx(DevNoClientNotice, {}) : null;
152
+ const notice = props.devNotices === false ? null : props.syncError ? /* @__PURE__ */ jsx(DevSyncErrorNotice, { error: props.syncError }) : props.staged ? /* @__PURE__ */ jsx(DevStagedNotice, { formUrl: props.formUrl }) : props.draft ? /* @__PURE__ */ jsx(DevDraftNotice, { warningUrl: props.warningUrl, formUrl: props.formUrl }) : props.noClient ? /* @__PURE__ */ jsx(DevNoClientNotice, {}) : null;
143
153
  if (!props.preview && !notice) return null;
144
154
  return /* @__PURE__ */ jsxs(Fragment, { children: [
145
155
  props.preview && /* @__PURE__ */ jsx(PreviewBadge, {}),
@@ -210,6 +220,24 @@ import {
210
220
  function useFilloController(options) {
211
221
  const optsRef = useRef(options);
212
222
  optsRef.current = options;
223
+ const errorCopy = options.respondentErrorStrings;
224
+ const respondentErrorStrings = useMemo2(
225
+ () => ({ ...errorCopy }),
226
+ [
227
+ errorCopy?.submitFailed,
228
+ errorCopy?.loadFailedNotFound,
229
+ errorCopy?.loadFailedNetwork,
230
+ errorCopy?.formUnavailable,
231
+ errorCopy?.formClosed,
232
+ errorCopy?.submitRateLimited,
233
+ errorCopy?.respondentUnrecognized,
234
+ errorCopy?.fileUnavailable,
235
+ errorCopy?.scopeMissing,
236
+ errorCopy?.challengeIncomplete,
237
+ errorCopy?.challengeRetry,
238
+ errorCopy?.reviewAnswers
239
+ ]
240
+ );
213
241
  const storeRef = useRef(null);
214
242
  if (storeRef.current === null) {
215
243
  storeRef.current = createFormController({
@@ -230,6 +258,7 @@ function useFilloController(options) {
230
258
  get verboseResolutionErrors() {
231
259
  return optsRef.current.verboseResolutionErrors;
232
260
  },
261
+ respondentErrorStrings,
233
262
  respondent: options.respondent,
234
263
  // Challenge wiring read through optsRef so the engine (created once) sees
235
264
  // the latest token/reset each submit — the widget solves after mount.
@@ -244,9 +273,17 @@ function useFilloController(options) {
244
273
  form: options.form,
245
274
  formId: options.formId,
246
275
  client: options.client,
247
- respondent: options.respondent
276
+ respondent: options.respondent,
277
+ respondentErrorStrings
248
278
  });
249
- }, [store, options.form, options.formId, options.client, options.respondent]);
279
+ }, [
280
+ store,
281
+ options.form,
282
+ options.formId,
283
+ options.client,
284
+ options.respondent,
285
+ respondentErrorStrings
286
+ ]);
250
287
  useEffect(() => () => store.destroy(), [store]);
251
288
  const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
252
289
  return useMemo2(
@@ -329,6 +366,7 @@ function useCodeFormSync(form, client, onError, options) {
329
366
  settled: false,
330
367
  formId: null,
331
368
  status: null,
369
+ manageUrl: void 0,
332
370
  error: null,
333
371
  noticeError: null,
334
372
  staged: false,
@@ -390,6 +428,7 @@ function useCodeFormSync(form, client, onError, options) {
390
428
  settled: true,
391
429
  formId: r.formId,
392
430
  status: r.status ?? null,
431
+ manageUrl: r.manageUrl,
393
432
  branding: r.branding,
394
433
  challenge: r.challenge,
395
434
  error: null,
@@ -405,7 +444,7 @@ function useCodeFormSync(form, client, onError, options) {
405
444
  warningCode: r.warningCode,
406
445
  warningUrl: r.warningUrl
407
446
  });
408
- const dashboardUrl = `${client.baseUrl || "https://fillo.so"}/forms/${r.formId}`;
447
+ const dashboardUrl = r.manageUrl;
409
448
  if (r.warning) {
410
449
  console.warn(
411
450
  `[fillo] ${r.warning}${r.warningUrl ? ` Connect storage to publish: ${r.warningUrl}` : ""}`
@@ -421,10 +460,14 @@ function useCodeFormSync(form, client, onError, options) {
421
460
  noticeSync(
422
461
  codeForm.id,
423
462
  "staged",
424
- `[fillo] "${codeForm.id}": changes staged as a draft \u2014 the live version remains rendered until you publish them at ${dashboardUrl}`
463
+ `[fillo] "${codeForm.id}": changes staged as a draft \u2014 the live version remains rendered until you publish them${dashboardUrl ? ` at ${dashboardUrl}` : " in Fillo"}`
425
464
  );
426
465
  } else if (r.status === "draft") {
427
- noticeSync(codeForm.id, "draft", `[fillo] "${codeForm.id}" is a draft \u2014 it renders here but won't accept responses until published: ${dashboardUrl}`);
466
+ noticeSync(
467
+ codeForm.id,
468
+ "draft",
469
+ `[fillo] "${codeForm.id}" is a draft \u2014 it renders here but won't accept responses until published${dashboardUrl ? `: ${dashboardUrl}` : " in Fillo"}`
470
+ );
428
471
  }
429
472
  }).catch((err) => {
430
473
  if (cancelled) return;
@@ -451,6 +494,7 @@ function useCodeFormSync(form, client, onError, options) {
451
494
  return synced.key === syncKey && synced.client === client ? {
452
495
  formId: synced.formId,
453
496
  status: synced.status,
497
+ manageUrl: synced.manageUrl,
454
498
  branding: synced.branding,
455
499
  challenge: synced.challenge,
456
500
  error: synced.error,
@@ -1167,8 +1211,9 @@ function formatBytes(bytes) {
1167
1211
  function uploadFailureMessage(error, strings) {
1168
1212
  if (error instanceof Error && error.name === "FilloError") {
1169
1213
  const status = error.status;
1170
- if (status !== void 0 && status >= 500) return strings.uploadUnavailable;
1171
- return error.message || strings.uploadFailed;
1214
+ if (status === 0 || status !== void 0 && status >= 500) {
1215
+ return strings.uploadUnavailable;
1216
+ }
1172
1217
  }
1173
1218
  return strings.uploadFailed;
1174
1219
  }
@@ -1323,7 +1368,10 @@ function FileUploadField({
1323
1368
  filesRef.current = next;
1324
1369
  setValue(next);
1325
1370
  } catch (err) {
1326
- if (mountedRef.current) {
1371
+ if (mountedRef.current && !controller.signal.aborted) {
1372
+ if (err instanceof Error && err.name === "FilloError") {
1373
+ chrome?.onError?.(err);
1374
+ }
1327
1375
  const message = uploadFailureMessage(err, strings);
1328
1376
  setInFlight((prev) => prev.map((f) => f.key === key ? { ...f, error: message } : f));
1329
1377
  }
@@ -2825,7 +2873,8 @@ function FilloForm(props) {
2825
2873
  resolvedSchema,
2826
2874
  resolvedTheme,
2827
2875
  warningCode: syncedWarningCode,
2828
- warningUrl: syncedWarningUrl
2876
+ warningUrl: syncedWarningUrl,
2877
+ manageUrl: syncedManageUrl
2829
2878
  } = useCodeFormSync(form, client, onError, {
2830
2879
  // An absent client is intentional render-only use. Builder previews and an
2831
2880
  // explicit canonical id also do not need publishable-key resolution.
@@ -3042,6 +3091,7 @@ function FilloForm(props) {
3042
3091
  draftNotice: notPublished,
3043
3092
  stagedNotice: codeChangesStaged,
3044
3093
  syncErrorNotice: syncError ?? syncNoticeError ?? void 0,
3094
+ syncFormUrl: syncedManageUrl,
3045
3095
  uploadsAvailable: codeForm ? syncedUploadsAvailable : currentLoaded?.uploadsAvailable,
3046
3096
  uploadFileSizeLimitMb: codeForm ? syncedUploadFileSizeLimitMb : currentLoaded?.uploadFileSizeLimitMb ?? props.uploadFileSizeLimitMb,
3047
3097
  syncWarningCode: codeForm ? syncedWarningCode : void 0,
@@ -3201,6 +3251,7 @@ function ResolvedForm(props) {
3201
3251
  ) : void 0,
3202
3252
  [codeForm, client, props.onError, schema]
3203
3253
  );
3254
+ const strings = resolveStrings(props.strings);
3204
3255
  const api = useFilloController({
3205
3256
  form: schema,
3206
3257
  formId: props.formId,
@@ -3217,6 +3268,7 @@ function ResolvedForm(props) {
3217
3268
  // Dev chrome gets the developer-grade resolution failure (message + code)
3218
3269
  // instead of the respondent-safe "unavailable" fallback.
3219
3270
  verboseResolutionErrors: devChrome,
3271
+ respondentErrorStrings: respondentErrorStringsFor(strings),
3220
3272
  respondent: props.respondent,
3221
3273
  challengeRequired,
3222
3274
  getChallengeToken: () => challengeTokenRef.current,
@@ -3226,7 +3278,6 @@ function ResolvedForm(props) {
3226
3278
  }
3227
3279
  });
3228
3280
  useDraftFlush(api, !displayOnly && !renderOnly);
3229
- const strings = resolveStrings(props.strings);
3230
3281
  const announceRef = useRef9(null);
3231
3282
  const announce = useCallback2((text) => {
3232
3283
  const el = announceRef.current;
@@ -3260,9 +3311,18 @@ function ResolvedForm(props) {
3260
3311
  uploadsAvailable,
3261
3312
  uploadFileSizeLimitMb,
3262
3313
  warningCode,
3263
- warningUrl
3314
+ warningUrl,
3315
+ onError: props.onError
3264
3316
  }),
3265
- [devChrome, renderOnly, uploadsAvailable, uploadFileSizeLimitMb, warningCode, warningUrl]
3317
+ [
3318
+ devChrome,
3319
+ renderOnly,
3320
+ uploadsAvailable,
3321
+ uploadFileSizeLimitMb,
3322
+ warningCode,
3323
+ warningUrl,
3324
+ props.onError
3325
+ ]
3266
3326
  );
3267
3327
  const settings = schema.settings;
3268
3328
  const submitted = api.status === "submitted";
@@ -3363,6 +3423,7 @@ function ResolvedForm(props) {
3363
3423
  staged: props.stagedNotice,
3364
3424
  draft: props.draftNotice,
3365
3425
  warningUrl: props.syncWarningUrl,
3426
+ formUrl: props.syncFormUrl,
3366
3427
  noClient: !props.client && !props.skipValidation && !renderOnly
3367
3428
  }
3368
3429
  ),
@@ -3662,6 +3723,7 @@ import {
3662
3723
  formSchemasEqual as formSchemasEqual2,
3663
3724
  isFilloError as isFilloError3,
3664
3725
  normalizeFormSchema as normalizeFormSchema2,
3726
+ respondentErrorStringsFor as respondentErrorStringsFor2,
3665
3727
  resolveStrings as resolveStrings2,
3666
3728
  syncCodeForm as syncCodeForm3
3667
3729
  } from "@usefillo/core";
@@ -3792,6 +3854,7 @@ function FilloProvider({
3792
3854
  } : void 0,
3793
3855
  [activeClient, codeForm, onError, schema]
3794
3856
  );
3857
+ const resolvedStrings = resolveStrings2(strings);
3795
3858
  const api = useFilloController({
3796
3859
  ...options,
3797
3860
  client: activeClient,
@@ -3802,6 +3865,7 @@ function FilloProvider({
3802
3865
  // Dev chrome (preview or dev env) surfaces the developer-grade resolution
3803
3866
  // failure (message + code); an explicit option wins.
3804
3867
  verboseResolutionErrors: options.verboseResolutionErrors ?? devChrome,
3868
+ respondentErrorStrings: respondentErrorStringsFor2(resolvedStrings),
3805
3869
  // Headless = your markup, no Fillo-rendered layout. Free like every embed
3806
3870
  // method; recorded per response so usage stays measurable.
3807
3871
  surface: "headless"
@@ -3818,7 +3882,8 @@ function FilloProvider({
3818
3882
  uploadsAvailable: freshUploadsAvailable,
3819
3883
  uploadFileSizeLimitMb: freshUploadFileSizeLimitMb,
3820
3884
  warningCode: freshWarningCode,
3821
- warningUrl: freshWarningUrl
3885
+ warningUrl: freshWarningUrl,
3886
+ onError
3822
3887
  }),
3823
3888
  [
3824
3889
  devChrome,
@@ -3826,10 +3891,10 @@ function FilloProvider({
3826
3891
  freshUploadsAvailable,
3827
3892
  freshUploadFileSizeLimitMb,
3828
3893
  freshWarningCode,
3829
- freshWarningUrl
3894
+ freshWarningUrl,
3895
+ onError
3830
3896
  ]
3831
3897
  );
3832
- const resolvedStrings = resolveStrings2(strings);
3833
3898
  if (targetError) {
3834
3899
  if (renderError) return /* @__PURE__ */ jsx10(Fragment4, { children: renderError(targetError) });
3835
3900
  throw targetError;
package/dist/styles.css CHANGED
@@ -20,20 +20,36 @@
20
20
 
21
21
  @layer components {
22
22
  .fillo-form {
23
- color-scheme: light;
24
23
  --fillo-primary: #18181b;
24
+ --fillo-primary: light-dark(#18181b, #fafafa);
25
25
  --fillo-primary-contrast: #ffffff;
26
+ --fillo-primary-contrast: light-dark(#ffffff, #18181b);
26
27
  --fillo-bg: transparent;
27
28
  --fillo-text: #18181b;
29
+ --fillo-text: light-dark(#18181b, #f4f4f5);
28
30
  --fillo-muted: #71717a;
31
+ --fillo-muted: light-dark(#71717a, #a1a1aa);
29
32
  --fillo-border: #e4e4e7;
33
+ --fillo-border: light-dark(#e4e4e7, #3f3f46);
30
34
  --fillo-error: #dc2626;
35
+ --fillo-error: light-dark(#dc2626, #f87171);
31
36
  --fillo-radius: 10px;
32
37
  --fillo-font: inherit;
38
+ /* The form's local type root. Default = inherit the host's font, so the
39
+ form blends in; set it once to pin or scale the WHOLE form — every
40
+ internal size is em-based and proportional to this. Prefer an ABSOLUTE
41
+ value (px): the not-open state nests a preview .fillo-form inside the
42
+ root .fillo-form, and a relative value would compound there. */
43
+ --fillo-font-size: 1em;
33
44
  --fillo-control-bg: #ffffff;
45
+ --fillo-control-bg: light-dark(#ffffff, #18181b);
34
46
 
35
- color-scheme: light;
47
+ /* Unthemed forms follow the host's CSS color-scheme. The first value for
48
+ each token above is a safe light fallback for browsers without
49
+ light-dark(); explicit light/dark and auto remain available below. */
50
+ color-scheme: inherit;
36
51
  font-family: var(--fillo-font);
52
+ font-size: var(--fillo-font-size);
37
53
  color: var(--fillo-text);
38
54
  background: var(--fillo-bg);
39
55
  display: flex;
@@ -46,8 +62,11 @@
46
62
  color-scheme: light;
47
63
  }
48
64
 
65
+ .fillo-form[data-fillo-color-scheme="auto"] {
66
+ color-scheme: light dark;
67
+ }
68
+
49
69
  .fillo-form[data-fillo-color-scheme="dark"] {
50
- color-scheme: dark;
51
70
  --fillo-primary: #fafafa;
52
71
  --fillo-primary-contrast: #18181b;
53
72
  --fillo-text: #f4f4f5;
@@ -60,9 +79,8 @@
60
79
  }
61
80
 
62
81
  @media (prefers-color-scheme: dark) {
63
- .fillo-form:not([data-fillo-color-scheme="light"]):not([data-fillo-has-color-theme]) {
64
- color-scheme: dark;
65
- --fillo-primary: #fafafa;
82
+ .fillo-form[data-fillo-color-scheme="auto"] {
83
+ --fillo-primary: #fafafa;
66
84
  --fillo-primary-contrast: #18181b;
67
85
  --fillo-text: #f4f4f5;
68
86
  --fillo-muted: #a1a1aa;
@@ -75,22 +93,22 @@
75
93
  }
76
94
 
77
95
  .fillo-header { display: flex; flex-direction: column; gap: 0.5rem; }
78
- .fillo-title { font-size: 1.6rem; font-weight: 650; letter-spacing: 0; margin: 0; }
96
+ .fillo-title { font-size: 1.6em; font-weight: 650; letter-spacing: 0; margin: 0; }
79
97
  .fillo-form-description { color: var(--fillo-muted); margin: 0; line-height: 1.55; }
80
- .fillo-page-title { font-size: 1.2rem; font-weight: 600; margin: 0; }
98
+ .fillo-page-title { font-size: 1.2em; font-weight: 600; margin: 0; }
81
99
 
82
100
  .fillo-blocks { display: flex; flex-direction: column; gap: 1.5rem; }
83
101
 
84
102
  /* ---------- Field chrome ---------- */
85
103
  .fillo-field { display: flex; flex-direction: column; gap: 0.45rem; }
86
- .fillo-label { font-weight: 550; font-size: 0.95rem; }
104
+ .fillo-label { font-weight: 550; font-size: 0.95em; }
87
105
  :where(.fillo-field--error) .fillo-label { color: var(--fillo-error); }
88
106
  /* Fillo marks the few optional fields rather than starring the many required
89
107
  ones — so a clean label reads as required by default. */
90
108
  .fillo-optional { color: var(--fillo-muted); font-weight: 400; font-size: 0.85em; }
91
- .fillo-description { color: var(--fillo-muted); font-size: 0.85rem; margin: 0; line-height: 1.5; }
92
- .fillo-error { color: var(--fillo-error); font-size: 0.85rem; font-weight: 500; line-height: 1.4; margin: 0; }
93
- .fillo-submit-error { color: var(--fillo-error); font-size: 0.85rem; font-weight: 500; margin: 0; }
109
+ .fillo-description { color: var(--fillo-muted); font-size: 0.85em; margin: 0; line-height: 1.5; }
110
+ .fillo-error { color: var(--fillo-error); font-size: 0.85em; font-weight: 500; line-height: 1.4; margin: 0; }
111
+ .fillo-submit-error { color: var(--fillo-error); font-size: 0.85em; font-weight: 500; margin: 0; }
94
112
 
95
113
  /* ---------- Inputs ---------- */
96
114
  .fillo-input {
@@ -294,7 +312,7 @@
294
312
  /* Selected marker for icon-mode options — the non-color signal (the native
295
313
  input is visually hidden there, so forced-colors has nothing else). */
296
314
  .fillo-option-check { flex: 0 0 auto; margin-inline-start: auto; color: var(--fillo-primary); }
297
- .fillo-option-label { font-size: 0.95rem; }
315
+ .fillo-option-label { font-size: 1em; }
298
316
 
299
317
  .fillo-option-main {
300
318
  display: flex;
@@ -396,7 +414,7 @@
396
414
  min-width: 0;
397
415
  margin-left: 0;
398
416
  padding: 0.5rem 0.7rem;
399
- font-size: 0.92rem;
417
+ font-size: 1em;
400
418
  background: color-mix(in srgb, var(--fillo-primary) 3%, var(--fillo-control-bg));
401
419
  }
402
420
  .fillo-other-input--block {
@@ -407,7 +425,7 @@
407
425
  /* ---------- Rating ---------- */
408
426
  .fillo-rating { display: flex; gap: 0.25rem; }
409
427
  .fillo-star {
410
- font-size: 1.75rem;
428
+ font-size: 1.75em;
411
429
  line-height: 1;
412
430
  background: none;
413
431
  border: none;
@@ -458,7 +476,7 @@
458
476
  display: flex;
459
477
  justify-content: space-between;
460
478
  color: var(--fillo-muted);
461
- font-size: 0.78rem;
479
+ font-size: 0.78em;
462
480
  margin-top: 0.35rem;
463
481
  }
464
482
 
@@ -481,11 +499,11 @@
481
499
  border-radius: 999px;
482
500
  background: color-mix(in srgb, var(--fillo-primary) 10%, transparent);
483
501
  color: var(--fillo-primary);
484
- font-size: 0.78rem;
502
+ font-size: 0.78em;
485
503
  font-weight: 650;
486
504
  font-variant-numeric: tabular-nums;
487
505
  }
488
- .fillo-ranking-label { flex: 1; font-size: 0.95rem; }
506
+ .fillo-ranking-label { flex: 1; font-size: 1em; }
489
507
  .fillo-ranking-controls { display: flex; gap: 0.2rem; }
490
508
  .fillo-ranking-move {
491
509
  font: inherit;
@@ -504,13 +522,13 @@
504
522
 
505
523
  /* ---------- Matrix ---------- */
506
524
  .fillo-matrix-wrap { overflow-x: auto; }
507
- .fillo-matrix { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
525
+ .fillo-matrix { border-collapse: collapse; width: 100%; font-size: 0.9em; }
508
526
  :where(.fillo-matrix) th, :where(.fillo-matrix) td {
509
527
  padding: 0.55rem 0.7rem;
510
528
  text-align: center;
511
529
  border-bottom: 1px solid var(--fillo-border);
512
530
  }
513
- .fillo-matrix thead th { color: var(--fillo-muted); font-weight: 500; font-size: 0.8rem; }
531
+ .fillo-matrix thead th { color: var(--fillo-muted); font-weight: 500; font-size: 0.89em; }
514
532
  .fillo-matrix tbody th { text-align: start; font-weight: 500; }
515
533
  .fillo-matrix tbody tr:last-child th, .fillo-matrix tbody tr:last-child td { border-bottom: none; }
516
534
  .fillo-matrix-cell:focus-within {
@@ -524,7 +542,7 @@
524
542
  /* A quiet computed-value line, not a control: no border or input chrome —
525
543
  the slightly heavier tabular number is the only emphasis. */
526
544
  .fillo-calculated-value {
527
- font-size: 1.05rem;
545
+ font-size: 1.05em;
528
546
  font-weight: 600;
529
547
  font-variant-numeric: tabular-nums;
530
548
  color: var(--fillo-text);
@@ -553,7 +571,7 @@
553
571
  display: grid;
554
572
  place-items: center;
555
573
  color: var(--fillo-muted);
556
- font-size: 0.85rem;
574
+ font-size: 0.85em;
557
575
  pointer-events: none;
558
576
  }
559
577
  .fillo-signature-clear {
@@ -561,7 +579,7 @@
561
579
  top: 0.4rem;
562
580
  inset-inline-end: 0.4rem;
563
581
  font: inherit;
564
- font-size: 0.75rem;
582
+ font-size: 0.75em;
565
583
  border: 1px solid var(--fillo-border);
566
584
  border-radius: calc(var(--fillo-radius) * 0.6);
567
585
  background: var(--fillo-control-bg);
@@ -580,7 +598,7 @@
580
598
  }
581
599
  .fillo-signature-type-label {
582
600
  color: var(--fillo-muted);
583
- font-size: 0.8rem;
601
+ font-size: 0.8em;
584
602
  }
585
603
  .fillo-signature-type-input {
586
604
  margin-top: 0;
@@ -620,8 +638,8 @@
620
638
  outline: none;
621
639
  }
622
640
  .fillo-dropzone--disabled { cursor: not-allowed; opacity: 0.6; }
623
- .fillo-dropzone-title { font-weight: 550; font-size: 0.95rem; }
624
- .fillo-dropzone-hint { color: var(--fillo-muted); font-size: 0.82rem; }
641
+ .fillo-dropzone-title { font-weight: 550; font-size: 0.95em; }
642
+ .fillo-dropzone-hint { color: var(--fillo-muted); font-size: 0.82em; }
625
643
 
626
644
  .fillo-files { list-style: none; display: flex; flex-direction: column; gap: 0.45rem; margin: 0; padding: 0; }
627
645
  .fillo-file {
@@ -632,7 +650,7 @@
632
650
  border: 1px solid var(--fillo-border);
633
651
  border-radius: var(--fillo-radius);
634
652
  padding: 0.625rem 0.75rem;
635
- font-size: 0.88rem;
653
+ font-size: 0.88em;
636
654
  background: var(--fillo-control-bg);
637
655
  box-sizing: border-box;
638
656
  min-width: 0;
@@ -692,7 +710,7 @@
692
710
  }
693
711
  .fillo-file-error {
694
712
  color: var(--fillo-error);
695
- font-size: 0.82rem;
713
+ font-size: 0.93em;
696
714
  line-height: 1.35;
697
715
  overflow-wrap: anywhere;
698
716
  }
@@ -747,7 +765,7 @@
747
765
  }
748
766
 
749
767
  /* ---------- Content blocks ---------- */
750
- .fillo-heading { font-size: 1.15rem; font-weight: 600; margin: 0.5rem 0 0; }
768
+ .fillo-heading { font-size: 1.15em; font-weight: 600; margin: 0.5rem 0 0; }
751
769
  .fillo-paragraph { color: var(--fillo-muted); line-height: 1.6; margin: 0; }
752
770
  .fillo-divider { border: none; border-top: 1px solid var(--fillo-border); margin: 0.25rem 0; }
753
771
 
@@ -821,11 +839,11 @@
821
839
  padding: 2rem 1rem;
822
840
  text-align: center;
823
841
  }
824
- .fillo-not-open-title { margin: 0; font-size: 1.05rem; font-weight: 650; letter-spacing: 0; }
842
+ .fillo-not-open-title { margin: 0; font-size: 1.05em; font-weight: 650; letter-spacing: 0; }
825
843
  .fillo-not-open-body {
826
844
  max-width: 26rem;
827
845
  margin: 0;
828
- font-size: 0.875rem;
846
+ font-size: 0.875em;
829
847
  color: var(--fillo-muted);
830
848
  line-height: 1.5;
831
849
  }
@@ -849,8 +867,8 @@
849
867
  border-bottom: 2.5px solid var(--fillo-primary-contrast);
850
868
  transform: translateY(-0.09rem) rotate(-45deg);
851
869
  }
852
- .fillo-success-title { margin: 0; font-size: 1.05rem; letter-spacing: 0; }
853
- .fillo-success-message { margin: 0; font-size: 0.875rem; color: var(--fillo-muted); }
870
+ .fillo-success-title { margin: 0; font-size: 1.05em; letter-spacing: 0; }
871
+ .fillo-success-message { margin: 0; font-size: 0.875em; color: var(--fillo-muted); }
854
872
 
855
873
  /* Honeypot — off-screen, never display:none (bots check that) */
856
874
  .fillo-hp {
@@ -911,7 +929,7 @@
911
929
  display: block;
912
930
  margin-top: 1.25rem;
913
931
  text-align: center;
914
- font-size: 0.75rem;
932
+ font-size: 0.75em;
915
933
  font-weight: 500;
916
934
  letter-spacing: 0;
917
935
  color: var(--fillo-muted);
@@ -929,7 +947,7 @@
929
947
  border-radius: var(--fillo-radius);
930
948
  background: #fffbeb;
931
949
  color: #92400e;
932
- font-size: 0.82rem;
950
+ font-size: 0.82em;
933
951
  line-height: 1.45;
934
952
  }
935
953
  .fillo-devwarning code {
@@ -948,7 +966,7 @@
948
966
  border-radius: 999px;
949
967
  background: #fef3c7;
950
968
  color: #92400e;
951
- font-size: 0.7rem;
969
+ font-size: 0.7em;
952
970
  font-weight: 600;
953
971
  letter-spacing: 0.06em;
954
972
  text-transform: uppercase;
@@ -966,7 +984,7 @@
966
984
  border-radius: var(--fillo-radius);
967
985
  background: var(--fillo-control-bg);
968
986
  color: var(--fillo-muted);
969
- font-size: 0.82rem;
987
+ font-size: 0.82em;
970
988
  line-height: 1.45;
971
989
  }
972
990
  .fillo-resume-clear {
@@ -1006,7 +1024,7 @@
1006
1024
  .fillo-turnstile-error {
1007
1025
  margin: 0;
1008
1026
  color: var(--fillo-error);
1009
- font-size: 0.82rem;
1027
+ font-size: 0.82em;
1010
1028
  line-height: 1.45;
1011
1029
  }
1012
1030
 
@@ -1027,7 +1045,7 @@
1027
1045
  justify-content: space-between;
1028
1046
  gap: 0.5rem;
1029
1047
  }
1030
- .fillo-group-instance-title { margin: 0; font-size: 0.95rem; font-weight: 600; }
1048
+ .fillo-group-instance-title { margin: 0; font-size: 0.95em; font-weight: 600; }
1031
1049
  .fillo-group-add {
1032
1050
  align-self: flex-start;
1033
1051
  font: inherit;
@@ -1040,7 +1058,7 @@
1040
1058
  }
1041
1059
  .fillo-group-add:disabled { opacity: 0.5; cursor: default; }
1042
1060
  .fillo-group-remove {
1043
- font-size: 1.1rem;
1061
+ font-size: 1.1em;
1044
1062
  line-height: 1;
1045
1063
  padding: 0 0.2rem;
1046
1064
  border: none;
@@ -20,20 +20,36 @@
20
20
 
21
21
 
22
22
  .fillo-form {
23
- color-scheme: light;
24
23
  --fillo-primary: #18181b;
24
+ --fillo-primary: light-dark(#18181b, #fafafa);
25
25
  --fillo-primary-contrast: #ffffff;
26
+ --fillo-primary-contrast: light-dark(#ffffff, #18181b);
26
27
  --fillo-bg: transparent;
27
28
  --fillo-text: #18181b;
29
+ --fillo-text: light-dark(#18181b, #f4f4f5);
28
30
  --fillo-muted: #71717a;
31
+ --fillo-muted: light-dark(#71717a, #a1a1aa);
29
32
  --fillo-border: #e4e4e7;
33
+ --fillo-border: light-dark(#e4e4e7, #3f3f46);
30
34
  --fillo-error: #dc2626;
35
+ --fillo-error: light-dark(#dc2626, #f87171);
31
36
  --fillo-radius: 10px;
32
37
  --fillo-font: inherit;
38
+ /* The form's local type root. Default = inherit the host's font, so the
39
+ form blends in; set it once to pin or scale the WHOLE form — every
40
+ internal size is em-based and proportional to this. Prefer an ABSOLUTE
41
+ value (px): the not-open state nests a preview .fillo-form inside the
42
+ root .fillo-form, and a relative value would compound there. */
43
+ --fillo-font-size: 1em;
33
44
  --fillo-control-bg: #ffffff;
45
+ --fillo-control-bg: light-dark(#ffffff, #18181b);
34
46
 
35
- color-scheme: light;
47
+ /* Unthemed forms follow the host's CSS color-scheme. The first value for
48
+ each token above is a safe light fallback for browsers without
49
+ light-dark(); explicit light/dark and auto remain available below. */
50
+ color-scheme: inherit;
36
51
  font-family: var(--fillo-font);
52
+ font-size: var(--fillo-font-size);
37
53
  color: var(--fillo-text);
38
54
  background: var(--fillo-bg);
39
55
  display: flex;
@@ -46,8 +62,11 @@
46
62
  color-scheme: light;
47
63
  }
48
64
 
65
+ .fillo-form[data-fillo-color-scheme="auto"] {
66
+ color-scheme: light dark;
67
+ }
68
+
49
69
  .fillo-form[data-fillo-color-scheme="dark"] {
50
- color-scheme: dark;
51
70
  --fillo-primary: #fafafa;
52
71
  --fillo-primary-contrast: #18181b;
53
72
  --fillo-text: #f4f4f5;
@@ -60,9 +79,8 @@
60
79
  }
61
80
 
62
81
  @media (prefers-color-scheme: dark) {
63
- .fillo-form:not([data-fillo-color-scheme="light"]):not([data-fillo-has-color-theme]) {
64
- color-scheme: dark;
65
- --fillo-primary: #fafafa;
82
+ .fillo-form[data-fillo-color-scheme="auto"] {
83
+ --fillo-primary: #fafafa;
66
84
  --fillo-primary-contrast: #18181b;
67
85
  --fillo-text: #f4f4f5;
68
86
  --fillo-muted: #a1a1aa;
@@ -75,22 +93,22 @@
75
93
  }
76
94
 
77
95
  .fillo-header { display: flex; flex-direction: column; gap: 0.5rem; }
78
- .fillo-title { font-size: 1.6rem; font-weight: 650; letter-spacing: 0; margin: 0; }
96
+ .fillo-title { font-size: 1.6em; font-weight: 650; letter-spacing: 0; margin: 0; }
79
97
  .fillo-form-description { color: var(--fillo-muted); margin: 0; line-height: 1.55; }
80
- .fillo-page-title { font-size: 1.2rem; font-weight: 600; margin: 0; }
98
+ .fillo-page-title { font-size: 1.2em; font-weight: 600; margin: 0; }
81
99
 
82
100
  .fillo-blocks { display: flex; flex-direction: column; gap: 1.5rem; }
83
101
 
84
102
  /* ---------- Field chrome ---------- */
85
103
  .fillo-field { display: flex; flex-direction: column; gap: 0.45rem; }
86
- .fillo-label { font-weight: 550; font-size: 0.95rem; }
104
+ .fillo-label { font-weight: 550; font-size: 0.95em; }
87
105
  :where(.fillo-field--error) .fillo-label { color: var(--fillo-error); }
88
106
  /* Fillo marks the few optional fields rather than starring the many required
89
107
  ones — so a clean label reads as required by default. */
90
108
  .fillo-optional { color: var(--fillo-muted); font-weight: 400; font-size: 0.85em; }
91
- .fillo-description { color: var(--fillo-muted); font-size: 0.85rem; margin: 0; line-height: 1.5; }
92
- .fillo-error { color: var(--fillo-error); font-size: 0.85rem; font-weight: 500; line-height: 1.4; margin: 0; }
93
- .fillo-submit-error { color: var(--fillo-error); font-size: 0.85rem; font-weight: 500; margin: 0; }
109
+ .fillo-description { color: var(--fillo-muted); font-size: 0.85em; margin: 0; line-height: 1.5; }
110
+ .fillo-error { color: var(--fillo-error); font-size: 0.85em; font-weight: 500; line-height: 1.4; margin: 0; }
111
+ .fillo-submit-error { color: var(--fillo-error); font-size: 0.85em; font-weight: 500; margin: 0; }
94
112
 
95
113
  /* ---------- Inputs ---------- */
96
114
  .fillo-input {
@@ -294,7 +312,7 @@
294
312
  /* Selected marker for icon-mode options — the non-color signal (the native
295
313
  input is visually hidden there, so forced-colors has nothing else). */
296
314
  .fillo-option-check { flex: 0 0 auto; margin-inline-start: auto; color: var(--fillo-primary); }
297
- .fillo-option-label { font-size: 0.95rem; }
315
+ .fillo-option-label { font-size: 1em; }
298
316
 
299
317
  .fillo-option-main {
300
318
  display: flex;
@@ -396,7 +414,7 @@
396
414
  min-width: 0;
397
415
  margin-left: 0;
398
416
  padding: 0.5rem 0.7rem;
399
- font-size: 0.92rem;
417
+ font-size: 1em;
400
418
  background: color-mix(in srgb, var(--fillo-primary) 3%, var(--fillo-control-bg));
401
419
  }
402
420
  .fillo-other-input--block {
@@ -407,7 +425,7 @@
407
425
  /* ---------- Rating ---------- */
408
426
  .fillo-rating { display: flex; gap: 0.25rem; }
409
427
  .fillo-star {
410
- font-size: 1.75rem;
428
+ font-size: 1.75em;
411
429
  line-height: 1;
412
430
  background: none;
413
431
  border: none;
@@ -458,7 +476,7 @@
458
476
  display: flex;
459
477
  justify-content: space-between;
460
478
  color: var(--fillo-muted);
461
- font-size: 0.78rem;
479
+ font-size: 0.78em;
462
480
  margin-top: 0.35rem;
463
481
  }
464
482
 
@@ -481,11 +499,11 @@
481
499
  border-radius: 999px;
482
500
  background: color-mix(in srgb, var(--fillo-primary) 10%, transparent);
483
501
  color: var(--fillo-primary);
484
- font-size: 0.78rem;
502
+ font-size: 0.78em;
485
503
  font-weight: 650;
486
504
  font-variant-numeric: tabular-nums;
487
505
  }
488
- .fillo-ranking-label { flex: 1; font-size: 0.95rem; }
506
+ .fillo-ranking-label { flex: 1; font-size: 1em; }
489
507
  .fillo-ranking-controls { display: flex; gap: 0.2rem; }
490
508
  .fillo-ranking-move {
491
509
  font: inherit;
@@ -504,13 +522,13 @@
504
522
 
505
523
  /* ---------- Matrix ---------- */
506
524
  .fillo-matrix-wrap { overflow-x: auto; }
507
- .fillo-matrix { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
525
+ .fillo-matrix { border-collapse: collapse; width: 100%; font-size: 0.9em; }
508
526
  :where(.fillo-matrix) th, :where(.fillo-matrix) td {
509
527
  padding: 0.55rem 0.7rem;
510
528
  text-align: center;
511
529
  border-bottom: 1px solid var(--fillo-border);
512
530
  }
513
- .fillo-matrix thead th { color: var(--fillo-muted); font-weight: 500; font-size: 0.8rem; }
531
+ .fillo-matrix thead th { color: var(--fillo-muted); font-weight: 500; font-size: 0.89em; }
514
532
  .fillo-matrix tbody th { text-align: start; font-weight: 500; }
515
533
  .fillo-matrix tbody tr:last-child th, .fillo-matrix tbody tr:last-child td { border-bottom: none; }
516
534
  .fillo-matrix-cell:focus-within {
@@ -524,7 +542,7 @@
524
542
  /* A quiet computed-value line, not a control: no border or input chrome —
525
543
  the slightly heavier tabular number is the only emphasis. */
526
544
  .fillo-calculated-value {
527
- font-size: 1.05rem;
545
+ font-size: 1.05em;
528
546
  font-weight: 600;
529
547
  font-variant-numeric: tabular-nums;
530
548
  color: var(--fillo-text);
@@ -553,7 +571,7 @@
553
571
  display: grid;
554
572
  place-items: center;
555
573
  color: var(--fillo-muted);
556
- font-size: 0.85rem;
574
+ font-size: 0.85em;
557
575
  pointer-events: none;
558
576
  }
559
577
  .fillo-signature-clear {
@@ -561,7 +579,7 @@
561
579
  top: 0.4rem;
562
580
  inset-inline-end: 0.4rem;
563
581
  font: inherit;
564
- font-size: 0.75rem;
582
+ font-size: 0.75em;
565
583
  border: 1px solid var(--fillo-border);
566
584
  border-radius: calc(var(--fillo-radius) * 0.6);
567
585
  background: var(--fillo-control-bg);
@@ -580,7 +598,7 @@
580
598
  }
581
599
  .fillo-signature-type-label {
582
600
  color: var(--fillo-muted);
583
- font-size: 0.8rem;
601
+ font-size: 0.8em;
584
602
  }
585
603
  .fillo-signature-type-input {
586
604
  margin-top: 0;
@@ -620,8 +638,8 @@
620
638
  outline: none;
621
639
  }
622
640
  .fillo-dropzone--disabled { cursor: not-allowed; opacity: 0.6; }
623
- .fillo-dropzone-title { font-weight: 550; font-size: 0.95rem; }
624
- .fillo-dropzone-hint { color: var(--fillo-muted); font-size: 0.82rem; }
641
+ .fillo-dropzone-title { font-weight: 550; font-size: 0.95em; }
642
+ .fillo-dropzone-hint { color: var(--fillo-muted); font-size: 0.82em; }
625
643
 
626
644
  .fillo-files { list-style: none; display: flex; flex-direction: column; gap: 0.45rem; margin: 0; padding: 0; }
627
645
  .fillo-file {
@@ -632,7 +650,7 @@
632
650
  border: 1px solid var(--fillo-border);
633
651
  border-radius: var(--fillo-radius);
634
652
  padding: 0.625rem 0.75rem;
635
- font-size: 0.88rem;
653
+ font-size: 0.88em;
636
654
  background: var(--fillo-control-bg);
637
655
  box-sizing: border-box;
638
656
  min-width: 0;
@@ -692,7 +710,7 @@
692
710
  }
693
711
  .fillo-file-error {
694
712
  color: var(--fillo-error);
695
- font-size: 0.82rem;
713
+ font-size: 0.93em;
696
714
  line-height: 1.35;
697
715
  overflow-wrap: anywhere;
698
716
  }
@@ -747,7 +765,7 @@
747
765
  }
748
766
 
749
767
  /* ---------- Content blocks ---------- */
750
- .fillo-heading { font-size: 1.15rem; font-weight: 600; margin: 0.5rem 0 0; }
768
+ .fillo-heading { font-size: 1.15em; font-weight: 600; margin: 0.5rem 0 0; }
751
769
  .fillo-paragraph { color: var(--fillo-muted); line-height: 1.6; margin: 0; }
752
770
  .fillo-divider { border: none; border-top: 1px solid var(--fillo-border); margin: 0.25rem 0; }
753
771
 
@@ -821,11 +839,11 @@
821
839
  padding: 2rem 1rem;
822
840
  text-align: center;
823
841
  }
824
- .fillo-not-open-title { margin: 0; font-size: 1.05rem; font-weight: 650; letter-spacing: 0; }
842
+ .fillo-not-open-title { margin: 0; font-size: 1.05em; font-weight: 650; letter-spacing: 0; }
825
843
  .fillo-not-open-body {
826
844
  max-width: 26rem;
827
845
  margin: 0;
828
- font-size: 0.875rem;
846
+ font-size: 0.875em;
829
847
  color: var(--fillo-muted);
830
848
  line-height: 1.5;
831
849
  }
@@ -849,8 +867,8 @@
849
867
  border-bottom: 2.5px solid var(--fillo-primary-contrast);
850
868
  transform: translateY(-0.09rem) rotate(-45deg);
851
869
  }
852
- .fillo-success-title { margin: 0; font-size: 1.05rem; letter-spacing: 0; }
853
- .fillo-success-message { margin: 0; font-size: 0.875rem; color: var(--fillo-muted); }
870
+ .fillo-success-title { margin: 0; font-size: 1.05em; letter-spacing: 0; }
871
+ .fillo-success-message { margin: 0; font-size: 0.875em; color: var(--fillo-muted); }
854
872
 
855
873
  /* Honeypot — off-screen, never display:none (bots check that) */
856
874
  .fillo-hp {
@@ -911,7 +929,7 @@
911
929
  display: block;
912
930
  margin-top: 1.25rem;
913
931
  text-align: center;
914
- font-size: 0.75rem;
932
+ font-size: 0.75em;
915
933
  font-weight: 500;
916
934
  letter-spacing: 0;
917
935
  color: var(--fillo-muted);
@@ -929,7 +947,7 @@
929
947
  border-radius: var(--fillo-radius);
930
948
  background: #fffbeb;
931
949
  color: #92400e;
932
- font-size: 0.82rem;
950
+ font-size: 0.82em;
933
951
  line-height: 1.45;
934
952
  }
935
953
  .fillo-devwarning code {
@@ -948,7 +966,7 @@
948
966
  border-radius: 999px;
949
967
  background: #fef3c7;
950
968
  color: #92400e;
951
- font-size: 0.7rem;
969
+ font-size: 0.7em;
952
970
  font-weight: 600;
953
971
  letter-spacing: 0.06em;
954
972
  text-transform: uppercase;
@@ -966,7 +984,7 @@
966
984
  border-radius: var(--fillo-radius);
967
985
  background: var(--fillo-control-bg);
968
986
  color: var(--fillo-muted);
969
- font-size: 0.82rem;
987
+ font-size: 0.82em;
970
988
  line-height: 1.45;
971
989
  }
972
990
  .fillo-resume-clear {
@@ -1006,7 +1024,7 @@
1006
1024
  .fillo-turnstile-error {
1007
1025
  margin: 0;
1008
1026
  color: var(--fillo-error);
1009
- font-size: 0.82rem;
1027
+ font-size: 0.82em;
1010
1028
  line-height: 1.45;
1011
1029
  }
1012
1030
 
@@ -1027,7 +1045,7 @@
1027
1045
  justify-content: space-between;
1028
1046
  gap: 0.5rem;
1029
1047
  }
1030
- .fillo-group-instance-title { margin: 0; font-size: 0.95rem; font-weight: 600; }
1048
+ .fillo-group-instance-title { margin: 0; font-size: 0.95em; font-weight: 600; }
1031
1049
  .fillo-group-add {
1032
1050
  align-self: flex-start;
1033
1051
  font: inherit;
@@ -1040,7 +1058,7 @@
1040
1058
  }
1041
1059
  .fillo-group-add:disabled { opacity: 0.5; cursor: default; }
1042
1060
  .fillo-group-remove {
1043
- font-size: 1.1rem;
1061
+ font-size: 1.1em;
1044
1062
  line-height: 1;
1045
1063
  padding: 0 0.2rem;
1046
1064
  border: none;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefillo/react",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Headless React components for embedding Fillo forms natively in your product.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -43,7 +43,7 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@usefillo/core": "^0.17.0"
46
+ "@usefillo/core": "^0.19.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": "^18.0.0 || ^19.0.0",