@civitai/blocks-react 0.64.0 → 0.65.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,6 +28,25 @@ export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
28
28
  * no deep hook paths, so the rename touches no reachable API.
29
29
  */
30
30
  export declare const HUMAN_INTERACTION_TIMEOUT_MS: number;
31
+ /**
32
+ * The bound for a `'protocol'` request whose SERVER WORK is itself slow — the
33
+ * host forwards it to the orchestrator and replies only once that answers. No
34
+ * person is in the loop, so it stays in the `'protocol'` bucket below; it just
35
+ * needs more than {@link DEFAULT_REQUEST_TIMEOUT_MS}.
36
+ *
37
+ * Callers: the workflow trio in `hooks/useBuzzWorkflow.ts` (a submit is a whatif
38
+ * preflight plus the real submit plus a prompt audit), and
39
+ * `PREPARE_TRAINING_DATASET` in `hooks/usePrepareTrainingDataset.ts`, whose
40
+ * server moderates the captions and imports up to 50 images into the
41
+ * orchestrator, the import alone under a 60s budget
42
+ * (`BLOCK_TRAINING_IMPORT_BUDGET_MS` in civitai/civitai's
43
+ * `block-training-dataset.service.ts`) — so the 30s default would reject a
44
+ * healthy prepare mid-import.
45
+ *
46
+ * Was a module-local constant in `useBuzzWorkflow.ts`; moved here so the second
47
+ * caller shares the number instead of re-typing it.
48
+ */
49
+ export declare const WORKFLOW_REQUEST_TIMEOUT_MS = 120000;
31
50
  /** Which bound a block→parent message is answered under. */
32
51
  export type RequestTimeoutClass =
33
52
  /** Reply waits on a PERSON — must pass {@link HUMAN_INTERACTION_TIMEOUT_MS}. */
@@ -27,6 +27,25 @@ export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
27
27
  * no deep hook paths, so the rename touches no reachable API.
28
28
  */
29
29
  export const HUMAN_INTERACTION_TIMEOUT_MS = 10 * 60_000;
30
+ /**
31
+ * The bound for a `'protocol'` request whose SERVER WORK is itself slow — the
32
+ * host forwards it to the orchestrator and replies only once that answers. No
33
+ * person is in the loop, so it stays in the `'protocol'` bucket below; it just
34
+ * needs more than {@link DEFAULT_REQUEST_TIMEOUT_MS}.
35
+ *
36
+ * Callers: the workflow trio in `hooks/useBuzzWorkflow.ts` (a submit is a whatif
37
+ * preflight plus the real submit plus a prompt audit), and
38
+ * `PREPARE_TRAINING_DATASET` in `hooks/usePrepareTrainingDataset.ts`, whose
39
+ * server moderates the captions and imports up to 50 images into the
40
+ * orchestrator, the import alone under a 60s budget
41
+ * (`BLOCK_TRAINING_IMPORT_BUDGET_MS` in civitai/civitai's
42
+ * `block-training-dataset.service.ts`) — so the 30s default would reject a
43
+ * healthy prepare mid-import.
44
+ *
45
+ * Was a module-local constant in `useBuzzWorkflow.ts`; moved here so the second
46
+ * caller shares the number instead of re-typing it.
47
+ */
48
+ export const WORKFLOW_REQUEST_TIMEOUT_MS = 120_000;
30
49
  /**
31
50
  * 🔴 THE TOTALITY GATE — every block→parent message type, bucketed.
32
51
  *
@@ -69,6 +88,12 @@ const REQUEST_TIMEOUT_CLASS = {
69
88
  // failure, with nothing to reconcile the two. Unlike the publish bridge there
70
89
  // is no "outputs were lost" framing available here: the post exists.
71
90
  CREATE_POST_FROM_APP: 'human',
91
+ // 🔴 `'human'`: the host opens a training CONSENT dialog (price, base model,
92
+ // length, dataset size) and replies only on the viewer's click or dismiss. At
93
+ // the 30s default the request would reject with the dialog still open, and a
94
+ // viewer who then clicked Train would start a CHARGED run the block had
95
+ // already reported as failed.
96
+ RUN_TRAINING: 'human',
72
97
  // ── Fire-and-forget: no requestId, so no pending promise to time out ────
73
98
  BLOCK_ERROR: 'no-reply',
74
99
  BLOCK_HELLO: 'no-reply',
@@ -97,6 +122,11 @@ const REQUEST_TIMEOUT_CLASS = {
97
122
  APP_STORAGE_SET: 'protocol',
98
123
  CANCEL_APP_WORKFLOW: 'protocol',
99
124
  CANCEL_WORKFLOW: 'protocol',
125
+ // No dialog — preparing a dataset charges nothing and the host replies when
126
+ // the server does. Server-bound, so `'protocol'`; but sent under
127
+ // `WORKFLOW_REQUEST_TIMEOUT_MS`, not the 30s default, because the server's
128
+ // image import alone may take 60s. Same reasoning as the workflow trio below.
129
+ PREPARE_TRAINING_DATASET: 'protocol',
100
130
  // The workflow trio is server-bound, not person-bound. It carries its OWN
101
131
  // longer bound (`WORKFLOW_REQUEST_TIMEOUT_MS` in `hooks/useBuzzWorkflow.ts`)
102
132
  // for orchestrator latency — a different reason for a different number.
@@ -399,6 +399,33 @@ export declare function isValidCollectionFollowResult(p: unknown): boolean;
399
399
  * so a drop at least times out visibly.
400
400
  */
401
401
  export declare function isValidCreatePostResult(p: unknown): boolean;
402
+ /**
403
+ * Reply to `PREPARE_TRAINING_DATASET`. A well-formed reply carries EITHER a
404
+ * `result` (`{ datasetId: non-empty string, count: integer ≥ 0, rejected: [{
405
+ * imageId: number, reason: string }] }`) OR an `error`; one with neither is
406
+ * malformed and dropped.
407
+ *
408
+ * 🔴 `error` IS SHAPE-CHECKED ONLY — the same decision as
409
+ * {@link isValidCreatePostResult}: the host's closed codes share the field with
410
+ * any server message it forwards, and a membership check would drop those
411
+ * replies before correlation, leaving the request to its timer.
412
+ *
413
+ * `rejected[].reason` is checked as a STRING, not against today's six members —
414
+ * the host adding a reason must not make every prepare reply that carries it a
415
+ * dropped one (the forward-compatibility rule in {@link isValidWorkflowSnapshot}).
416
+ */
417
+ export declare function isValidTrainingDatasetResult(p: unknown): boolean;
418
+ /**
419
+ * Reply to `RUN_TRAINING`. A well-formed reply carries EITHER a `snapshot` (a
420
+ * valid {@link isValidWorkflowSnapshot} — the block polls its `workflowId`) OR an
421
+ * `error`; one with neither is malformed and dropped.
422
+ *
423
+ * 🔴 `error` IS SHAPE-CHECKED ONLY, and here a membership check would cost the
424
+ * most: this request carries the TEN-MINUTE consent bound, and the host forwards
425
+ * server refusals (an expired quote, a changed body, an ineligible image) in this
426
+ * field. A dropped refusal would leave a Train button spinning for ten minutes.
427
+ */
428
+ export declare function isValidTrainingResult(p: unknown): boolean;
402
429
  /**
403
430
  * Reply to `APP_STORAGE_GET`. `value` is arbitrary JSON (incl. `null` for an
404
431
  * unset key / anon viewer), so there is nothing to type-check on it — the guard
@@ -315,18 +315,47 @@ export function isValidWorkflowSnapshot(s) {
315
315
  return false;
316
316
  }
317
317
  }
318
- if (s.trainedEpochs !== undefined) {
319
- if (!Array.isArray(s.trainedEpochs))
320
- return false;
321
- for (const e of s.trainedEpochs) {
322
- if (!isObject(e) || typeof e.$type !== 'string')
323
- return false;
324
- if (!Number.isInteger(e.epochNumber))
325
- return false;
326
- }
327
- }
318
+ if (s.trainedEpochs !== undefined && !isValidTrainedEpochs(s.trainedEpochs))
319
+ return false;
328
320
  if (s.publishedModel !== undefined && !isValidPublishedModel(s.publishedModel))
329
321
  return false;
322
+ if (s.trainingQuote !== undefined && !isValidTrainingQuote(s.trainingQuote))
323
+ return false;
324
+ return true;
325
+ }
326
+ /**
327
+ * Shape check for `trainingQuote` — the quote a `kind: 'training'` estimate
328
+ * returns. The block passes `quoteId` back verbatim and renders `total`, so both
329
+ * are pinned: a non-empty string, a finite number. `quoteId`'s `tq_…` FORMAT is
330
+ * deliberately not pinned — it is opaque, and a host changing it must not turn
331
+ * every estimate reply into a dropped one.
332
+ */
333
+ function isValidTrainingQuote(q) {
334
+ if (!isObject(q))
335
+ return false;
336
+ if (!isNonEmptyString(q.quoteId))
337
+ return false;
338
+ if (!isFiniteNumber(q.total))
339
+ return false;
340
+ if (!Number.isInteger(q.imageCount) || q.imageCount < 0)
341
+ return false;
342
+ return typeof q.expiresAt === 'string';
343
+ }
344
+ /**
345
+ * Shape check for `trainedEpochs` — shared by {@link isValidWorkflowSnapshot}
346
+ * and the `AppWorkflow` row guard, which carry the same field. `$type` is
347
+ * checked as a STRING, not against today's members (see the forward-
348
+ * compatibility note in {@link isValidWorkflowSnapshot}).
349
+ */
350
+ function isValidTrainedEpochs(epochs) {
351
+ if (!Array.isArray(epochs))
352
+ return false;
353
+ for (const e of epochs) {
354
+ if (!isObject(e) || typeof e.$type !== 'string')
355
+ return false;
356
+ if (!Number.isInteger(e.epochNumber))
357
+ return false;
358
+ }
330
359
  return true;
331
360
  }
332
361
  /**
@@ -906,9 +935,11 @@ function isValidAppWorkflow(w) {
906
935
  if (img.nsfwLevel !== null && !isFiniteNumber(img.nsfwLevel))
907
936
  return false;
908
937
  }
909
- // OPTIONAL — absent on hosts that predate it; checked only when present.
938
+ // OPTIONAL — absent on hosts that predate them; checked only when present.
910
939
  if (w.publishedModel !== undefined && !isValidPublishedModel(w.publishedModel))
911
940
  return false;
941
+ if (w.trainedEpochs !== undefined && !isValidTrainedEpochs(w.trainedEpochs))
942
+ return false;
912
943
  return true;
913
944
  }
914
945
  /**
@@ -1246,6 +1277,71 @@ export function isValidCreatePostResult(p) {
1246
1277
  return false;
1247
1278
  return true;
1248
1279
  }
1280
+ /**
1281
+ * Reply to `PREPARE_TRAINING_DATASET`. A well-formed reply carries EITHER a
1282
+ * `result` (`{ datasetId: non-empty string, count: integer ≥ 0, rejected: [{
1283
+ * imageId: number, reason: string }] }`) OR an `error`; one with neither is
1284
+ * malformed and dropped.
1285
+ *
1286
+ * 🔴 `error` IS SHAPE-CHECKED ONLY — the same decision as
1287
+ * {@link isValidCreatePostResult}: the host's closed codes share the field with
1288
+ * any server message it forwards, and a membership check would drop those
1289
+ * replies before correlation, leaving the request to its timer.
1290
+ *
1291
+ * `rejected[].reason` is checked as a STRING, not against today's six members —
1292
+ * the host adding a reason must not make every prepare reply that carries it a
1293
+ * dropped one (the forward-compatibility rule in {@link isValidWorkflowSnapshot}).
1294
+ */
1295
+ export function isValidTrainingDatasetResult(p) {
1296
+ if (!isObject(p))
1297
+ return false;
1298
+ if (!isWireRequestIdShape(p.requestId))
1299
+ return false;
1300
+ if (p.error !== undefined && typeof p.error !== 'string')
1301
+ return false;
1302
+ if (p.result !== undefined) {
1303
+ const r = p.result;
1304
+ if (!isObject(r))
1305
+ return false;
1306
+ if (!isNonEmptyString(r.datasetId))
1307
+ return false;
1308
+ if (!Number.isInteger(r.count) || r.count < 0)
1309
+ return false;
1310
+ if (!Array.isArray(r.rejected))
1311
+ return false;
1312
+ for (const x of r.rejected) {
1313
+ if (!isObject(x) || !isFiniteNumber(x.imageId) || typeof x.reason !== 'string') {
1314
+ return false;
1315
+ }
1316
+ }
1317
+ }
1318
+ if (p.result === undefined && p.error === undefined)
1319
+ return false;
1320
+ return true;
1321
+ }
1322
+ /**
1323
+ * Reply to `RUN_TRAINING`. A well-formed reply carries EITHER a `snapshot` (a
1324
+ * valid {@link isValidWorkflowSnapshot} — the block polls its `workflowId`) OR an
1325
+ * `error`; one with neither is malformed and dropped.
1326
+ *
1327
+ * 🔴 `error` IS SHAPE-CHECKED ONLY, and here a membership check would cost the
1328
+ * most: this request carries the TEN-MINUTE consent bound, and the host forwards
1329
+ * server refusals (an expired quote, a changed body, an ineligible image) in this
1330
+ * field. A dropped refusal would leave a Train button spinning for ten minutes.
1331
+ */
1332
+ export function isValidTrainingResult(p) {
1333
+ if (!isObject(p))
1334
+ return false;
1335
+ if (!isWireRequestIdShape(p.requestId))
1336
+ return false;
1337
+ if (p.error !== undefined && typeof p.error !== 'string')
1338
+ return false;
1339
+ if (p.snapshot !== undefined && !isValidWorkflowSnapshot(p.snapshot))
1340
+ return false;
1341
+ if (p.snapshot === undefined && p.error === undefined)
1342
+ return false;
1343
+ return true;
1344
+ }
1249
1345
  // ============================================================
1250
1346
  // App-storage (per-viewer KV) reply validators
1251
1347
  // ============================================================
@@ -1789,6 +1885,13 @@ export function payloadValidatorFor(type) {
1789
1885
  // `validate.test.ts`.
1790
1886
  case 'CREATE_POST_RESULT':
1791
1887
  return isValidCreatePostResult;
1888
+ // Same caveat as CREATE_POST_RESULT: presence is compiler-enforced, the
1889
+ // identity is pinned by test (`validate.test.ts`). TRAINING_RESULT settles a
1890
+ // charged run.
1891
+ case 'TRAINING_DATASET_RESULT':
1892
+ return isValidTrainingDatasetResult;
1893
+ case 'TRAINING_RESULT':
1894
+ return isValidTrainingResult;
1792
1895
  case 'IMAGE_UPLOAD_RESULT':
1793
1896
  return isValidImageUploadResult;
1794
1897
  case 'IMAGE_SCAN_RESOLVED':
@@ -5,7 +5,7 @@
5
5
  * CSS. (At RUNTIME, `injectBlocksStyles()` injects these as three separately
6
6
  * marked `<style>` elements instead — see below.)
7
7
  */
8
- export declare const BLOCKS_UI_STYLES = "/* AUTOGENERATED by @civitai/theme (scripts/build-tokens.ts). DO NOT EDIT.\n Regenerate: pnpm --filter @civitai/theme build. Guarded by generation-parity test. */\n@property --civitai-color-text {\n syntax: '<color>';\n inherits: true;\n initial-value: #C1C2C5;\n}\n@property --civitai-color-text-dimmed {\n syntax: '<color>';\n inherits: true;\n initial-value: #8c8fa3;\n}\n@property --civitai-color-body {\n syntax: '<color>';\n inherits: true;\n initial-value: #1A1B1E;\n}\n@property --civitai-color-surface {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-surface-2 {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-border {\n syntax: '<color>';\n inherits: true;\n initial-value: #373A40;\n}\n@property --civitai-color-primary {\n syntax: '<color>';\n inherits: true;\n initial-value: #1971C2;\n}\n@property --civitai-color-primary-hover {\n syntax: '<color>';\n inherits: true;\n initial-value: #1864AB;\n}\n@property --civitai-color-primary-fg {\n syntax: '<color>';\n inherits: true;\n initial-value: #fefefe;\n}\n@property --civitai-color-primary-light {\n syntax: '<color>';\n inherits: true;\n initial-value: rgba(34, 139, 230, 0.15);\n}\n@property --civitai-color-error {\n syntax: '<color>';\n inherits: true;\n initial-value: #e03131;\n}\n@property --civitai-color-success {\n syntax: '<color>';\n inherits: true;\n initial-value: #326D5C;\n}\n@property --civitai-color-warning {\n syntax: '<color>';\n inherits: true;\n initial-value: #e8590c;\n}\n@property --civitai-color-info {\n syntax: '<color>';\n inherits: true;\n initial-value: #1971C2;\n}\n@property --civitai-color-gray-0 {\n syntax: '<color>';\n inherits: true;\n initial-value: #f8f9fa;\n}\n@property --civitai-color-gray-1 {\n syntax: '<color>';\n inherits: true;\n initial-value: #f1f3f5;\n}\n@property --civitai-color-gray-2 {\n syntax: '<color>';\n inherits: true;\n initial-value: #e9ecef;\n}\n@property --civitai-color-gray-3 {\n syntax: '<color>';\n inherits: true;\n initial-value: #dee2e6;\n}\n@property --civitai-color-gray-4 {\n syntax: '<color>';\n inherits: true;\n initial-value: #ced4da;\n}\n@property --civitai-color-gray-5 {\n syntax: '<color>';\n inherits: true;\n initial-value: #adb5bd;\n}\n@property --civitai-color-gray-6 {\n syntax: '<color>';\n inherits: true;\n initial-value: #868e96;\n}\n@property --civitai-color-gray-7 {\n syntax: '<color>';\n inherits: true;\n initial-value: #495057;\n}\n@property --civitai-color-gray-8 {\n syntax: '<color>';\n inherits: true;\n initial-value: #343a40;\n}\n@property --civitai-color-gray-9 {\n syntax: '<color>';\n inherits: true;\n initial-value: #212529;\n}\n@property --civitai-color-track {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-segmented-bg {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-media-placeholder {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n\n:root {\n color-scheme: dark;\n --civitai-font: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;\n --civitai-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;\n --civitai-radius: 0.25rem;\n --civitai-color-text: #C1C2C5;\n --civitai-color-text-dimmed: #8c8fa3;\n --civitai-color-body: #1A1B1E;\n --civitai-color-surface: #25262B;\n --civitai-color-surface-2: #25262B;\n --civitai-color-border: #373A40;\n --civitai-color-primary: #1971C2;\n --civitai-color-primary-hover: #1864AB;\n --civitai-color-primary-fg: #fefefe;\n --civitai-color-primary-light: rgba(34, 139, 230, 0.15);\n --civitai-color-error: #e03131;\n --civitai-color-success: #326D5C;\n --civitai-color-warning: #e8590c;\n --civitai-color-info: #1971C2;\n --civitai-color-gray-0: #f8f9fa;\n --civitai-color-gray-1: #f1f3f5;\n --civitai-color-gray-2: #e9ecef;\n --civitai-color-gray-3: #dee2e6;\n --civitai-color-gray-4: #ced4da;\n --civitai-color-gray-5: #adb5bd;\n --civitai-color-gray-6: #868e96;\n --civitai-color-gray-7: #495057;\n --civitai-color-gray-8: #343a40;\n --civitai-color-gray-9: #212529;\n --civitai-bp-xs: 480px;\n --civitai-bp-sm: 768px;\n --civitai-bp-md: 1024px;\n --civitai-bp-lg: 1184px;\n --civitai-bp-xl: 1440px;\n --civitai-card-border-width: 0;\n --civitai-color-track: #25262B;\n --civitai-color-segmented-bg: #25262B;\n --civitai-color-media-placeholder: #25262B;\n}\n\n[data-theme='light'] {\n color-scheme: light;\n --civitai-font: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;\n --civitai-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;\n --civitai-radius: 0.25rem;\n --civitai-color-text: #222;\n --civitai-color-text-dimmed: #868e96;\n --civitai-color-body: #fefefe;\n --civitai-color-surface: #fefefe;\n --civitai-color-surface-2: #fefefe;\n --civitai-color-border: #ced4da;\n --civitai-color-primary: #228BE6;\n --civitai-color-primary-hover: #1C7ED6;\n --civitai-color-primary-fg: #fefefe;\n --civitai-color-primary-light: rgba(34, 139, 230, 0.1);\n --civitai-color-error: #fa5252;\n --civitai-color-success: #299C7A;\n --civitai-color-warning: #fd7e14;\n --civitai-color-info: #228BE6;\n --civitai-color-gray-0: #f8f9fa;\n --civitai-color-gray-1: #f1f3f5;\n --civitai-color-gray-2: #e9ecef;\n --civitai-color-gray-3: #dee2e6;\n --civitai-color-gray-4: #ced4da;\n --civitai-color-gray-5: #adb5bd;\n --civitai-color-gray-6: #868e96;\n --civitai-color-gray-7: #495057;\n --civitai-color-gray-8: #343a40;\n --civitai-color-gray-9: #212529;\n --civitai-bp-xs: 480px;\n --civitai-bp-sm: 768px;\n --civitai-bp-md: 1024px;\n --civitai-bp-lg: 1184px;\n --civitai-bp-xl: 1440px;\n --civitai-card-border-width: 1px;\n --civitai-color-track: #e9ecef;\n --civitai-color-segmented-bg: #f1f3f5;\n --civitai-color-media-placeholder: #e9ecef;\n}\n\n[data-theme='dark'] {\n color-scheme: dark;\n --civitai-color-text: #C1C2C5;\n --civitai-color-text-dimmed: #8c8fa3;\n --civitai-color-body: #1A1B1E;\n --civitai-color-surface: #25262B;\n --civitai-color-surface-2: #25262B;\n --civitai-color-border: #373A40;\n --civitai-color-primary: #1971C2;\n --civitai-color-primary-hover: #1864AB;\n --civitai-color-primary-fg: #fefefe;\n --civitai-color-primary-light: rgba(34, 139, 230, 0.15);\n --civitai-color-error: #e03131;\n --civitai-color-success: #326D5C;\n --civitai-color-warning: #e8590c;\n --civitai-color-info: #1971C2;\n --civitai-card-border-width: 0;\n --civitai-color-track: #25262B;\n --civitai-color-segmented-bg: #25262B;\n --civitai-color-media-placeholder: #25262B;\n}\n\n/*\n * @civitai/components \u2014 attribute-driven, framework-agnostic component CSS.\n *\n * Contract: style is selected by `data-civitai-ui=\"<name>\"` + `data-variant` +\n * `data-size` (+ a few component-specific `data-*`). Theme via an ancestor\n * `[data-theme='light'|'dark']`. Tokens come from @civitai/theme's `--civitai-*`\n * custom properties (link `@civitai/theme/styles.css` alongside this file, or\n * call `injectStyles()` which injects both).\n *\n * ALL rules HERE live in `@layer civitai.components`, so a consumer's own\n * unlayered CSS always wins over THIS SHEET without specificity fights.\n * \uD83D\uDD34 That does NOT extend to the TOKENS. @civitai/theme's sheet carries no\n * layer and declares them at `:root`/`[data-theme='\u2026']` (0-1-0), so a\n * consumer's `:root { --civitai-\u2026: }` only TIES with it and stylesheet order\n * decides \u2014 silently, in the order `useBlocksStyles()` produces. Scope a token\n * override (a class on your root, or an inline style); never `:root`. Measured\n * matrix: MARKUP.md, \"Cascade / overriding\".\n * State colors are derived with `color-mix()` from base tokens (no shade\n * enumeration), and structure uses native CSS nesting (no preprocessor).\n *\n * See MARKUP.md for the full per-component markup + ARIA contract.\n */\n@layer civitai.components {\n [data-civitai-ui] {\n box-sizing: border-box;\n font-family: var(--civitai-font);\n\n & *,\n & *::before,\n & *::after {\n box-sizing: border-box;\n }\n }\n\n /* ----- Button ----- */\n [data-civitai-ui='button'] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n border: 1px solid transparent;\n border-radius: var(--civitai-radius);\n font-family: var(--civitai-font);\n font-weight: 600;\n line-height: 1;\n cursor: pointer;\n user-select: none;\n text-decoration: none;\n transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;\n\n /*\n * Documented DEFAULTS (MARKUP.md): variant=filled, size=md. Applied on the\n * BASE rule (unconditionally) so BARE markup \u2014 a `<button data-civitai-ui=\n * \"button\">` with no `data-variant`/`data-size` \u2014 renders the documented\n * default (filled + md), not an unstyled/zero-size button. The explicit\n * `[data-variant]` / `[data-size]` rules below OVERRIDE (all four variants\n * set their own bg/border/hover, so the base filled default only ever wins\n * for a bare button). Every non-filled variant also sets its own\n * `border-color`, so the base primary border never leaks to them.\n */\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n height: 36px;\n padding: 0 18px;\n font-size: 14px;\n\n &:hover:not(:disabled) {\n background: var(--civitai-color-primary-hover);\n border-color: var(--civitai-color-primary-hover);\n }\n\n &[data-size='sm'] { height: 30px; padding: 0 14px; font-size: 13px; }\n &[data-size='md'] { height: 36px; padding: 0 18px; font-size: 14px; }\n &[data-size='lg'] { height: 44px; padding: 0 22px; font-size: 16px; }\n &[data-full-width='true'] { width: 100%; }\n\n &[data-variant='filled'] {\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n\n &:hover:not(:disabled) {\n background: var(--civitai-color-primary-hover);\n border-color: var(--civitai-color-primary-hover);\n }\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-primary) 12%, transparent);\n color: var(--civitai-color-primary);\n border-color: transparent;\n\n &:hover:not(:disabled) {\n background: color-mix(in srgb, var(--civitai-color-primary) 22%, transparent);\n }\n }\n &[data-variant='outline'] {\n background: transparent;\n color: var(--civitai-color-primary);\n border-color: var(--civitai-color-primary);\n\n &:hover:not(:disabled) {\n background: color-mix(in srgb, var(--civitai-color-primary) 10%, transparent);\n }\n }\n &[data-variant='subtle'] {\n background: transparent;\n color: var(--civitai-color-primary);\n border-color: transparent;\n\n &:hover:not(:disabled) {\n background: color-mix(in srgb, var(--civitai-color-primary) 10%, transparent);\n }\n }\n &:disabled,\n &[aria-busy='true'] {\n opacity: 0.6;\n cursor: not-allowed;\n }\n & [data-civitai-ui-section] {\n display: inline-flex;\n align-items: center;\n }\n }\n\n /* ----- TextInput / Textarea / NumberInput / Select ----- */\n [data-civitai-ui='text-input'],\n [data-civitai-ui='textarea'],\n [data-civitai-ui='number-input'],\n [data-civitai-ui='select'] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n [data-civitai-ui-label] {\n font-size: 14px;\n font-weight: 600;\n color: var(--civitai-color-text);\n }\n [data-civitai-ui-required] {\n color: var(--civitai-color-error);\n margin-left: 2px;\n }\n [data-civitai-ui-description] {\n font-size: 12px;\n color: var(--civitai-color-text-dimmed);\n }\n [data-civitai-ui-error] {\n font-size: 12px;\n color: var(--civitai-color-error);\n }\n [data-civitai-ui-control] {\n width: 100%;\n font-family: var(--civitai-font);\n font-size: 14px;\n color: var(--civitai-color-text);\n background: var(--civitai-color-surface);\n border: 1px solid var(--civitai-color-border);\n border-radius: var(--civitai-radius);\n padding: 8px 12px;\n transition: border-color 120ms ease;\n\n &:focus {\n outline: none;\n border-color: var(--civitai-color-primary);\n }\n &[aria-invalid='true'] {\n border-color: var(--civitai-color-error);\n }\n &:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n }\n textarea[data-civitai-ui-control] {\n resize: vertical;\n line-height: 1.5;\n }\n /*\n * Select (issue #181 F6): a native <select> reusing the shared\n * `-control` field chrome (border/background/radius/focus/invalid, above).\n * Keep the native disclosure caret (most robust + accessible); just make the\n * control feel interactive and reserve room on the right so the caret never\n * overlaps a long option label.\n */\n select[data-civitai-ui-control] {\n cursor: pointer;\n padding-right: 28px;\n line-height: 1.4;\n }\n select[data-civitai-ui-control]:disabled {\n cursor: not-allowed;\n }\n\n /* ----- Checkbox / Radio (issue #181 F6) ----- */\n /*\n * Themed native <input type=\"checkbox\">/<input type=\"radio\">: `accent-color`\n * carries the primary tint (native control, so keyboard/indeterminate/tab all\n * work for free), plus custom sizing, a token focus ring, and disabled state.\n * The box + its inline label sit in a `-choice` row; description + error live\n * below, reusing the shared field markers. The inputs deliberately do NOT\n * carry `data-civitai-ui-control` (that is the full-width field-input chrome).\n */\n [data-civitai-ui='checkbox'],\n [data-civitai-ui='radio'] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n [data-civitai-ui-choice] {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n [data-civitai-ui='checkbox'] [data-civitai-ui-label],\n [data-civitai-ui='radio'] [data-civitai-ui-label] {\n font-weight: 500;\n cursor: pointer;\n user-select: none;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox'],\n [data-civitai-ui='radio'] input[type='radio'] {\n accent-color: var(--civitai-color-primary);\n width: 16px;\n height: 16px;\n margin: 0;\n flex: none;\n cursor: pointer;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox']:focus-visible,\n [data-civitai-ui='radio'] input[type='radio']:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox']:disabled,\n [data-civitai-ui='radio'] input[type='radio']:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox']:disabled ~ [data-civitai-ui-label],\n [data-civitai-ui='radio'] input[type='radio']:disabled ~ [data-civitai-ui-label] {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n /* ----- RadioGroup (issue #181 F6) \u2014 role=radiogroup layout ----- */\n [data-civitai-ui='radio-group'] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n [data-civitai-ui-radio-options] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n\n &[data-orientation='horizontal'] {\n flex-direction: row;\n flex-wrap: wrap;\n gap: 16px;\n }\n }\n /*\n * RadioGroup group-level error (0.2.0): mirrors the field `-error` treatment.\n * The message reuses the shared `[data-civitai-ui-error]` styling (12px, error\n * token); the invalid state (`data-invalid` on the group, set alongside\n * `aria-invalid`) tints the child radios' `accent-color` to the error token so\n * the invalid cue reads on the group, not just the message text \u2014 the group\n * analogue of a field control's error-colored border.\n */\n [data-civitai-ui='radio-group'] [data-civitai-ui-error] {\n font-size: 12px;\n color: var(--civitai-color-error);\n }\n [data-civitai-ui='radio-group'][data-invalid] input[type='radio'] {\n accent-color: var(--civitai-color-error);\n }\n\n /* ----- Card ----- */\n /*\n * F5 (issue #181): light resolves surface and body to the same #fefefe, so a\n * borderless card needs a default hairline to be visible at all; dark already\n * separates them and zeroes the width. `data-with-border` is the stronger,\n * explicit, opaque border and is unconditional.\n */\n [data-civitai-ui='card'] {\n background: var(--civitai-color-surface);\n border-radius: var(--civitai-radius);\n color: var(--civitai-color-text);\n border: var(--civitai-card-border-width, 1px) solid\n color-mix(in srgb, var(--civitai-color-border) 55%, transparent);\n\n &[data-with-border='true'] {\n border: 1px solid var(--civitai-color-border);\n }\n &[data-padding='sm'] { padding: 10px; }\n &[data-padding='md'] { padding: 16px; }\n &[data-padding='lg'] { padding: 24px; }\n }\n\n /* ----- Stack / Group ----- */\n [data-civitai-ui='stack'] {\n display: flex;\n flex-direction: column;\n gap: 12px;\n\n &[data-gap='sm'] { gap: 8px; }\n &[data-gap='md'] { gap: 16px; }\n &[data-gap='lg'] { gap: 24px; }\n }\n [data-civitai-ui='group'] {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n\n /*\n * RESPONSIVE BASE. A group is a ROW, and a row of three or more controls\n * has no escape at phone widths \u2014 a block's slot is ~360px there, so the\n * row simply ran off the edge.\n *\n * \uD83D\uDD34 There are THREE `group` surfaces, and for TWO of them this is a NEW\n * DEFAULT \u2014 do not read it as the CSS merely catching up:\n * - `@civitai/blocks-react`'s `<Group>` defaults `wrap = true` and writes\n * `flex-wrap` as an INLINE style, so its consumers already wrapped.\n * For that one, and only that one, this is the CSS agreeing with a\n * default that was already shipping.\n * - `@civitai/components-react`'s `<Group>` writes NO inline style and has\n * NO `wrap` prop, so it resolves against this rule \u2014 a React consumer\n * that did NOT wrap before. \uD83D\uDD34 THIS SURFACE NO LONGER EXISTS: see below.\n * - bare `data-civitai-ui=\"group\"` markup, the framework-agnostic contract\n * this package exists to serve \u2014 likewise.\n * Nothing could see the disagreement, because each surface was only ever\n * tested against itself. TWO tests now pin it \u2014 there is no single \"the\n * parity test\":\n * - `civitai-blocks-react/test/Group.test.tsx` reads the wrap default off\n * ITS `<Group>` and compares it to this rule, so flipping either alone\n * fails;\n * - `civitai-components/test/presentational-parity.browser.test.ts`,\n * case **`group`** \u2014 compares `<civitai-group>` against the bare\n * `data-civitai-ui=\"group\"` markup with `flexWrap` among the properties\n * it reads. \uD83D\uDD34 It is `group`, NOT `group/nowrap`: that case sets\n * `nowrap` on BOTH arms, so both resolve to `flex-wrap: nowrap` and it\n * stays green through a deletion of the rule below. Mutate the rule and\n * check the wrong case and you will conclude it is unguarded.\n *\n * \uD83D\uDD34 THE MIDDLE SURFACE IS GONE, and so is the test that used to pin it.\n * `@civitai/components-react@0.9.0` deleted its hand-written `<Group>` when\n * the custom elements superseded the React layer, taking\n * `html-vs-react-parity.browser.test.tsx` (\"styling anchors \u2014 Group\") with\n * it. TWO surfaces now resolve against this rule, not three: bare markup,\n * and blocks-react's `<Group>`. `<civitai-group>` is a third surface that\n * does NOT read this sheet at all \u2014 shadow DOM, carrying its own\n * `flex-wrap` at `src/elements/civitai-group.ts:20`.\n *\n * That independence is why the replacement is not the weak kind of parity\n * check this comment used to warn about. The warning was right about the\n * OLD test: both its arms consumed THIS sheet, so deleting the rule moved\n * them identically and only an absolute anchor could see it.\n * `presentational-parity`'s arms are independent IMPLEMENTATIONS, so\n * deleting `flex-wrap` here moves the legacy arm alone and the comparison\n * fails. WATCHED, not inferred: deleting `flex-wrap: wrap` from this block\n * in `styles.generated.ts` (the string the browser test injects) fails\n * `light / group` and `dark / group` with\n * `flexWrap: expected 'wrap' to be 'nowrap'` \u2014 2 failed / 108 passed;\n * restoring it returns 110/110. (Re-measured after #477 added 21 Text\n * parity cases; the kill is what matters, the totals move with the file.)\n *\n * \uD83D\uDD34 The three-surface distinction is load-bearing for the RELEASE, not just\n * the prose: \"existing callers notice\" is what `RELEASING.md` reserves\n * `major` for, and it is true for two of the three surfaces. Shipped as a\n * MINOR by maintainer decision \u2014 see the changeset for the reasoning and for\n * the upgrade note that decision makes load-bearing.\n *\n * Opt out with `data-nowrap=\"true\"`. An inline `flex-wrap` also still\n * outranks this layer, so existing markup that already sets it is untouched.\n */\n flex-wrap: wrap;\n\n /*\n * A flex item defaults to `min-width: auto`, which refuses to shrink below\n * its CONTENT width \u2014 so one long label (a model name, a prompt) pushes the\n * row past the slot even WITH wrap. `min-width: 0` lets it shrink.\n *\n * \uD83D\uDD34 SCOPE, measured: this only matters for a child whose computed\n * `overflow` is `visible`. Per CSS Flexbox \u00A74.5 an item with any other\n * `overflow` ALREADY has an automatic minimum size of 0 \u2014 so a child that\n * sets `overflow: hidden`, as any `text-overflow: ellipsis` child must, is\n * unaffected by this line. An earlier version of this comment claimed\n * ellipsis \"can never engage\" without it, which was exactly backwards; the\n * browser guard built on that belief used an ellipsis fixture and therefore\n * passed with this rule DELETED. Both are fixed \u2014 see\n * `civitai-blocks-react/test/responsive-group.browser.test.tsx`.\n *\n * Specificity is (0,1,0), not zero: `&` carries the parent selector's, and\n * `:where(*)` is specificity-identical to `*`, so that wrapper bought\n * nothing and has been dropped. What actually lets a CONSUMER's own rule\n * win is `@layer civitai.components`, which every rule in this file sits\n * inside \u2014 unlayered author CSS beats it regardless of specificity. Note\n * the limit: another rule INSIDE this layer does not get that protection,\n * so a future non-zero `min-width` on a nested component here must be more\n * specific or come later.\n */\n & > * { min-width: 0; }\n\n &[data-nowrap='true'] { flex-wrap: nowrap; }\n\n &[data-gap='sm'] { gap: 6px; }\n &[data-gap='md'] { gap: 16px; }\n &[data-gap='lg'] { gap: 24px; }\n }\n\n /* ----- Alert ----- */\n [data-civitai-ui='alert'] {\n display: flex;\n gap: 10px;\n align-items: flex-start;\n padding: 12px 14px;\n border-radius: var(--civitai-radius);\n border: 1px solid transparent;\n font-size: 14px;\n color: var(--civitai-color-text);\n\n /*\n * Documented DEFAULT (MARKUP.md): color=info (default intent). Applied on\n * the base rule so a BARE alert \u2014 `<div data-civitai-ui=\"alert\">` with no\n * `data-color` \u2014 renders the info intent (tinted bg + border), not just the\n * neutral chrome. The explicit `[data-color]` rules below OVERRIDE (success\n * / warning / error each set both bg + border-color), so the info default\n * only ever wins when `data-color` is omitted.\n */\n background: color-mix(in srgb, var(--civitai-color-info) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-info) 35%, transparent);\n\n &[data-color='info'] {\n background: color-mix(in srgb, var(--civitai-color-info) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-info) 35%, transparent);\n }\n &[data-color='success'] {\n background: color-mix(in srgb, var(--civitai-color-success) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-success) 35%, transparent);\n }\n &[data-color='warning'] {\n background: color-mix(in srgb, var(--civitai-color-warning) 14%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-warning) 35%, transparent);\n }\n &[data-color='error'] {\n background: color-mix(in srgb, var(--civitai-color-error) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-error) 35%, transparent);\n }\n & [data-civitai-ui-alert-body] {\n flex: 1;\n min-width: 0;\n }\n & [data-civitai-ui-alert-title] {\n font-weight: 600;\n margin-bottom: 2px;\n }\n & [data-civitai-ui-alert-close] {\n background: transparent;\n border: none;\n cursor: pointer;\n color: inherit;\n font-size: 16px;\n line-height: 1;\n padding: 0;\n opacity: 0.7;\n\n &:hover { opacity: 1; }\n }\n }\n\n /* ----- Loader ----- */\n [data-civitai-ui='loader'] {\n display: inline-block;\n border-radius: 50%;\n border-style: solid;\n border-color: color-mix(in srgb, currentColor 25%, transparent);\n border-top-color: currentColor;\n color: var(--civitai-color-primary);\n animation: civitai-ui-spin 0.7s linear infinite;\n\n /*\n * Documented DEFAULT (MARKUP.md): size=md. Applied on the base rule so a\n * BARE loader \u2014 `<span data-civitai-ui=\"loader\">` with no `data-size` \u2014 has\n * real dimensions (otherwise 0\u00D70 and invisible). sm/lg OVERRIDE below.\n */\n width: 22px;\n height: 22px;\n border-width: 3px;\n\n &[data-size='sm'] { width: 16px; height: 16px; border-width: 2px; }\n &[data-size='md'] { width: 22px; height: 22px; border-width: 3px; }\n &[data-size='lg'] { width: 32px; height: 32px; border-width: 4px; }\n }\n [data-civitai-ui='button'] [data-civitai-ui='loader'] {\n color: currentColor;\n }\n @keyframes civitai-ui-spin {\n to { transform: rotate(360deg); }\n }\n\n /* ----- Badge ----- */\n [data-civitai-ui='badge'] {\n display: inline-flex;\n align-items: center;\n border: 1px solid transparent;\n border-radius: 999px;\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n text-transform: uppercase;\n letter-spacing: 0.02em;\n\n /*\n * Documented DEFAULTS (MARKUP.md): variant=filled, size=md. Applied on the\n * base rule so a BARE badge \u2014 `<span data-civitai-ui=\"badge\">` with no\n * `data-variant`/`data-size` (MARKUP's own minimal example) \u2014 renders\n * filled + md (padding + primary fill), not an unstyled, zero-padding pill.\n * The explicit `[data-variant]`/`[data-size]` rules below OVERRIDE. Because\n * the base now carries a primary `border-color`, the `light` variant (which\n * previously relied on the base transparent border) explicitly resets it to\n * transparent so light badges are visually unchanged.\n */\n height: 22px;\n padding: 0 10px;\n font-size: 11px;\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n\n &[data-size='sm'] { height: 18px; padding: 0 8px; font-size: 10px; }\n &[data-size='md'] { height: 22px; padding: 0 10px; font-size: 11px; }\n &[data-size='lg'] { height: 26px; padding: 0 12px; font-size: 13px; }\n &[data-variant='filled'] {\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-primary) 14%, transparent);\n color: var(--civitai-color-primary);\n border-color: transparent;\n }\n &[data-variant='outline'] {\n background: transparent;\n color: var(--civitai-color-primary);\n border-color: var(--civitai-color-primary);\n }\n\n /*\n * Intent color via `data-color`, mirroring Alert's `data-color` contract\n * (info / success / warning / error). Absent `data-color` => the default\n * primary above (non-breaking). Each intent recolors the `filled`, `light`\n * and `outline` variants with the same `color-mix()` token approach Alert\n * uses; `filled` keeps the white `--civitai-color-primary-fg` text. These\n * `[data-color][data-variant]` rules out-specify the plain-variant rules\n * above, so order-independence holds.\n */\n &[data-color='info'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-info);\n border-color: var(--civitai-color-info);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-info) 14%, transparent);\n color: var(--civitai-color-info);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-info);\n border-color: var(--civitai-color-info);\n }\n }\n &[data-color='success'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-success);\n border-color: var(--civitai-color-success);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-success) 14%, transparent);\n color: var(--civitai-color-success);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-success);\n border-color: var(--civitai-color-success);\n }\n }\n &[data-color='warning'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-warning);\n border-color: var(--civitai-color-warning);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-warning) 14%, transparent);\n color: var(--civitai-color-warning);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-warning);\n border-color: var(--civitai-color-warning);\n }\n }\n &[data-color='error'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-error);\n border-color: var(--civitai-color-error);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-error) 14%, transparent);\n color: var(--civitai-color-error);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-error);\n border-color: var(--civitai-color-error);\n }\n }\n }\n\n /* ----- Slider ----- */\n /*\n * A themed native <input type=\"range\">: `accent-color` carries the primary\n * tint (native control, so keyboard arrow-key nav + ARIA come for free), plus\n * a full-width track, a token focus ring, and disabled/invalid states. Like\n * checkbox/radio, the range input deliberately does NOT carry\n * `data-civitai-ui-control` (that is the bordered field-input chrome). The\n * label/description/error reuse the shared field markers, laid out in a\n * column; an optional value read-out sits inline with the label in a header.\n */\n [data-civitai-ui='slider'] {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n [data-civitai-ui-slider-header] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n }\n [data-civitai-ui-slider-value] {\n font-size: 13px;\n font-weight: 600;\n color: var(--civitai-color-text-dimmed);\n font-variant-numeric: tabular-nums;\n }\n [data-civitai-ui='slider'] input[type='range'] {\n -webkit-appearance: none;\n appearance: none;\n width: 100%;\n height: 6px;\n margin: 6px 0;\n padding: 0;\n border-radius: 999px;\n accent-color: var(--civitai-color-primary);\n background: var(--civitai-color-track, var(--civitai-color-gray-2));\n cursor: pointer;\n }\n [data-civitai-ui='slider'] input[type='range']:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 4px;\n }\n [data-civitai-ui='slider'] input[type='range']:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n accent-color: var(--civitai-color-gray-5);\n }\n [data-civitai-ui='slider'][data-invalid] input[type='range'] {\n accent-color: var(--civitai-color-error);\n }\n\n /* ----- SegmentedControl / Tabs ----- */\n /*\n * A `role=tablist` of segment buttons (`role=tab`). Presentational chrome\n * only \u2014 the roving-tabindex + arrow-key nav + selection follow-focus live in\n * the React binding (or must be author-provided for hand HTML). The selected\n * segment (`aria-selected='true'`) lifts to the surface color with a soft\n * shadow; unselected segments read dimmed. Paired tab panels use\n * `data-civitai-ui-tabpanel` (role=tabpanel).\n */\n [data-civitai-ui='segmented-control'] {\n display: inline-flex;\n gap: 2px;\n padding: 4px;\n background: var(--civitai-color-segmented-bg, var(--civitai-color-gray-1));\n border-radius: var(--civitai-radius);\n max-width: 100%;\n overflow-x: auto;\n }\n [data-civitai-ui-segment] {\n -webkit-appearance: none;\n appearance: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 0;\n background: transparent;\n color: var(--civitai-color-text-dimmed);\n font-family: var(--civitai-font);\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n cursor: pointer;\n border-radius: calc(var(--civitai-radius) - 1px);\n transition: background-color 120ms ease, color 120ms ease;\n\n /* Documented DEFAULT (MARKUP.md): size=md. On the base rule so a bare\n segment renders at md height, not zero-height. sm/lg override below. */\n height: 30px;\n padding: 0 14px;\n font-size: 13px;\n\n &[data-size='sm'] { height: 26px; padding: 0 10px; font-size: 12px; }\n &[data-size='md'] { height: 30px; padding: 0 14px; font-size: 13px; }\n &[data-size='lg'] { height: 38px; padding: 0 18px; font-size: 15px; }\n\n &:hover:not(:disabled):not([aria-selected='true']):not([aria-checked='true']) {\n color: var(--civitai-color-text);\n }\n /* Selected state \u2014 `aria-selected` in tabs mode (role=tab) OR `aria-checked`\n in toggle mode (role=radio). Same visual treatment for both. */\n &[aria-selected='true'],\n &[aria-checked='true'] {\n background: var(--civitai-color-surface);\n color: var(--civitai-color-text);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);\n }\n &:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n }\n &:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n }\n [data-civitai-ui-tabpanel] {\n color: var(--civitai-color-text);\n\n &:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n }\n &[hidden] { display: none; }\n }\n\n /* ----- Toast ----- */\n /*\n * `toast-region` is the fixed-position `aria-live` host (bottom-right stack).\n * The behavior \u2014 enqueue, auto-dismiss timers, portal \u2014 lives in the React\n * binding's ToastProvider/useToast; the presentational `toast` is a standalone\n * component so hand HTML and React share one visual contract. Each toast is\n * opaque (it sits over content) with an intent-colored left accent, mirroring\n * Alert's `data-color` set.\n */\n [data-civitai-ui='toast-region'] {\n position: fixed;\n bottom: 16px;\n right: 16px;\n z-index: 9999;\n display: flex;\n flex-direction: column;\n gap: 8px;\n width: min(92vw, 380px);\n pointer-events: none;\n }\n [data-civitai-ui='toast'] {\n pointer-events: auto;\n display: flex;\n gap: 10px;\n align-items: flex-start;\n padding: 12px 14px;\n border-radius: var(--civitai-radius);\n background: var(--civitai-color-surface);\n color: var(--civitai-color-text);\n border: 1px solid var(--civitai-color-border);\n border-left: 4px solid var(--civitai-color-border);\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);\n font-size: 14px;\n\n &[data-color='info'] { border-left-color: var(--civitai-color-info); }\n &[data-color='success'] { border-left-color: var(--civitai-color-success); }\n &[data-color='warning'] { border-left-color: var(--civitai-color-warning); }\n &[data-color='error'] { border-left-color: var(--civitai-color-error); }\n\n & [data-civitai-ui-toast-body] {\n flex: 1;\n min-width: 0;\n }\n & [data-civitai-ui-toast-title] {\n font-weight: 600;\n margin-bottom: 2px;\n }\n & [data-civitai-ui-toast-close] {\n background: transparent;\n border: none;\n cursor: pointer;\n color: inherit;\n font-size: 16px;\n line-height: 1;\n padding: 0;\n opacity: 0.7;\n\n &:hover { opacity: 1; }\n }\n }\n\n /* ----- Tooltip ----- */\n /*\n * A hover/focus tooltip: a positioned `role=tooltip` bubble revealed when the\n * wrapper is hovered or contains focus (pure-CSS reveal), or explicitly via\n * `data-open='true'` (the React binding also wires `aria-describedby` +\n * Escape-to-dismiss). The bubble is a dark chip (gray-9) with light text in\n * both themes.\n */\n [data-civitai-ui='tooltip'] {\n position: relative;\n display: inline-flex;\n }\n [data-civitai-ui-tooltip-bubble] {\n position: absolute;\n bottom: calc(100% + 6px);\n left: 50%;\n transform: translateX(-50%);\n z-index: 200;\n max-width: 260px;\n width: max-content;\n padding: 4px 8px;\n border-radius: var(--civitai-radius);\n background: var(--civitai-color-gray-9);\n color: var(--civitai-color-primary-fg);\n font-size: 12px;\n font-weight: 500;\n line-height: 1.4;\n text-align: center;\n pointer-events: none;\n opacity: 0;\n visibility: hidden;\n transition: opacity 120ms ease;\n }\n /*\n * Reveal on hover/focus or an explicit `data-open='true'` \u2014 BUT\n * `data-dismissed='true'` overrides (gates every reveal selector), so the\n * React binding's Escape-to-dismiss can force-HIDE the bubble even while the\n * pointer still hovers or focus is still within. The dismissed flag is cleared\n * on the next hover/focus so the tooltip can re-open normally.\n */\n [data-civitai-ui='tooltip']:hover [data-civitai-ui-tooltip-bubble]:not([data-dismissed='true']),\n [data-civitai-ui='tooltip']:focus-within [data-civitai-ui-tooltip-bubble]:not([data-dismissed='true']),\n [data-civitai-ui-tooltip-bubble][data-open='true']:not([data-dismissed='true']) {\n opacity: 1;\n visibility: visible;\n }\n\n /* ----- Image ----- */\n /*\n * A media container with a token placeholder background (visible while the\n * image loads), object-fit control, and a broken-image fallback. `data-status`\n * (loading|loaded|error) \u2014 set by the React binding's onLoad/onError, or by\n * the author for hand HTML \u2014 fades the <img> in on load and swaps to the\n * fallback overlay on error. Bare markup (no `data-status`) shows the image.\n */\n [data-civitai-ui='image'] {\n position: relative;\n display: block;\n overflow: hidden;\n background: var(--civitai-color-media-placeholder, var(--civitai-color-gray-2));\n border-radius: var(--civitai-radius);\n }\n [data-civitai-ui-image-img] {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n opacity: 1;\n transition: opacity 200ms ease;\n\n &[data-fit='contain'] { object-fit: contain; }\n &[data-fit='cover'] { object-fit: cover; }\n }\n [data-civitai-ui='image'][data-status='loading'] [data-civitai-ui-image-img],\n [data-civitai-ui='image'][data-status='error'] [data-civitai-ui-image-img] {\n opacity: 0;\n }\n [data-civitai-ui-image-fallback] {\n position: absolute;\n inset: 0;\n display: none;\n align-items: center;\n justify-content: center;\n padding: 8px;\n color: var(--civitai-color-text-dimmed);\n font-size: 13px;\n text-align: center;\n }\n [data-civitai-ui='image'][data-status='error'] [data-civitai-ui-image-fallback] {\n display: flex;\n }\n\n /* ----- Text ----- */\n /*\n * The typography primitive. Alone in this sheet it prescribes NO element of\n * its own: the author writes the tag the meaning calls for \u2014 `<h1>`..`<h6>`\n * for a heading, `<p>` for a paragraph, `<span>` for inline \u2014 and this rule\n * sizes and colours whatever that is. Nothing in the visual scale implies a\n * heading level and nothing in a heading level implies a size, which is what\n * lets an `<h2>` be the small caption of a card.\n *\n * `display` is deliberately NOT set, so `<p>`/`<h2>` stay block and `<span>`\n * stays inline: the element's own semantics keep deciding its box.\n *\n * `margin: 0` IS set, and it is a decision rather than a reset for tidiness.\n * The UA gives headings and paragraphs an em-relative margin that moves with\n * every `data-size`, spacing in this pack is owned by `stack`/`group`, and\n * without it `<civitai-text as=\"h2\">` and `<h2 data-civitai-ui=\"text\">` lay\n * out differently \u2014 which is the one thing the two tracks may not do.\n *\n * Documented DEFAULTS (MARKUP.md): size=md, weight=normal, no `data-color`.\n * On the BASE rule so BARE markup renders them, exactly as button and badge\n * do; the explicit `[data-size]`/`[data-weight]` rules below OVERRIDE.\n */\n [data-civitai-ui='text'] {\n margin: 0;\n /*\n * \uD83D\uDD34 `inherit`, NOT `var(--civitai-color-text)` \u2014 see the NO COLOUR AXIS\n * note at the end of this rule. A SPECIFIED value beats an INHERITED one\n * whatever the specificity, so the token here silently cancelled the\n * ancestor route this component's whole colour story rests on.\n */\n color: inherit;\n font-size: 14px;\n font-weight: 400;\n line-height: 1.5;\n\n /*\n * THE TYPE SCALE \u2014 ONE scale, and the reason it runs this far is that the\n * package must not ship two that disagree.\n *\n * The bottom half is the UI ramp: `sm`/`md`/`lg` are byte-identical to\n * Button's own font-size ramp (13/14/16px), so one size name means one size\n * across the pack, and `xs` is the 12px the field description already uses.\n *\n * The top half is the HEADING ramp, and every step of it is a value\n * `utilities.css` already ships as `ci-fs-N` \u2014 so the two are the same\n * scale under two spellings, not two scales:\n *\n * lg 16px = ci-fs-6 2xl 24px = ci-fs-4 4xl 32px = ci-fs-2\n * xl 20px = ci-fs-5 3xl 28px = ci-fs-3 5xl 40px = ci-fs-1\n *\n * What that costs: the t-shirt names do not encode the `ci-fs-N` number,\n * and the two sequences run in OPPOSITE directions (5xl is ci-fs-1), so a\n * reader needs the mapping above. That was the cheaper trade \u2014 naming the\n * new steps `fs-1`..`fs-4` would have put two naming conventions inside one\n * attribute and reversed its direction halfway up. Nothing above `ci-fs-1`\n * is invented here; 40px is the top of both ladders.\n *\n * UNIT: this ramp is px (Button's unit), `ci-fs-*` is rem. They agree at\n * the default 16px root and diverge if a consumer changes it. Deliberate \u2014\n * mixing units within one ramp would make it non-monotonic under a changed\n * root, which is worse than this caveat. Already true of `lg`/`xl` before\n * the ramp was extended.\n *\n * Everything from `xl` up tightens its line-height to 1.25 \u2014 a 20px-plus\n * heading leaded at 1.5 reads as loose. Two leading values in total.\n */\n &[data-size='xs'] { font-size: 12px; }\n &[data-size='sm'] { font-size: 13px; }\n &[data-size='md'] { font-size: 14px; }\n &[data-size='lg'] { font-size: 16px; }\n &[data-size='xl'] { font-size: 20px; line-height: 1.25; }\n &[data-size='2xl'] { font-size: 24px; line-height: 1.25; }\n &[data-size='3xl'] { font-size: 28px; line-height: 1.25; }\n &[data-size='4xl'] { font-size: 32px; line-height: 1.25; }\n &[data-size='5xl'] { font-size: 40px; line-height: 1.25; }\n\n /* `normal` / `semibold` / `bold` are the weights `utilities.css` already\n spells (`ci-normal` 400, `ci-semibold` 600, `ci-bold` 700); `medium` is\n the 500 this sheet already uses for a radio-group and a toast title. */\n &[data-weight='normal'] { font-weight: 400; }\n &[data-weight='medium'] { font-weight: 500; }\n &[data-weight='semibold'] { font-weight: 600; }\n &[data-weight='bold'] { font-weight: 700; }\n\n /*\n * NO COLOUR AXIS, and that is the same predicate this component already\n * applies to alignment and truncation rather than a separate judgement.\n * Every value one would carry already exists as a utility that reaches this\n * element: `ci-muted` (the dimmed token), `ci-text-info` / `-success` /\n * `-warning` / `-error` (the intent enum), `ci-text-default` (the body\n * colour). `color` INHERITS, so a utility on this element or any ancestor\n * reaches the inner element of `<civitai-text>` through its shadow boundary\n * too \u2014 the inner element is `color: inherit` \u2014 which is exactly why a\n * `data-color` here would be a second copy of a predicate that already has\n * an implementation one layer down.\n *\n * \uD83D\uDD34 WHICH IS WHY THE BASE RULE SAYS `color: inherit` AND NOT THE TOKEN.\n * The ancestor half of that sentence was measurably FALSE while this rule\n * specified `var(--civitai-color-text)` \u2014 a *specified* value beats an\n * *inherited* one however far up the utility sits, so `ci-muted` on a wrapper\n * dimmed a plain `<p>` and left `<p data-civitai-ui=\"text\">` undimmed.\n * `ci-text-center` on that same wrapper always worked, because nothing here\n * re-specifies `text-align`. `<civitai-text>`'s `:host` had the identical\n * defect; both tracks changed together, as they must.\n *\n * THE TRADE, and it is NOT limited to a page that paints nothing: Text no\n * longer paints the token itself, so it takes whatever colour it inherits.\n * That decides what it renders on every page that DOES set a colour \u2014 the\n * larger population, and the one every in-repo consumer would be in once it\n * renders Text, which none does today (four starters colour `body` via\n * Tailwind; seven more colour a `[data-theme]` root).\n * Measured on both tracks, on the shape a block here actually has \u2014 a\n * `[data-theme='dark']` root carrying `color: #e6e6e6` \u2014 Text computes\n * `rgb(230, 230, 230)`, where restoring the removed declaration puts both\n * tracks back at the dark token `rgb(193, 194, 197)`. With no colour anywhere\n * it lands on the UA default `rgb(0, 0, 0)`. `ci-text-default` asks for the\n * token back, but it ships in a separate stylesheet that neither\n * `injectStyles()` nor `injectBlocksStyles()` injects \u2014 see the cost section\n * in `src/elements/civitai-text.ts` for every measurement and\n * `packages/civitai-components/MARKUP.md` for what a consumer has to load.\n * Both tracks are pinned in `test/civitai-text.browser.test.ts`.\n *\n * It is also the direction that stays open: this ships `minor` on a\n * published package, so adding the axis later is additive and taking it\n * away would not be.\n */\n }\n}\n\n\n/* ----- Select / Slider field wrappers -----\n The shared -control/-label/-required/-description/-error primitives come from\n @civitai/components (layered). Only the select/slider WRAPPERS live here \u2014 the\n design-system package scopes its field-wrapper rule to the presentational\n inputs (text-input/textarea/number-input). */\n[data-civitai-ui='select'],\n[data-civitai-ui='slider'] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n/* ----- Modal ----- */\n[data-civitai-ui='modal-overlay'] {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.55);\n display: flex;\n align-items: flex-start;\n justify-content: center;\n padding: 32px 16px;\n overflow-y: auto;\n z-index: 1000;\n}\n[data-civitai-ui='modal'] {\n background: var(--civitai-color-surface);\n color: var(--civitai-color-text);\n border: 1px solid var(--civitai-color-border);\n border-radius: var(--civitai-radius);\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);\n width: 100%;\n max-width: 440px;\n outline: none;\n}\n[data-civitai-ui='modal'][data-size='sm'] { max-width: 340px; }\n[data-civitai-ui='modal'][data-size='md'] { max-width: 440px; }\n[data-civitai-ui='modal'][data-size='lg'] { max-width: 620px; }\n[data-civitai-ui='modal'] [data-civitai-ui-modal-header] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 16px 18px;\n border-bottom: 1px solid var(--civitai-color-border);\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-title] {\n font-size: 16px;\n font-weight: 700;\n margin: 0;\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-close] {\n background: transparent;\n border: none;\n cursor: pointer;\n color: var(--civitai-color-text-dimmed);\n font-size: 20px;\n line-height: 1;\n padding: 0;\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-close]:hover {\n color: var(--civitai-color-text);\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-body] {\n padding: 18px;\n}\n\n/* ----- Slider ----- */\n[data-civitai-ui='slider'] [data-civitai-ui-label] {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 8px;\n}\n[data-civitai-ui-slider-value] {\n font-weight: 500;\n color: var(--civitai-color-text-dimmed);\n font-variant-numeric: tabular-nums;\n}\n[data-civitai-ui-range] {\n width: 100%;\n margin: 0;\n accent-color: var(--civitai-color-primary);\n cursor: pointer;\n}\n[data-civitai-ui-range]:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n[data-civitai-ui-range]:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n}\n\n/* ----- Collapse ----- */\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-trigger] {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 6px 0;\n background: transparent;\n border: none;\n font-family: var(--civitai-font);\n font-size: 14px;\n font-weight: 600;\n color: var(--civitai-color-text);\n text-align: left;\n cursor: pointer;\n}\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-trigger]:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-chevron] {\n display: inline-block;\n width: 1em;\n color: var(--civitai-color-text-dimmed);\n}\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-region] {\n padding-top: 4px;\n}\n\n/* ----- ResourceCard -----\n Two variants off ONE box. data-variant='card' stacks (grid tile),\n data-variant='row' runs inline (compact list line). Every selector is\n double-qualified with [data-civitai-ui='resource-card'] on purpose:\n data-variant is also emitted by Button and Badge, so a bare\n [data-variant='card'] would reach across the whole pack.\n NOTE: this block lives inside a JS TEMPLATE LITERAL. No backticks, and no\n dollar-brace, or the string ends here and the file stops parsing as\n TypeScript (backticks in this comment did exactly that once). */\n[data-civitai-ui='resource-card'] {\n display: flex;\n box-sizing: border-box;\n /* The positioned ancestor for the overlay slot. It lives on the ROOT, not on\n the thumbnail frame, so the overlay can be a SIBLING of the hit <button>\n rather than a descendant of it \u2014 see the overlay prop's doc for why that\n distinction is the whole point of the slot. */\n position: relative;\n font-family: var(--civitai-font);\n color: var(--civitai-color-text);\n background: var(--civitai-color-surface);\n border: 1px solid var(--civitai-color-border);\n border-radius: var(--civitai-radius);\n overflow: hidden;\n}\n[data-civitai-ui='resource-card'][data-variant='card'] {\n flex-direction: column;\n align-items: stretch;\n}\n[data-civitai-ui='resource-card'][data-variant='row'] {\n flex-direction: row;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n}\n[data-civitai-ui='resource-card'][data-selected='true'] {\n border-color: var(--civitai-color-primary);\n}\n[data-civitai-ui='resource-card'][data-disabled='true'] {\n opacity: 0.6;\n}\n/* The hit area. A <button> when interactive, a <div> when not \u2014 both are reset\n to the same box so the two variants lay out identically either way. */\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-hit] {\n display: flex;\n /* \uD83D\uDD34 A flex ITEM defaults to min-width:auto, which refuses to shrink below its\n content \u2014 so without this the name's nowrap+ellipsis never engages and a\n long model name blows the card out of its grid cell instead of truncating. */\n min-width: 0;\n box-sizing: border-box;\n margin: 0;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n}\n[data-civitai-ui='resource-card'][data-variant='card'] [data-civitai-ui-resource-hit] {\n flex-direction: column;\n align-items: stretch;\n gap: 6px;\n padding: 6px;\n}\n[data-civitai-ui='resource-card'][data-variant='row'] [data-civitai-ui-resource-hit] {\n flex: 1 1 auto;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n padding: 0;\n}\n[data-civitai-ui='resource-card'] button[data-civitai-ui-resource-hit] {\n cursor: pointer;\n}\n[data-civitai-ui='resource-card'] button[data-civitai-ui-resource-hit]:disabled {\n cursor: not-allowed;\n}\n[data-civitai-ui='resource-card'] button[data-civitai-ui-resource-hit]:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: -2px;\n}\n/* The thumbnail frame. Rendered whether or not there is an image \u2014 see the\n component's thumbnailUrl doc: BlockResourceInfo has no image field, so \"no\n image\" is the COMMON case and the frame must keep its box regardless. */\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-thumb] {\n display: flex;\n align-items: center;\n justify-content: center;\n flex: none;\n overflow: hidden;\n background: var(--civitai-color-surface-2);\n border-radius: calc(var(--civitai-radius) - 1px);\n color: var(--civitai-color-text-dimmed);\n}\n[data-civitai-ui='resource-card'][data-variant='card'] [data-civitai-ui-resource-thumb] {\n /* \uD83D\uDD34 aspect-ratio, not a fixed height: the tile is grid-sized by the caller,\n and this is the line that stops a thumbnail-less card collapsing to a\n text-height sliver. */\n width: 100%;\n aspect-ratio: 1 / 1;\n font-size: 11px;\n}\n[data-civitai-ui='resource-card'][data-variant='row'] [data-civitai-ui-resource-thumb] {\n width: 36px;\n height: 36px;\n font-size: 9px;\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-thumb] img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-placeholder] {\n padding: 0 4px;\n text-align: center;\n line-height: 1.2;\n}\n/* Positioned from the ROOT, over the thumbnail corner. 10px = the hit area's\n 6px padding plus a 4px inset inside the frame; the three move together, and\n the browser tier asserts the result lands inside the frame's own rect rather\n than trusting the arithmetic.\n pointer-events: none is load-bearing, not polish: this slot is STATUS, and a\n pill that can swallow a click meant for the card is the same class of bug as\n nesting a control inside the hit <button>. A consumer who really wants an\n interactive badge opts back in with pointer-events: auto on their own node. */\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-overlay] {\n position: absolute;\n top: 10px;\n right: 10px;\n z-index: 1;\n pointer-events: none;\n display: flex;\n align-items: center;\n gap: 4px;\n max-width: calc(100% - 20px);\n font-size: 11px;\n line-height: 1;\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-nameline] {\n display: flex;\n align-items: center;\n gap: 4px;\n min-width: 0;\n}\n/* The non-colour half of the selected affordance (WCAG 1.4.1). The border-hue\n change on the root is reinforcement; this glyph is what carries it. */\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-selected] {\n flex: none;\n font-size: 12px;\n font-weight: 700;\n line-height: 1;\n color: var(--civitai-color-primary);\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-text] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n flex: 1 1 auto;\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-name] {\n display: block;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.3;\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-meta] {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 4px 6px;\n min-width: 0;\n font-size: 11px;\n color: var(--civitai-color-text-dimmed);\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-actions] {\n display: flex;\n align-items: center;\n flex: none;\n gap: 6px;\n}\n[data-civitai-ui='resource-card'][data-variant='card'] [data-civitai-ui-resource-actions] {\n padding: 0 6px 6px;\n}\n\n/* ----- SegmentedControl ----- */\n[data-civitai-ui='segmented-control'] {\n display: inline-flex;\n flex-direction: row;\n gap: 2px;\n padding: 3px;\n background: var(--civitai-color-surface-2);\n border: 1px solid var(--civitai-color-border);\n border-radius: var(--civitai-radius);\n vertical-align: middle;\n}\n[data-civitai-ui='segmented-control'][data-full-width='true'] {\n display: flex;\n width: 100%;\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment] {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: calc(var(--civitai-radius) - 3px);\n background: transparent;\n color: var(--civitai-color-text-dimmed);\n font-family: var(--civitai-font);\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n cursor: pointer;\n user-select: none;\n transition: background-color 120ms ease, color 120ms ease, box-shadow 120ms ease;\n}\n[data-civitai-ui='segmented-control'][data-full-width='true'] [data-civitai-ui-segment] {\n flex: 1 1 0;\n}\n[data-civitai-ui='segmented-control'][data-size='sm'] [data-civitai-ui-segment] { height: 24px; padding: 0 12px; font-size: 13px; }\n[data-civitai-ui='segmented-control'][data-size='md'] [data-civitai-ui-segment] { height: 30px; padding: 0 16px; font-size: 14px; }\n[data-civitai-ui='segmented-control'][data-size='lg'] [data-civitai-ui-segment] { height: 38px; padding: 0 20px; font-size: 16px; }\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment]:hover:not(:disabled):not([data-active]) {\n color: var(--civitai-color-text);\n background: color-mix(in srgb, var(--civitai-color-text) 6%, transparent);\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment][data-active] {\n background: var(--civitai-color-surface);\n color: var(--civitai-color-primary);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment]:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 1px;\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment]:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n}\n[data-civitai-ui='segmented-control'][data-disabled='true'] {\n opacity: 0.7;\n}\n";
8
+ export declare const BLOCKS_UI_STYLES = "/* AUTOGENERATED by @civitai/theme (scripts/build-tokens.ts). DO NOT EDIT.\n Regenerate: pnpm --filter @civitai/theme build. Guarded by generation-parity test. */\n@property --civitai-color-text {\n syntax: '<color>';\n inherits: true;\n initial-value: #C1C2C5;\n}\n@property --civitai-color-text-dimmed {\n syntax: '<color>';\n inherits: true;\n initial-value: #8c8fa3;\n}\n@property --civitai-color-body {\n syntax: '<color>';\n inherits: true;\n initial-value: #1A1B1E;\n}\n@property --civitai-color-surface {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-surface-2 {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-border {\n syntax: '<color>';\n inherits: true;\n initial-value: #373A40;\n}\n@property --civitai-color-primary {\n syntax: '<color>';\n inherits: true;\n initial-value: #1971C2;\n}\n@property --civitai-color-primary-hover {\n syntax: '<color>';\n inherits: true;\n initial-value: #1864AB;\n}\n@property --civitai-color-primary-fg {\n syntax: '<color>';\n inherits: true;\n initial-value: #fefefe;\n}\n@property --civitai-color-primary-light {\n syntax: '<color>';\n inherits: true;\n initial-value: rgba(34, 139, 230, 0.15);\n}\n@property --civitai-color-error {\n syntax: '<color>';\n inherits: true;\n initial-value: #e03131;\n}\n@property --civitai-color-success {\n syntax: '<color>';\n inherits: true;\n initial-value: #326D5C;\n}\n@property --civitai-color-warning {\n syntax: '<color>';\n inherits: true;\n initial-value: #e8590c;\n}\n@property --civitai-color-info {\n syntax: '<color>';\n inherits: true;\n initial-value: #1971C2;\n}\n@property --civitai-color-gray-0 {\n syntax: '<color>';\n inherits: true;\n initial-value: #f8f9fa;\n}\n@property --civitai-color-gray-1 {\n syntax: '<color>';\n inherits: true;\n initial-value: #f1f3f5;\n}\n@property --civitai-color-gray-2 {\n syntax: '<color>';\n inherits: true;\n initial-value: #e9ecef;\n}\n@property --civitai-color-gray-3 {\n syntax: '<color>';\n inherits: true;\n initial-value: #dee2e6;\n}\n@property --civitai-color-gray-4 {\n syntax: '<color>';\n inherits: true;\n initial-value: #ced4da;\n}\n@property --civitai-color-gray-5 {\n syntax: '<color>';\n inherits: true;\n initial-value: #adb5bd;\n}\n@property --civitai-color-gray-6 {\n syntax: '<color>';\n inherits: true;\n initial-value: #868e96;\n}\n@property --civitai-color-gray-7 {\n syntax: '<color>';\n inherits: true;\n initial-value: #495057;\n}\n@property --civitai-color-gray-8 {\n syntax: '<color>';\n inherits: true;\n initial-value: #343a40;\n}\n@property --civitai-color-gray-9 {\n syntax: '<color>';\n inherits: true;\n initial-value: #212529;\n}\n@property --civitai-color-track {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-segmented-bg {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-media-placeholder {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n\n:root {\n color-scheme: dark;\n --civitai-font: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;\n --civitai-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;\n --civitai-radius: 0.25rem;\n --civitai-color-text: #C1C2C5;\n --civitai-color-text-dimmed: #8c8fa3;\n --civitai-color-body: #1A1B1E;\n --civitai-color-surface: #25262B;\n --civitai-color-surface-2: #25262B;\n --civitai-color-border: #373A40;\n --civitai-color-primary: #1971C2;\n --civitai-color-primary-hover: #1864AB;\n --civitai-color-primary-fg: #fefefe;\n --civitai-color-primary-light: rgba(34, 139, 230, 0.15);\n --civitai-color-error: #e03131;\n --civitai-color-success: #326D5C;\n --civitai-color-warning: #e8590c;\n --civitai-color-info: #1971C2;\n --civitai-color-gray-0: #f8f9fa;\n --civitai-color-gray-1: #f1f3f5;\n --civitai-color-gray-2: #e9ecef;\n --civitai-color-gray-3: #dee2e6;\n --civitai-color-gray-4: #ced4da;\n --civitai-color-gray-5: #adb5bd;\n --civitai-color-gray-6: #868e96;\n --civitai-color-gray-7: #495057;\n --civitai-color-gray-8: #343a40;\n --civitai-color-gray-9: #212529;\n --civitai-bp-xs: 480px;\n --civitai-bp-sm: 768px;\n --civitai-bp-md: 1024px;\n --civitai-bp-lg: 1184px;\n --civitai-bp-xl: 1440px;\n --civitai-card-border-width: 0;\n --civitai-color-track: #25262B;\n --civitai-color-segmented-bg: #25262B;\n --civitai-color-media-placeholder: #25262B;\n}\n\n[data-theme='light'] {\n color-scheme: light;\n --civitai-font: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;\n --civitai-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;\n --civitai-radius: 0.25rem;\n --civitai-color-text: #222;\n --civitai-color-text-dimmed: #868e96;\n --civitai-color-body: #fefefe;\n --civitai-color-surface: #fefefe;\n --civitai-color-surface-2: #fefefe;\n --civitai-color-border: #ced4da;\n --civitai-color-primary: #228BE6;\n --civitai-color-primary-hover: #1C7ED6;\n --civitai-color-primary-fg: #fefefe;\n --civitai-color-primary-light: rgba(34, 139, 230, 0.1);\n --civitai-color-error: #fa5252;\n --civitai-color-success: #299C7A;\n --civitai-color-warning: #fd7e14;\n --civitai-color-info: #228BE6;\n --civitai-color-gray-0: #f8f9fa;\n --civitai-color-gray-1: #f1f3f5;\n --civitai-color-gray-2: #e9ecef;\n --civitai-color-gray-3: #dee2e6;\n --civitai-color-gray-4: #ced4da;\n --civitai-color-gray-5: #adb5bd;\n --civitai-color-gray-6: #868e96;\n --civitai-color-gray-7: #495057;\n --civitai-color-gray-8: #343a40;\n --civitai-color-gray-9: #212529;\n --civitai-bp-xs: 480px;\n --civitai-bp-sm: 768px;\n --civitai-bp-md: 1024px;\n --civitai-bp-lg: 1184px;\n --civitai-bp-xl: 1440px;\n --civitai-card-border-width: 1px;\n --civitai-color-track: #e9ecef;\n --civitai-color-segmented-bg: #f1f3f5;\n --civitai-color-media-placeholder: #e9ecef;\n}\n\n[data-theme='dark'] {\n color-scheme: dark;\n --civitai-color-text: #C1C2C5;\n --civitai-color-text-dimmed: #8c8fa3;\n --civitai-color-body: #1A1B1E;\n --civitai-color-surface: #25262B;\n --civitai-color-surface-2: #25262B;\n --civitai-color-border: #373A40;\n --civitai-color-primary: #1971C2;\n --civitai-color-primary-hover: #1864AB;\n --civitai-color-primary-fg: #fefefe;\n --civitai-color-primary-light: rgba(34, 139, 230, 0.15);\n --civitai-color-error: #e03131;\n --civitai-color-success: #326D5C;\n --civitai-color-warning: #e8590c;\n --civitai-color-info: #1971C2;\n --civitai-card-border-width: 0;\n --civitai-color-track: #25262B;\n --civitai-color-segmented-bg: #25262B;\n --civitai-color-media-placeholder: #25262B;\n}\n\n/*\n * @civitai/components \u2014 attribute-driven, framework-agnostic component CSS.\n *\n * Contract: style is selected by `data-civitai-ui=\"<name>\"` + `data-variant` +\n * `data-size` (+ a few component-specific `data-*`). Theme via an ancestor\n * `[data-theme='light'|'dark']`. Tokens come from @civitai/theme's `--civitai-*`\n * custom properties (link `@civitai/theme/styles.css` alongside this file, or\n * call `injectStyles()` which injects both).\n *\n * ALL rules HERE live in `@layer civitai.components`, so a consumer's own\n * unlayered CSS always wins over THIS SHEET without specificity fights.\n * \uD83D\uDD34 That does NOT extend to the TOKENS. @civitai/theme's sheet carries no\n * layer and declares them at `:root`/`[data-theme='\u2026']` (0-1-0), so a\n * consumer's `:root { --civitai-\u2026: }` only TIES with it and stylesheet order\n * decides \u2014 silently, in the order `useBlocksStyles()` produces. Scope a token\n * override (a class on your root, or an inline style); never `:root`. Measured\n * matrix: MARKUP.md, \"Cascade / overriding\".\n * State colors are derived with `color-mix()` from base tokens (no shade\n * enumeration), and structure uses native CSS nesting (no preprocessor).\n *\n * See MARKUP.md for the full per-component markup + ARIA contract.\n */\n@layer civitai.components {\n [data-civitai-ui] {\n box-sizing: border-box;\n font-family: var(--civitai-font);\n\n & *,\n & *::before,\n & *::after {\n box-sizing: border-box;\n }\n }\n\n /* ----- Button ----- */\n [data-civitai-ui='button'] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n border: 1px solid transparent;\n border-radius: var(--civitai-radius);\n font-family: var(--civitai-font);\n font-weight: 600;\n line-height: 1;\n cursor: pointer;\n user-select: none;\n text-decoration: none;\n transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;\n\n /*\n * Documented DEFAULTS (MARKUP.md): variant=filled, size=md. Applied on the\n * BASE rule (unconditionally) so BARE markup \u2014 a `<button data-civitai-ui=\n * \"button\">` with no `data-variant`/`data-size` \u2014 renders the documented\n * default (filled + md), not an unstyled/zero-size button. The explicit\n * `[data-variant]` / `[data-size]` rules below OVERRIDE (all four variants\n * set their own bg/border/hover, so the base filled default only ever wins\n * for a bare button). Every non-filled variant also sets its own\n * `border-color`, so the base primary border never leaks to them.\n */\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n height: 36px;\n padding: 0 18px;\n font-size: 14px;\n\n &:hover:not(:disabled) {\n background: var(--civitai-color-primary-hover);\n border-color: var(--civitai-color-primary-hover);\n }\n\n &[data-size='sm'] { height: 30px; padding: 0 14px; font-size: 13px; }\n &[data-size='md'] { height: 36px; padding: 0 18px; font-size: 14px; }\n &[data-size='lg'] { height: 44px; padding: 0 22px; font-size: 16px; }\n &[data-full-width='true'] { width: 100%; }\n\n &[data-variant='filled'] {\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n\n &:hover:not(:disabled) {\n background: var(--civitai-color-primary-hover);\n border-color: var(--civitai-color-primary-hover);\n }\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-primary) 12%, transparent);\n color: var(--civitai-color-primary);\n border-color: transparent;\n\n &:hover:not(:disabled) {\n background: color-mix(in srgb, var(--civitai-color-primary) 22%, transparent);\n }\n }\n &[data-variant='outline'] {\n background: transparent;\n color: var(--civitai-color-primary);\n border-color: var(--civitai-color-primary);\n\n &:hover:not(:disabled) {\n background: color-mix(in srgb, var(--civitai-color-primary) 10%, transparent);\n }\n }\n &[data-variant='subtle'] {\n background: transparent;\n color: var(--civitai-color-primary);\n border-color: transparent;\n\n &:hover:not(:disabled) {\n background: color-mix(in srgb, var(--civitai-color-primary) 10%, transparent);\n }\n }\n &:disabled,\n &[aria-busy='true'] {\n opacity: 0.6;\n cursor: not-allowed;\n }\n & [data-civitai-ui-section] {\n display: inline-flex;\n align-items: center;\n }\n }\n\n /* ----- TextInput / Textarea / NumberInput / Select ----- */\n [data-civitai-ui='text-input'],\n [data-civitai-ui='textarea'],\n [data-civitai-ui='number-input'],\n [data-civitai-ui='select'] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n [data-civitai-ui-label] {\n font-size: 14px;\n font-weight: 600;\n color: var(--civitai-color-text);\n }\n [data-civitai-ui-required] {\n color: var(--civitai-color-error);\n margin-left: 2px;\n }\n [data-civitai-ui-description] {\n font-size: 12px;\n color: var(--civitai-color-text-dimmed);\n }\n [data-civitai-ui-error] {\n font-size: 12px;\n color: var(--civitai-color-error);\n }\n [data-civitai-ui-control] {\n width: 100%;\n font-family: var(--civitai-font);\n font-size: 14px;\n color: var(--civitai-color-text);\n background: var(--civitai-color-surface);\n border: 1px solid var(--civitai-color-border);\n border-radius: var(--civitai-radius);\n padding: 8px 12px;\n transition: border-color 120ms ease;\n\n &:focus {\n outline: none;\n border-color: var(--civitai-color-primary);\n }\n &[aria-invalid='true'] {\n border-color: var(--civitai-color-error);\n }\n &:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n }\n textarea[data-civitai-ui-control] {\n resize: vertical;\n line-height: 1.5;\n }\n /*\n * Select (issue #181 F6): a native <select> reusing the shared\n * `-control` field chrome (border/background/radius/focus/invalid, above).\n * Keep the native disclosure caret (most robust + accessible); just make the\n * control feel interactive and reserve room on the right so the caret never\n * overlaps a long option label.\n */\n select[data-civitai-ui-control] {\n cursor: pointer;\n padding-right: 28px;\n line-height: 1.4;\n }\n select[data-civitai-ui-control]:disabled {\n cursor: not-allowed;\n }\n\n /* ----- Checkbox / Radio (issue #181 F6) ----- */\n /*\n * Themed native <input type=\"checkbox\">/<input type=\"radio\">: `accent-color`\n * carries the primary tint (native control, so keyboard/indeterminate/tab all\n * work for free), plus custom sizing, a token focus ring, and disabled state.\n * The box + its inline label sit in a `-choice` row; description + error live\n * below, reusing the shared field markers. The inputs deliberately do NOT\n * carry `data-civitai-ui-control` (that is the full-width field-input chrome).\n */\n [data-civitai-ui='checkbox'],\n [data-civitai-ui='radio'] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n [data-civitai-ui-choice] {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n [data-civitai-ui='checkbox'] [data-civitai-ui-label],\n [data-civitai-ui='radio'] [data-civitai-ui-label] {\n font-weight: 500;\n cursor: pointer;\n user-select: none;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox'],\n [data-civitai-ui='radio'] input[type='radio'] {\n accent-color: var(--civitai-color-primary);\n width: 16px;\n height: 16px;\n margin: 0;\n flex: none;\n cursor: pointer;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox']:focus-visible,\n [data-civitai-ui='radio'] input[type='radio']:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox']:disabled,\n [data-civitai-ui='radio'] input[type='radio']:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox']:disabled ~ [data-civitai-ui-label],\n [data-civitai-ui='radio'] input[type='radio']:disabled ~ [data-civitai-ui-label] {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n /* ----- RadioGroup (issue #181 F6) \u2014 role=radiogroup layout ----- */\n [data-civitai-ui='radio-group'] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n [data-civitai-ui-radio-options] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n\n &[data-orientation='horizontal'] {\n flex-direction: row;\n flex-wrap: wrap;\n gap: 16px;\n }\n }\n /*\n * RadioGroup group-level error (0.2.0): mirrors the field `-error` treatment.\n * The message reuses the shared `[data-civitai-ui-error]` styling (12px, error\n * token); the invalid state (`data-invalid` on the group, set alongside\n * `aria-invalid`) tints the child radios' `accent-color` to the error token so\n * the invalid cue reads on the group, not just the message text \u2014 the group\n * analogue of a field control's error-colored border.\n */\n [data-civitai-ui='radio-group'] [data-civitai-ui-error] {\n font-size: 12px;\n color: var(--civitai-color-error);\n }\n [data-civitai-ui='radio-group'][data-invalid] input[type='radio'] {\n accent-color: var(--civitai-color-error);\n }\n\n /* ----- Card ----- */\n /*\n * F5 (issue #181): light resolves surface and body to the same #fefefe, so a\n * borderless card needs a default hairline to be visible at all; dark already\n * separates them and zeroes the width. `data-with-border` is the stronger,\n * explicit, opaque border and is unconditional.\n */\n [data-civitai-ui='card'] {\n background: var(--civitai-color-surface);\n border-radius: var(--civitai-radius);\n color: var(--civitai-color-text);\n border: var(--civitai-card-border-width, 1px) solid\n color-mix(in srgb, var(--civitai-color-border) 55%, transparent);\n\n &[data-with-border='true'] {\n border: 1px solid var(--civitai-color-border);\n }\n &[data-padding='sm'] { padding: 10px; }\n &[data-padding='md'] { padding: 16px; }\n &[data-padding='lg'] { padding: 24px; }\n }\n\n /* ----- Stack / Group ----- */\n [data-civitai-ui='stack'] {\n display: flex;\n flex-direction: column;\n gap: 12px;\n\n &[data-gap='sm'] { gap: 8px; }\n &[data-gap='md'] { gap: 16px; }\n &[data-gap='lg'] { gap: 24px; }\n }\n [data-civitai-ui='group'] {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n\n /*\n * RESPONSIVE BASE. A group is a ROW, and a row of three or more controls\n * has no escape at phone widths \u2014 a block's slot is ~360px there, so the\n * row simply ran off the edge.\n *\n * \uD83D\uDD34 There are THREE `group` surfaces, and for TWO of them this is a NEW\n * DEFAULT \u2014 do not read it as the CSS merely catching up:\n * - `@civitai/blocks-react`'s `<Group>` defaults `wrap = true` and writes\n * `flex-wrap` as an INLINE style, so its consumers already wrapped.\n * For that one, and only that one, this is the CSS agreeing with a\n * default that was already shipping.\n * - `@civitai/components-react`'s `<Group>` writes NO inline style and has\n * NO `wrap` prop, so it resolves against this rule \u2014 a React consumer\n * that did NOT wrap before. \uD83D\uDD34 THIS SURFACE NO LONGER EXISTS: see below.\n * - bare `data-civitai-ui=\"group\"` markup, the framework-agnostic contract\n * this package exists to serve \u2014 likewise.\n * Nothing could see the disagreement, because each surface was only ever\n * tested against itself. TWO tests now pin it \u2014 there is no single \"the\n * parity test\":\n * - `civitai-blocks-react/test/Group.test.tsx` reads the wrap default off\n * ITS `<Group>` and compares it to this rule, so flipping either alone\n * fails;\n * - `civitai-components/test/presentational-parity.browser.test.ts`,\n * case **`group`** \u2014 compares `<civitai-group>` against the bare\n * `data-civitai-ui=\"group\"` markup with `flexWrap` among the properties\n * it reads. \uD83D\uDD34 It is `group`, NOT `group/nowrap`: that case sets\n * `nowrap` on BOTH arms, so both resolve to `flex-wrap: nowrap` and it\n * stays green through a deletion of the rule below. Mutate the rule and\n * check the wrong case and you will conclude it is unguarded.\n *\n * \uD83D\uDD34 THE MIDDLE SURFACE IS GONE, and so is the test that used to pin it.\n * `@civitai/components-react@0.9.0` deleted its hand-written `<Group>` when\n * the custom elements superseded the React layer, taking\n * `html-vs-react-parity.browser.test.tsx` (\"styling anchors \u2014 Group\") with\n * it. TWO surfaces now resolve against this rule, not three: bare markup,\n * and blocks-react's `<Group>`. `<civitai-group>` is a third surface that\n * does NOT read this sheet at all \u2014 shadow DOM, carrying its own\n * `flex-wrap` at `src/elements/civitai-group.ts:20`.\n *\n * That independence is why the replacement is not the weak kind of parity\n * check this comment used to warn about. The warning was right about the\n * OLD test: both its arms consumed THIS sheet, so deleting the rule moved\n * them identically and only an absolute anchor could see it.\n * `presentational-parity`'s arms are independent IMPLEMENTATIONS, so\n * deleting `flex-wrap` here moves the legacy arm alone and the comparison\n * fails. WATCHED, not inferred: deleting `flex-wrap: wrap` from this block\n * in `styles.generated.ts` (the string the browser test injects) fails\n * `light / group` and `dark / group` with\n * `flexWrap: expected 'wrap' to be 'nowrap'` \u2014 2 failed / 108 passed;\n * restoring it returns 110/110. (Re-measured after #477 added 21 Text\n * parity cases; the kill is what matters, the totals move with the file.)\n *\n * \uD83D\uDD34 The three-surface distinction is load-bearing for the RELEASE, not just\n * the prose: \"existing callers notice\" is what `RELEASING.md` reserves\n * `major` for, and it is true for two of the three surfaces. Shipped as a\n * MINOR by maintainer decision \u2014 see the changeset for the reasoning and for\n * the upgrade note that decision makes load-bearing.\n *\n * Opt out with `data-nowrap=\"true\"`. An inline `flex-wrap` also still\n * outranks this layer, so existing markup that already sets it is untouched.\n */\n flex-wrap: wrap;\n\n /*\n * A flex item defaults to `min-width: auto`, which refuses to shrink below\n * its CONTENT width \u2014 so one long label (a model name, a prompt) pushes the\n * row past the slot even WITH wrap. `min-width: 0` lets it shrink.\n *\n * \uD83D\uDD34 SCOPE, measured: this only matters for a child whose computed\n * `overflow` is `visible`. Per CSS Flexbox \u00A74.5 an item with any other\n * `overflow` ALREADY has an automatic minimum size of 0 \u2014 so a child that\n * sets `overflow: hidden`, as any `text-overflow: ellipsis` child must, is\n * unaffected by this line. An earlier version of this comment claimed\n * ellipsis \"can never engage\" without it, which was exactly backwards; the\n * browser guard built on that belief used an ellipsis fixture and therefore\n * passed with this rule DELETED. Both are fixed \u2014 see\n * `civitai-blocks-react/test/responsive-group.browser.test.tsx`.\n *\n * Specificity is (0,1,0), not zero: `&` carries the parent selector's, and\n * `:where(*)` is specificity-identical to `*`, so that wrapper bought\n * nothing and has been dropped. What actually lets a CONSUMER's own rule\n * win is `@layer civitai.components`, which every rule in this file sits\n * inside \u2014 unlayered author CSS beats it regardless of specificity. Note\n * the limit: another rule INSIDE this layer does not get that protection,\n * so a future non-zero `min-width` on a nested component here must be more\n * specific or come later.\n */\n & > * { min-width: 0; }\n\n &[data-nowrap='true'] { flex-wrap: nowrap; }\n\n &[data-gap='sm'] { gap: 6px; }\n &[data-gap='md'] { gap: 16px; }\n &[data-gap='lg'] { gap: 24px; }\n }\n\n /* ----- Alert ----- */\n [data-civitai-ui='alert'] {\n display: flex;\n gap: 10px;\n align-items: flex-start;\n padding: 12px 14px;\n border-radius: var(--civitai-radius);\n border: 1px solid transparent;\n font-size: 14px;\n color: var(--civitai-color-text);\n\n /*\n * Documented DEFAULT (MARKUP.md): color=info (default intent). Applied on\n * the base rule so a BARE alert \u2014 `<div data-civitai-ui=\"alert\">` with no\n * `data-color` \u2014 renders the info intent (tinted bg + border), not just the\n * neutral chrome. The explicit `[data-color]` rules below OVERRIDE (success\n * / warning / error each set both bg + border-color), so the info default\n * only ever wins when `data-color` is omitted.\n */\n background: color-mix(in srgb, var(--civitai-color-info) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-info) 35%, transparent);\n\n &[data-color='info'] {\n background: color-mix(in srgb, var(--civitai-color-info) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-info) 35%, transparent);\n }\n &[data-color='success'] {\n background: color-mix(in srgb, var(--civitai-color-success) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-success) 35%, transparent);\n }\n &[data-color='warning'] {\n background: color-mix(in srgb, var(--civitai-color-warning) 14%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-warning) 35%, transparent);\n }\n &[data-color='error'] {\n background: color-mix(in srgb, var(--civitai-color-error) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-error) 35%, transparent);\n }\n & [data-civitai-ui-alert-body] {\n flex: 1;\n min-width: 0;\n }\n & [data-civitai-ui-alert-title] {\n font-weight: 600;\n margin-bottom: 2px;\n }\n & [data-civitai-ui-alert-close] {\n background: transparent;\n border: none;\n cursor: pointer;\n color: inherit;\n font-size: 16px;\n line-height: 1;\n padding: 0;\n opacity: 0.7;\n\n &:hover { opacity: 1; }\n }\n }\n\n /* ----- Loader ----- */\n [data-civitai-ui='loader'] {\n display: inline-block;\n border-radius: 50%;\n border-style: solid;\n border-color: color-mix(in srgb, currentColor 25%, transparent);\n border-top-color: currentColor;\n color: var(--civitai-color-primary);\n animation: civitai-ui-spin 0.7s linear infinite;\n\n /*\n * Documented DEFAULT (MARKUP.md): size=md. Applied on the base rule so a\n * BARE loader \u2014 `<span data-civitai-ui=\"loader\">` with no `data-size` \u2014 has\n * real dimensions (otherwise 0\u00D70 and invisible). sm/lg OVERRIDE below.\n */\n width: 22px;\n height: 22px;\n border-width: 3px;\n\n &[data-size='sm'] { width: 16px; height: 16px; border-width: 2px; }\n &[data-size='md'] { width: 22px; height: 22px; border-width: 3px; }\n &[data-size='lg'] { width: 32px; height: 32px; border-width: 4px; }\n }\n [data-civitai-ui='button'] [data-civitai-ui='loader'] {\n color: currentColor;\n }\n @keyframes civitai-ui-spin {\n to { transform: rotate(360deg); }\n }\n\n /* ----- Badge ----- */\n [data-civitai-ui='badge'] {\n display: inline-flex;\n align-items: center;\n border: 1px solid transparent;\n border-radius: 999px;\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n text-transform: uppercase;\n letter-spacing: 0.02em;\n\n /*\n * Documented DEFAULTS (MARKUP.md): variant=filled, size=md. Applied on the\n * base rule so a BARE badge \u2014 `<span data-civitai-ui=\"badge\">` with no\n * `data-variant`/`data-size` (MARKUP's own minimal example) \u2014 renders\n * filled + md (padding + primary fill), not an unstyled, zero-padding pill.\n * The explicit `[data-variant]`/`[data-size]` rules below OVERRIDE. Because\n * the base now carries a primary `border-color`, the `light` variant (which\n * previously relied on the base transparent border) explicitly resets it to\n * transparent so light badges are visually unchanged.\n */\n height: 22px;\n padding: 0 10px;\n font-size: 11px;\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n\n &[data-size='sm'] { height: 18px; padding: 0 8px; font-size: 10px; }\n &[data-size='md'] { height: 22px; padding: 0 10px; font-size: 11px; }\n &[data-size='lg'] { height: 26px; padding: 0 12px; font-size: 13px; }\n &[data-variant='filled'] {\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-primary) 14%, transparent);\n color: var(--civitai-color-primary);\n border-color: transparent;\n }\n &[data-variant='outline'] {\n background: transparent;\n color: var(--civitai-color-primary);\n border-color: var(--civitai-color-primary);\n }\n\n /*\n * Intent color via `data-color`, mirroring Alert's `data-color` contract\n * (info / success / warning / error). Absent `data-color` => the default\n * primary above (non-breaking). Each intent recolors the `filled`, `light`\n * and `outline` variants with the same `color-mix()` token approach Alert\n * uses; `filled` keeps the white `--civitai-color-primary-fg` text. These\n * `[data-color][data-variant]` rules out-specify the plain-variant rules\n * above, so order-independence holds.\n */\n &[data-color='info'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-info);\n border-color: var(--civitai-color-info);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-info) 14%, transparent);\n color: var(--civitai-color-info);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-info);\n border-color: var(--civitai-color-info);\n }\n }\n &[data-color='success'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-success);\n border-color: var(--civitai-color-success);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-success) 14%, transparent);\n color: var(--civitai-color-success);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-success);\n border-color: var(--civitai-color-success);\n }\n }\n &[data-color='warning'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-warning);\n border-color: var(--civitai-color-warning);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-warning) 14%, transparent);\n color: var(--civitai-color-warning);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-warning);\n border-color: var(--civitai-color-warning);\n }\n }\n &[data-color='error'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-error);\n border-color: var(--civitai-color-error);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-error) 14%, transparent);\n color: var(--civitai-color-error);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-error);\n border-color: var(--civitai-color-error);\n }\n }\n }\n\n /* ----- Slider ----- */\n /*\n * A themed native <input type=\"range\">: `accent-color` carries the primary\n * tint (native control, so keyboard arrow-key nav + ARIA come for free), plus\n * a full-width track, a token focus ring, and disabled/invalid states. Like\n * checkbox/radio, the range input deliberately does NOT carry\n * `data-civitai-ui-control` (that is the bordered field-input chrome). The\n * label/description/error reuse the shared field markers, laid out in a\n * column; an optional value read-out sits inline with the label in a header.\n */\n [data-civitai-ui='slider'] {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n [data-civitai-ui-slider-header] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n }\n [data-civitai-ui-slider-value] {\n font-size: 13px;\n font-weight: 600;\n color: var(--civitai-color-text-dimmed);\n font-variant-numeric: tabular-nums;\n }\n [data-civitai-ui='slider'] input[type='range'] {\n -webkit-appearance: none;\n appearance: none;\n width: 100%;\n height: 6px;\n margin: 6px 0;\n padding: 0;\n border-radius: 999px;\n accent-color: var(--civitai-color-primary);\n background: var(--civitai-color-track, var(--civitai-color-gray-2));\n cursor: pointer;\n }\n [data-civitai-ui='slider'] input[type='range']:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 4px;\n }\n [data-civitai-ui='slider'] input[type='range']:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n accent-color: var(--civitai-color-gray-5);\n }\n [data-civitai-ui='slider'][data-invalid] input[type='range'] {\n accent-color: var(--civitai-color-error);\n }\n\n /* ----- SegmentedControl / Tabs ----- */\n /*\n * A `role=tablist` of segment buttons (`role=tab`). Presentational chrome\n * only \u2014 the roving-tabindex + arrow-key nav + selection follow-focus live in\n * the React binding (or must be author-provided for hand HTML). The selected\n * segment (`aria-selected='true'`) lifts to the surface color with a soft\n * shadow; unselected segments read dimmed. Paired tab panels use\n * `data-civitai-ui-tabpanel` (role=tabpanel).\n */\n [data-civitai-ui='segmented-control'] {\n display: inline-flex;\n gap: 2px;\n padding: 4px;\n background: var(--civitai-color-segmented-bg, var(--civitai-color-gray-1));\n border-radius: var(--civitai-radius);\n max-width: 100%;\n overflow-x: auto;\n }\n [data-civitai-ui-segment] {\n -webkit-appearance: none;\n appearance: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 0;\n background: transparent;\n color: var(--civitai-color-text-dimmed);\n font-family: var(--civitai-font);\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n cursor: pointer;\n border-radius: calc(var(--civitai-radius) - 1px);\n transition: background-color 120ms ease, color 120ms ease;\n\n /* Documented DEFAULT (MARKUP.md): size=md. On the base rule so a bare\n segment renders at md height, not zero-height. sm/lg override below. */\n height: 30px;\n padding: 0 14px;\n font-size: 13px;\n\n &[data-size='sm'] { height: 26px; padding: 0 10px; font-size: 12px; }\n &[data-size='md'] { height: 30px; padding: 0 14px; font-size: 13px; }\n &[data-size='lg'] { height: 38px; padding: 0 18px; font-size: 15px; }\n\n &:hover:not(:disabled):not([aria-selected='true']):not([aria-checked='true']) {\n color: var(--civitai-color-text);\n }\n /* Selected state \u2014 `aria-selected` in tabs mode (role=tab) OR `aria-checked`\n in toggle mode (role=radio). Same visual treatment for both. */\n &[aria-selected='true'],\n &[aria-checked='true'] {\n background: var(--civitai-color-surface);\n color: var(--civitai-color-text);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);\n }\n &:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n }\n &:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n }\n [data-civitai-ui-tabpanel] {\n color: var(--civitai-color-text);\n\n &:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n }\n &[hidden] { display: none; }\n }\n\n /* ----- Toast ----- */\n /*\n * `toast-region` is the fixed-position `aria-live` host (bottom-right stack).\n * The behavior \u2014 enqueue, auto-dismiss timers, portal \u2014 lives in the React\n * binding's ToastProvider/useToast; the presentational `toast` is a standalone\n * component so hand HTML and React share one visual contract. Each toast is\n * opaque (it sits over content) with an intent-colored left accent, mirroring\n * Alert's `data-color` set.\n */\n [data-civitai-ui='toast-region'] {\n position: fixed;\n bottom: 16px;\n right: 16px;\n z-index: 9999;\n display: flex;\n flex-direction: column;\n gap: 8px;\n width: min(92vw, 380px);\n pointer-events: none;\n }\n [data-civitai-ui='toast'] {\n pointer-events: auto;\n display: flex;\n gap: 10px;\n align-items: flex-start;\n padding: 12px 14px;\n border-radius: var(--civitai-radius);\n background: var(--civitai-color-surface);\n color: var(--civitai-color-text);\n border: 1px solid var(--civitai-color-border);\n border-left: 4px solid var(--civitai-color-border);\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);\n font-size: 14px;\n\n &[data-color='info'] { border-left-color: var(--civitai-color-info); }\n &[data-color='success'] { border-left-color: var(--civitai-color-success); }\n &[data-color='warning'] { border-left-color: var(--civitai-color-warning); }\n &[data-color='error'] { border-left-color: var(--civitai-color-error); }\n\n & [data-civitai-ui-toast-body] {\n flex: 1;\n min-width: 0;\n }\n & [data-civitai-ui-toast-title] {\n font-weight: 600;\n margin-bottom: 2px;\n }\n & [data-civitai-ui-toast-close] {\n background: transparent;\n border: none;\n cursor: pointer;\n color: inherit;\n font-size: 16px;\n line-height: 1;\n padding: 0;\n opacity: 0.7;\n\n &:hover { opacity: 1; }\n }\n }\n\n /* ----- Tooltip ----- */\n /*\n * A hover/focus tooltip: a positioned `role=tooltip` bubble revealed when the\n * wrapper is hovered or contains focus (pure-CSS reveal), or explicitly via\n * `data-open='true'` (the React binding also wires `aria-describedby` +\n * Escape-to-dismiss). The bubble is a dark chip (gray-9) with light text in\n * both themes.\n */\n [data-civitai-ui='tooltip'] {\n position: relative;\n display: inline-flex;\n }\n [data-civitai-ui-tooltip-bubble] {\n position: absolute;\n bottom: calc(100% + 6px);\n left: 50%;\n transform: translateX(-50%);\n z-index: 200;\n max-width: 260px;\n width: max-content;\n padding: 4px 8px;\n border-radius: var(--civitai-radius);\n background: var(--civitai-color-gray-9);\n color: var(--civitai-color-primary-fg);\n font-size: 12px;\n font-weight: 500;\n line-height: 1.4;\n text-align: center;\n pointer-events: none;\n opacity: 0;\n visibility: hidden;\n transition: opacity 120ms ease;\n }\n /*\n * Reveal on hover/focus or an explicit `data-open='true'` \u2014 BUT\n * `data-dismissed='true'` overrides (gates every reveal selector), so the\n * React binding's Escape-to-dismiss can force-HIDE the bubble even while the\n * pointer still hovers or focus is still within. The dismissed flag is cleared\n * on the next hover/focus so the tooltip can re-open normally.\n */\n [data-civitai-ui='tooltip']:hover [data-civitai-ui-tooltip-bubble]:not([data-dismissed='true']),\n [data-civitai-ui='tooltip']:focus-within [data-civitai-ui-tooltip-bubble]:not([data-dismissed='true']),\n [data-civitai-ui-tooltip-bubble][data-open='true']:not([data-dismissed='true']) {\n opacity: 1;\n visibility: visible;\n }\n\n /* ----- Image ----- */\n /*\n * A media container with a token placeholder background (visible while the\n * image loads), object-fit control, and a broken-image fallback. `data-status`\n * (loading|loaded|error) \u2014 set by the React binding's onLoad/onError, or by\n * the author for hand HTML \u2014 fades the <img> in on load and swaps to the\n * fallback overlay on error. Bare markup (no `data-status`) shows the image.\n */\n [data-civitai-ui='image'] {\n position: relative;\n display: block;\n overflow: hidden;\n background: var(--civitai-color-media-placeholder, var(--civitai-color-gray-2));\n border-radius: var(--civitai-radius);\n }\n [data-civitai-ui-image-img] {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n opacity: 1;\n transition: opacity 200ms ease;\n\n &[data-fit='contain'] { object-fit: contain; }\n &[data-fit='cover'] { object-fit: cover; }\n }\n [data-civitai-ui='image'][data-status='loading'] [data-civitai-ui-image-img],\n [data-civitai-ui='image'][data-status='error'] [data-civitai-ui-image-img] {\n opacity: 0;\n }\n [data-civitai-ui-image-fallback] {\n position: absolute;\n inset: 0;\n display: none;\n align-items: center;\n justify-content: center;\n padding: 8px;\n color: var(--civitai-color-text-dimmed);\n font-size: 13px;\n text-align: center;\n }\n [data-civitai-ui='image'][data-status='error'] [data-civitai-ui-image-fallback] {\n display: flex;\n }\n\n /* ----- Text ----- */\n /*\n * The typography primitive. Alone in this sheet it prescribes NO element of\n * its own: the author writes the tag the meaning calls for \u2014 `<h1>`..`<h6>`\n * for a heading, `<p>` for a paragraph, `<span>` for inline \u2014 and this rule\n * sizes and colours whatever that is. Nothing in the visual scale implies a\n * heading level and nothing in a heading level implies a size, which is what\n * lets an `<h2>` be the small caption of a card.\n *\n * `display` is deliberately NOT set, so `<p>`/`<h2>` stay block and `<span>`\n * stays inline: the element's own semantics keep deciding its box.\n *\n * `margin: 0` IS set, and it is a decision rather than a reset for tidiness.\n * The UA gives headings and paragraphs an em-relative margin that moves with\n * every `data-size`, spacing in this pack is owned by `stack`/`group`, and\n * without it `<civitai-text as=\"h2\">` and `<h2 data-civitai-ui=\"text\">` lay\n * out differently \u2014 which is the one thing the two tracks may not do.\n *\n * Documented DEFAULTS (MARKUP.md): size=md, weight=normal, no `data-color`.\n * On the BASE rule so BARE markup renders them, exactly as button and badge\n * do; the explicit `[data-size]`/`[data-weight]` rules below OVERRIDE.\n */\n [data-civitai-ui='text'] {\n margin: 0;\n /*\n * \uD83D\uDD34 `inherit`, NOT `var(--civitai-color-text)` \u2014 see the NO COLOUR AXIS\n * note at the end of this rule. A SPECIFIED value beats an INHERITED one\n * whatever the specificity, so the token here silently cancelled the\n * ancestor route this component's whole colour story rests on.\n */\n color: inherit;\n font-size: 14px;\n font-weight: 400;\n line-height: 1.5;\n\n /*\n * THE TYPE SCALE \u2014 ONE scale, and the reason it runs this far is that the\n * package must not ship two that disagree.\n *\n * The bottom half is the UI ramp: `sm`/`md`/`lg` are byte-identical to\n * Button's own font-size ramp (13/14/16px), so one size name means one size\n * across the pack, and `xs` is the 12px the field description already uses.\n *\n * The top half is the HEADING ramp, and every step of it is a value\n * `utilities.css` already ships as `ci-fs-N` \u2014 so the two are the same\n * scale under two spellings, not two scales:\n *\n * lg 16px = ci-fs-6 2xl 24px = ci-fs-4 4xl 32px = ci-fs-2\n * xl 20px = ci-fs-5 3xl 28px = ci-fs-3 5xl 40px = ci-fs-1\n *\n * What that costs: the t-shirt names do not encode the `ci-fs-N` number,\n * and the two sequences run in OPPOSITE directions (5xl is ci-fs-1), so a\n * reader needs the mapping above. That was the cheaper trade \u2014 naming the\n * new steps `fs-1`..`fs-4` would have put two naming conventions inside one\n * attribute and reversed its direction halfway up. Nothing above `ci-fs-1`\n * is invented here; 40px is the top of both ladders.\n *\n * UNIT: this ramp is px (Button's unit), `ci-fs-*` is rem. They agree at\n * the default 16px root and diverge if a consumer changes it. Deliberate \u2014\n * mixing units within one ramp would make it non-monotonic under a changed\n * root, which is worse than this caveat. Already true of `lg`/`xl` before\n * the ramp was extended.\n *\n * Everything from `xl` up tightens its line-height to 1.25 \u2014 a 20px-plus\n * heading leaded at 1.5 reads as loose. Two leading values in total.\n */\n &[data-size='xs'] { font-size: 12px; }\n &[data-size='sm'] { font-size: 13px; }\n &[data-size='md'] { font-size: 14px; }\n &[data-size='lg'] { font-size: 16px; }\n &[data-size='xl'] { font-size: 20px; line-height: 1.25; }\n &[data-size='2xl'] { font-size: 24px; line-height: 1.25; }\n &[data-size='3xl'] { font-size: 28px; line-height: 1.25; }\n &[data-size='4xl'] { font-size: 32px; line-height: 1.25; }\n &[data-size='5xl'] { font-size: 40px; line-height: 1.25; }\n\n /* `normal` / `semibold` / `bold` are the weights `utilities.css` already\n spells (`ci-normal` 400, `ci-semibold` 600, `ci-bold` 700); `medium` is\n the 500 this sheet already uses for a radio-group and a toast title. */\n &[data-weight='normal'] { font-weight: 400; }\n &[data-weight='medium'] { font-weight: 500; }\n &[data-weight='semibold'] { font-weight: 600; }\n &[data-weight='bold'] { font-weight: 700; }\n\n /*\n * NO COLOUR AXIS, and that is the same predicate this component already\n * applies to alignment and truncation rather than a separate judgement.\n * Every value one would carry already exists as a utility that reaches this\n * element: `ci-muted` (the dimmed token), `ci-text-info` / `-success` /\n * `-warning` / `-error` (the intent enum), `ci-text-default` (the body\n * colour). `color` INHERITS, so a utility on this element or any ancestor\n * reaches the inner element of `<civitai-text>` through its shadow boundary\n * too \u2014 the inner element is `color: inherit` \u2014 which is exactly why a\n * `data-color` here would be a second copy of a predicate that already has\n * an implementation one layer down.\n *\n * \uD83D\uDD34 WHICH IS WHY THE BASE RULE SAYS `color: inherit` AND NOT THE TOKEN.\n * The ancestor half of that sentence was measurably FALSE while this rule\n * specified `var(--civitai-color-text)` \u2014 a *specified* value beats an\n * *inherited* one however far up the utility sits, so `ci-muted` on a wrapper\n * dimmed a plain `<p>` and left `<p data-civitai-ui=\"text\">` undimmed.\n * `ci-text-center` on that same wrapper always worked, because nothing here\n * re-specifies `text-align`. `<civitai-text>`'s `:host` had the identical\n * defect; both tracks changed together, as they must.\n *\n * THE TRADE, and it is NOT limited to a page that paints nothing: Text no\n * longer paints the token itself, so it takes whatever colour it inherits.\n * That decides what it renders on every page that DOES set a colour \u2014 the\n * larger population. Measured on both tracks, on a `[data-theme='dark']`\n * root carrying `color: #e6e6e6` (the shape this repo's blocks had when this\n * was measured; none sets that colour today): Text computes\n * `rgb(230, 230, 230)`, where restoring the removed declaration puts both\n * tracks back at the dark token `rgb(193, 194, 197)`. With no colour anywhere\n * it lands on the UA default `rgb(0, 0, 0)`.\n * In this repo: the Tailwind starters colour `body` themselves, so they\n * would show Text in their own colour once one of them renders it. Every\n * Civitai App page sets `color: var(--civitai-color-text)` itself \u2014 each\n * example's src/index.css on `:root`, the block starter's on `body` \u2014 so\n * Text there shows the token by INHERITING it rather than by naming it.\n * (See `src/elements/civitai-text.ts` for the full enumeration.)\n * `ci-text-default` asks for the\n * token back, but it ships in a separate stylesheet that neither\n * `injectStyles()` nor `injectBlocksStyles()` injects \u2014 see the cost section\n * in `src/elements/civitai-text.ts` for every measurement and\n * `packages/civitai-components/MARKUP.md` for what a consumer has to load.\n * Both tracks are pinned in `test/civitai-text.browser.test.ts`.\n *\n * It is also the direction that stays open: this ships `minor` on a\n * published package, so adding the axis later is additive and taking it\n * away would not be.\n */\n }\n}\n\n\n/* ----- Select / Slider field wrappers -----\n The shared -control/-label/-required/-description/-error primitives come from\n @civitai/components (layered). Only the select/slider WRAPPERS live here \u2014 the\n design-system package scopes its field-wrapper rule to the presentational\n inputs (text-input/textarea/number-input). */\n[data-civitai-ui='select'],\n[data-civitai-ui='slider'] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n/* ----- Modal ----- */\n[data-civitai-ui='modal-overlay'] {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.55);\n display: flex;\n align-items: flex-start;\n justify-content: center;\n padding: 32px 16px;\n overflow-y: auto;\n z-index: 1000;\n}\n[data-civitai-ui='modal'] {\n background: var(--civitai-color-surface);\n color: var(--civitai-color-text);\n border: 1px solid var(--civitai-color-border);\n border-radius: var(--civitai-radius);\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);\n width: 100%;\n max-width: 440px;\n outline: none;\n}\n[data-civitai-ui='modal'][data-size='sm'] { max-width: 340px; }\n[data-civitai-ui='modal'][data-size='md'] { max-width: 440px; }\n[data-civitai-ui='modal'][data-size='lg'] { max-width: 620px; }\n[data-civitai-ui='modal'] [data-civitai-ui-modal-header] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 16px 18px;\n border-bottom: 1px solid var(--civitai-color-border);\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-title] {\n font-size: 16px;\n font-weight: 700;\n margin: 0;\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-close] {\n background: transparent;\n border: none;\n cursor: pointer;\n color: var(--civitai-color-text-dimmed);\n font-size: 20px;\n line-height: 1;\n padding: 0;\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-close]:hover {\n color: var(--civitai-color-text);\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-body] {\n padding: 18px;\n}\n\n/* ----- Slider ----- */\n[data-civitai-ui='slider'] [data-civitai-ui-label] {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 8px;\n}\n[data-civitai-ui-slider-value] {\n font-weight: 500;\n color: var(--civitai-color-text-dimmed);\n font-variant-numeric: tabular-nums;\n}\n[data-civitai-ui-range] {\n width: 100%;\n margin: 0;\n accent-color: var(--civitai-color-primary);\n cursor: pointer;\n}\n[data-civitai-ui-range]:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n[data-civitai-ui-range]:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n}\n\n/* ----- Collapse ----- */\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-trigger] {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 6px 0;\n background: transparent;\n border: none;\n font-family: var(--civitai-font);\n font-size: 14px;\n font-weight: 600;\n color: var(--civitai-color-text);\n text-align: left;\n cursor: pointer;\n}\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-trigger]:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-chevron] {\n display: inline-block;\n width: 1em;\n color: var(--civitai-color-text-dimmed);\n}\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-region] {\n padding-top: 4px;\n}\n\n/* ----- ResourceCard -----\n Two variants off ONE box. data-variant='card' stacks (grid tile),\n data-variant='row' runs inline (compact list line). Every selector is\n double-qualified with [data-civitai-ui='resource-card'] on purpose:\n data-variant is also emitted by Button and Badge, so a bare\n [data-variant='card'] would reach across the whole pack.\n NOTE: this block lives inside a JS TEMPLATE LITERAL. No backticks, and no\n dollar-brace, or the string ends here and the file stops parsing as\n TypeScript (backticks in this comment did exactly that once). */\n[data-civitai-ui='resource-card'] {\n display: flex;\n box-sizing: border-box;\n /* The positioned ancestor for the overlay slot. It lives on the ROOT, not on\n the thumbnail frame, so the overlay can be a SIBLING of the hit <button>\n rather than a descendant of it \u2014 see the overlay prop's doc for why that\n distinction is the whole point of the slot. */\n position: relative;\n font-family: var(--civitai-font);\n color: var(--civitai-color-text);\n background: var(--civitai-color-surface);\n border: 1px solid var(--civitai-color-border);\n border-radius: var(--civitai-radius);\n overflow: hidden;\n}\n[data-civitai-ui='resource-card'][data-variant='card'] {\n flex-direction: column;\n align-items: stretch;\n}\n[data-civitai-ui='resource-card'][data-variant='row'] {\n flex-direction: row;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n}\n[data-civitai-ui='resource-card'][data-selected='true'] {\n border-color: var(--civitai-color-primary);\n}\n[data-civitai-ui='resource-card'][data-disabled='true'] {\n opacity: 0.6;\n}\n/* The hit area. A <button> when interactive, a <div> when not \u2014 both are reset\n to the same box so the two variants lay out identically either way. */\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-hit] {\n display: flex;\n /* \uD83D\uDD34 A flex ITEM defaults to min-width:auto, which refuses to shrink below its\n content \u2014 so without this the name's nowrap+ellipsis never engages and a\n long model name blows the card out of its grid cell instead of truncating. */\n min-width: 0;\n box-sizing: border-box;\n margin: 0;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n}\n[data-civitai-ui='resource-card'][data-variant='card'] [data-civitai-ui-resource-hit] {\n flex-direction: column;\n align-items: stretch;\n gap: 6px;\n padding: 6px;\n}\n[data-civitai-ui='resource-card'][data-variant='row'] [data-civitai-ui-resource-hit] {\n flex: 1 1 auto;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n padding: 0;\n}\n[data-civitai-ui='resource-card'] button[data-civitai-ui-resource-hit] {\n cursor: pointer;\n}\n[data-civitai-ui='resource-card'] button[data-civitai-ui-resource-hit]:disabled {\n cursor: not-allowed;\n}\n[data-civitai-ui='resource-card'] button[data-civitai-ui-resource-hit]:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: -2px;\n}\n/* The thumbnail frame. Rendered whether or not there is an image \u2014 see the\n component's thumbnailUrl doc: BlockResourceInfo has no image field, so \"no\n image\" is the COMMON case and the frame must keep its box regardless. */\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-thumb] {\n display: flex;\n align-items: center;\n justify-content: center;\n flex: none;\n overflow: hidden;\n background: var(--civitai-color-surface-2);\n border-radius: calc(var(--civitai-radius) - 1px);\n color: var(--civitai-color-text-dimmed);\n}\n[data-civitai-ui='resource-card'][data-variant='card'] [data-civitai-ui-resource-thumb] {\n /* \uD83D\uDD34 aspect-ratio, not a fixed height: the tile is grid-sized by the caller,\n and this is the line that stops a thumbnail-less card collapsing to a\n text-height sliver. */\n width: 100%;\n aspect-ratio: 1 / 1;\n font-size: 11px;\n}\n[data-civitai-ui='resource-card'][data-variant='row'] [data-civitai-ui-resource-thumb] {\n width: 36px;\n height: 36px;\n font-size: 9px;\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-thumb] img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-placeholder] {\n padding: 0 4px;\n text-align: center;\n line-height: 1.2;\n}\n/* Positioned from the ROOT, over the thumbnail corner. 10px = the hit area's\n 6px padding plus a 4px inset inside the frame; the three move together, and\n the browser tier asserts the result lands inside the frame's own rect rather\n than trusting the arithmetic.\n pointer-events: none is load-bearing, not polish: this slot is STATUS, and a\n pill that can swallow a click meant for the card is the same class of bug as\n nesting a control inside the hit <button>. A consumer who really wants an\n interactive badge opts back in with pointer-events: auto on their own node. */\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-overlay] {\n position: absolute;\n top: 10px;\n right: 10px;\n z-index: 1;\n pointer-events: none;\n display: flex;\n align-items: center;\n gap: 4px;\n max-width: calc(100% - 20px);\n font-size: 11px;\n line-height: 1;\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-nameline] {\n display: flex;\n align-items: center;\n gap: 4px;\n min-width: 0;\n}\n/* The non-colour half of the selected affordance (WCAG 1.4.1). The border-hue\n change on the root is reinforcement; this glyph is what carries it. */\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-selected] {\n flex: none;\n font-size: 12px;\n font-weight: 700;\n line-height: 1;\n color: var(--civitai-color-primary);\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-text] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n flex: 1 1 auto;\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-name] {\n display: block;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.3;\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-meta] {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 4px 6px;\n min-width: 0;\n font-size: 11px;\n color: var(--civitai-color-text-dimmed);\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-actions] {\n display: flex;\n align-items: center;\n flex: none;\n gap: 6px;\n}\n[data-civitai-ui='resource-card'][data-variant='card'] [data-civitai-ui-resource-actions] {\n padding: 0 6px 6px;\n}\n\n/* ----- SegmentedControl ----- */\n[data-civitai-ui='segmented-control'] {\n display: inline-flex;\n flex-direction: row;\n gap: 2px;\n padding: 3px;\n background: var(--civitai-color-surface-2);\n border: 1px solid var(--civitai-color-border);\n border-radius: var(--civitai-radius);\n vertical-align: middle;\n}\n[data-civitai-ui='segmented-control'][data-full-width='true'] {\n display: flex;\n width: 100%;\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment] {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: calc(var(--civitai-radius) - 3px);\n background: transparent;\n color: var(--civitai-color-text-dimmed);\n font-family: var(--civitai-font);\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n cursor: pointer;\n user-select: none;\n transition: background-color 120ms ease, color 120ms ease, box-shadow 120ms ease;\n}\n[data-civitai-ui='segmented-control'][data-full-width='true'] [data-civitai-ui-segment] {\n flex: 1 1 0;\n}\n[data-civitai-ui='segmented-control'][data-size='sm'] [data-civitai-ui-segment] { height: 24px; padding: 0 12px; font-size: 13px; }\n[data-civitai-ui='segmented-control'][data-size='md'] [data-civitai-ui-segment] { height: 30px; padding: 0 16px; font-size: 14px; }\n[data-civitai-ui='segmented-control'][data-size='lg'] [data-civitai-ui-segment] { height: 38px; padding: 0 20px; font-size: 16px; }\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment]:hover:not(:disabled):not([data-active]) {\n color: var(--civitai-color-text);\n background: color-mix(in srgb, var(--civitai-color-text) 6%, transparent);\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment][data-active] {\n background: var(--civitai-color-surface);\n color: var(--civitai-color-primary);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment]:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 1px;\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment]:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n}\n[data-civitai-ui='segmented-control'][data-disabled='true'] {\n opacity: 0.7;\n}\n";
9
9
  /**
10
10
  * Inject the pack's stylesheets into a document's `<head>`, idempotently.
11
11
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/blocks-react",
3
- "version": "0.64.0",
3
+ "version": "0.65.1",
4
4
  "description": "React hooks and iframe transport for Civitai Apps. Pairs with @civitai/app-sdk/blocks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "node": ">=20"
38
38
  },
39
39
  "dependencies": {
40
- "@civitai/components": "^0.9.2",
40
+ "@civitai/components": "^0.9.3",
41
41
  "@civitai/theme": "^0.5.2"
42
42
  },
43
43
  "comment-peerDependencies": "This floor is DERIVED, not chosen — the lowest published app-sdk that exports every symbol this package imports. Do not edit it without reading ./PEER_FLOOR.md (in-repo, unpublished) and tests/guards/blocks-react-peer-floor.test.mjs.",
@@ -58,7 +58,7 @@
58
58
  "typescript": "^5.9.2",
59
59
  "vite": "^8.0.14",
60
60
  "vitest": "^4.1.11",
61
- "@civitai/app-sdk": "^0.57.0"
61
+ "@civitai/app-sdk": "^0.59.0"
62
62
  },
63
63
  "publishConfig": {
64
64
  "access": "public"