@ecomconsult/consentkit 0.5.9 → 0.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -184,9 +184,10 @@ Pass any subset to `init()`. Nested objects merge with the defaults.
184
184
  | `theme.accent` | `string` | `"#2B50D8"` | Exposed as `--ck-accent` |
185
185
  | `theme.font` | `"inherit" \| "system"` | `"inherit"` | v0.5.0. `inherit` takes the host page's font family; `system` restores the pre-0.5.0 system stack. Font *sizes* are fixed either way |
186
186
  | `theme.radius` | `{ card, button }` | `{ card: 16, button: 8 }` | v0.5.0. px, clamped 0–32. A bare string or number is the pre-0.5.0 form and still sets the card radius |
187
- | `theme.buttons` | `{ accept, reject, settings }` | see below | v0.5.0. Per-button appearance. **Contrast is enforced automatically** — see [Button appearance](#button-appearance) |
187
+ | `theme.buttons` | `{ accept, reject, settings }` | see below | v0.5.0. Per-button appearance. A colour you set is painted as set; contrast rules correct only derived colours (v0.5.10) — see [Button appearance](#button-appearance) |
188
188
  | `theme.mode` | `"auto" \| "light" \| "dark"` | `"auto"` | `auto` follows `prefers-color-scheme` |
189
189
  | `theme.dark` | `{ bg, ink, accent, onAccent }` | built-in | Overrides the dark palette |
190
+ | `theme.light` | `{ onAccent }` | built-in | v0.5.10. The light mirror of `theme.dark`. An `onAccent` set here is the filled buttons' text colour for light mode, painted as given |
190
191
  | `texts.policyUrl` | `string` | — | v0.5.0. Cookie policy address. `http(s)` only; anything else is ignored |
191
192
  | `texts.detailsAction` | `"policy" \| "settings" \| "hide" \| "declaration"` | see notes | v0.5.0, `declaration` in v0.5.7. What «Learn more» does. Defaults to `policy` when `policyUrl` is set, `settings` when it is not. `policy` or `declaration` without a usable URL falls back to `settings` rather than rendering a dead link |
192
193
  | `texts.declarationUrl` | `string` | — | v0.5.7. Address of the cookie declaration page, used by `detailsAction: "declaration"`. `http(s)` only. Filled by the hosted service; the client only reads it |
@@ -274,18 +275,25 @@ accept is a dark pattern, and consent collected through one is not freely
274
275
  given, so the config simply cannot express it. `settings` is independent and
275
276
  may itself be filled.
276
277
 
277
- **Contrast is enforced automatically and cannot be switched off.** A colour
278
- combination that would be unreadable is corrected before it reaches the
279
- stylesheet:
280
-
281
- - button text keeps the colour you set only when it clears **4.5:1** against
282
- the fill it sits on; otherwise it becomes white or `#161616`, whichever
283
- contrasts more;
284
- - an `outline` border is darkened (light card) or lightened (dark card) in
285
- small steps until it clears **3:1** against the card, so it stays
286
- recognisably your colour rather than jumping to black or white;
287
- - that resolved border colour is then the outline button's text colour,
288
- subject to the same 4.5:1 rule.
278
+ **A colour you set is painted as you set it; contrast rules decide only the
279
+ colours you left to us.** Since v0.5.10 the **4.5:1** text rule and the **3:1**
280
+ border rule apply to *derived* values — the automatic text on a filled button,
281
+ the border and text an `outline` button takes from `theme.accent`, the link
282
+ colour read off the accent. A `fg`, `border` or `onAccent` you wrote yourself
283
+ is never repainted, however low it measures: the debug panel reports the ratio
284
+ and warns («контраст 4.32 — ниже рекомендуемых 4.5»), and the choice stays
285
+ yours. Concretely:
286
+
287
+ - a `fg` you set is painted as set and only measured against the fill behind
288
+ it; the *derived* text on a filled button still becomes white or `#161616`,
289
+ whichever contrasts more, when it would fall under 4.5:1;
290
+ - a `border` you set on an `outline` button is painted as set; a border
291
+ *derived* from `theme.accent` is darkened (light card) or lightened (dark
292
+ card) in small steps until it clears 3:1 against the card, so a default theme
293
+ can never produce a button invisible against its own card;
294
+ - an outline button's text, when you did not set one, is that resolved border
295
+ colour put through the same 4.5:1 rule — derived even when the border it came
296
+ from was yours.
289
297
 
290
298
  The card is `#ffffff` in light mode and `#1c1c1e` in dark. A colour the
291
299
  arithmetic cannot read — a CSS colour name, an `rgb()` string — is left exactly
@@ -298,7 +306,11 @@ The same arithmetic is exposed as pure functions on `ConsentKit._contrast`
298
306
  instead of reimplementing them. It is present whenever `src/ck-ui.js` is loaded,
299
307
  and it is safe to call in Node — nothing in it touches the DOM. The debug
300
308
  panel's **Appearance** section reads it directly and reports each button's
301
- resolved colours, its contrast ratio, and whether the value was adjusted.
309
+ resolved colours, its contrast ratio, and either that a derived value was
310
+ adjusted or that a value you set measures below the recommended floor. Each
311
+ resolved record carries `ratio` and `adjusted`, plus `low` (painted text under
312
+ 4.5:1) and, for outline buttons, `borderRatio`, `borderAdjusted` and
313
+ `borderLow` (border under 3:1).
302
314
 
303
315
  ### The «Learn more» link
304
316
 
@@ -28,7 +28,7 @@ export function undecidedState() {
28
28
  */
29
29
  export function createStub() {
30
30
  const stub = {
31
- version: '0.5.9',
31
+ version: '0.5.10',
32
32
  config: {},
33
33
  init: function () { return undecidedState(); },
34
34
  allowed: function (cat) { return cat === 'necessary'; },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomconsult/consentkit",
3
- "version": "0.5.9",
3
+ "version": "0.5.10",
4
4
  "description": "GDPR cookie consent core with blocking engine, Shadow DOM UI and Google Consent Mode v2. Zero dependencies, no build step.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/ck-core.js CHANGED
@@ -1941,7 +1941,7 @@
1941
1941
  // Public API
1942
1942
  // ---------------------------------------------------------------------------
1943
1943
  var ConsentKit = {
1944
- version: '0.5.9',
1944
+ version: '0.5.10',
1945
1945
  config: config,
1946
1946
 
1947
1947
  init: function (userConfig) {
package/src/ck-debug.js CHANGED
@@ -322,6 +322,14 @@
322
322
  btnAdjusted: 'исправлено автоматически',
323
323
  btnOk: 'AA',
324
324
  btnFail: 'ниже AA',
325
+ // 0.5.10: a colour the owner set is painted as set, so a low ratio is a
326
+ // WARNING, not a correction. Split around the measured number because it
327
+ // is the whole point of the note — «ниже рекомендуемых» with no reading
328
+ // tells the owner nothing about how far below they are.
329
+ lowTextA: 'контраст ',
330
+ lowTextB: ' — ниже рекомендуемых 4.5, текст может читаться хуже',
331
+ lowBorderA: 'обводка ',
332
+ lowBorderB: ' — ниже 3, кнопка плохо видна',
325
333
  secActions: 'Действия',
326
334
  reset: 'Сбросить согласие',
327
335
  showPrefs: 'Показать настройки',
@@ -388,6 +396,10 @@
388
396
  btnAdjusted: 'adjusted automatically',
389
397
  btnOk: 'AA',
390
398
  btnFail: 'below AA',
399
+ lowTextA: 'contrast ',
400
+ lowTextB: ' — below the recommended 4.5, the text may be harder to read',
401
+ lowBorderA: 'border ',
402
+ lowBorderB: ' — below 3, the button is hard to see',
391
403
  secActions: 'Actions',
392
404
  reset: 'Reset consent',
393
405
  showPrefs: 'Show preferences',
@@ -459,6 +471,10 @@
459
471
  btnAdjusted: 'corectat automat',
460
472
  btnOk: 'AA',
461
473
  btnFail: 'sub AA',
474
+ lowTextA: 'contrast ',
475
+ lowTextB: ' — sub 4.5 recomandat, textul poate fi mai greu de citit',
476
+ lowBorderA: 'contur ',
477
+ lowBorderB: ' — sub 3, butonul se vede greu',
462
478
  secActions: 'Acțiuni',
463
479
  reset: 'Resetează consimțământul',
464
480
  showPrefs: 'Arată setările',
@@ -1035,7 +1051,8 @@
1035
1051
  lkTxt = lk.color + ' ' + T.btnOn + ' ' + lk.against +
1036
1052
  ' · ' + fmtRatio(lk.ratio) +
1037
1053
  ' ' + ((typeof lk.ratio === 'number' && lk.ratio >= 4.5) ? T.btnOk : T.btnFail) +
1038
- (lk.adjusted ? ' · ' + T.btnAdjusted : '');
1054
+ (lk.adjusted ? ' · ' + T.btnAdjusted : '') +
1055
+ (lk.low ? ' · ' + T.lowTextA + fmtRatio(lk.ratio) + T.lowTextB : '');
1039
1056
  }
1040
1057
 
1041
1058
  sT.appendChild(defs([
@@ -1062,9 +1079,21 @@
1062
1079
  (b.variant === 'outline'
1063
1080
  ? ' · ' + T.btnBorder + ' ' + b.border + ' (' + fmtRatio(b.borderRatio) + ')'
1064
1081
  : '')));
1082
+ // «исправлено» only where something WAS in fact replaced. Since
1083
+ // 0.5.10 a colour the owner set is painted as set, and its shortfall
1084
+ // is reported as a plain warning with the measured number instead —
1085
+ // the panel must never claim to have corrected what it left alone.
1065
1086
  if (b.adjusted) {
1066
1087
  li.appendChild(el('span', { class: 'mut' }, ' · ' + T.btnAdjusted));
1067
1088
  }
1089
+ if (b.low) {
1090
+ li.appendChild(el('span', { class: 'mut' },
1091
+ ' · ' + T.lowTextA + fmtRatio(b.ratio) + T.lowTextB));
1092
+ }
1093
+ if (b.borderLow) {
1094
+ li.appendChild(el('span', { class: 'mut' },
1095
+ ' · ' + T.lowBorderA + fmtRatio(b.borderRatio) + T.lowBorderB));
1096
+ }
1068
1097
  uT.appendChild(li);
1069
1098
  });
1070
1099
  sT.appendChild(uT);
package/src/ck-ui.js CHANGED
@@ -842,10 +842,14 @@
842
842
  var WHITE = '#ffffff';
843
843
  var NEAR_BLACK = '#161616'; // SPEC §1: white or #161616, whichever contrasts more
844
844
 
845
- /* SPEC §1, the TEXT rule: keep the author's fg when it clears `min` against
846
- bg; otherwise swap to white or #161616 — whichever wins. Unreadable input
845
+ /* SPEC §1, the TEXT rule: keep `fg` when it clears `min` against bg;
846
+ otherwise swap to white or #161616 — whichever wins. Unreadable input
847
847
  (a colour name, rgb(), a missing value) is returned untouched with
848
- adjusted:false: the author sees their own colour, not a silent rewrite. */
848
+ adjusted:false rather than guessed at.
849
+
850
+ Since 0.5.10 this is only ever handed a DERIVED colour — resolveButtonStyles
851
+ keeps an explicitly set one out of here entirely — so the swap can no longer
852
+ overwrite something the owner chose. */
849
853
  function ensureContrast(fg, bg, min) {
850
854
  var floor = typeof min === 'number' && min > 0 ? min : 4.5;
851
855
  var have = contrastRatio(fg, bg);
@@ -861,7 +865,10 @@
861
865
  against the CARD background, so the button is visible as a button at all.
862
866
  Stepwise rather than a jump to black/white: a brand colour that is only
863
867
  slightly too pale should stay recognisably the brand colour. Direction is
864
- chosen by which way there is room to move — away from the card. */
868
+ chosen by which way there is room to move — away from the card.
869
+
870
+ Since 0.5.10 only a DERIVED border reaches this: a border the owner set is
871
+ painted as set and merely measured. */
865
872
  function stepToContrast(color, bg, min) {
866
873
  var floor = typeof min === 'number' && min > 0 ? min : 3;
867
874
  var have = contrastRatio(color, bg);
@@ -1221,7 +1228,14 @@
1221
1228
  числа": nothing recomputes these numbers a second time.
1222
1229
 
1223
1230
  `palette` is optional and lets buildThemeCss pass the ALREADY-RESOLVED
1224
- accent/card colours for the mode, so a theme.dark.accent is honoured. */
1231
+ accent/card colours for the mode, so a theme.dark.accent is honoured.
1232
+
1233
+ OWNER'S RULE (0.5.10): a colour the owner EXPLICITLY SET is painted as set.
1234
+ The >= 4.5 text rule and the >= 3 border rule may only decide colours the
1235
+ owner did NOT choose — the automatic text on a filled button, the border
1236
+ and text derived from the accent on an outline one. A typed colour that
1237
+ falls short is reported (`low` / `borderLow`) so the debug panel and the
1238
+ cabinet can WARN about it, never corrected behind the owner's back. */
1225
1239
  function resolveButtonStyles(theme, mode, palette) {
1226
1240
  var t = (theme && typeof theme === 'object') ? theme : {};
1227
1241
  var base = mode === 'dark' ? DARK : LIGHT;
@@ -1229,9 +1243,12 @@
1229
1243
 
1230
1244
  var cardBg = sanitizeCssValue('color', pal.bg, base.bg);
1231
1245
  var accent = sanitizeCssValue('color', pal.accent, base.accent);
1232
- // theme.dark.onAccent stays meaningful: it is the GIVEN fg for filled
1233
- // buttons in that mode. The >= 4.5 rule may still override it.
1246
+ // theme.dark.onAccent / theme.light.onAccent is the GIVEN fg for filled
1247
+ // buttons in that mode. When the owner set it, it counts as an explicit
1248
+ // colour and is painted as set; the built-in default is derived and stays
1249
+ // subject to the >= 4.5 rule.
1234
1250
  var onAccent = sanitizeCssValue('color', pal.onAccent, base.onAccent);
1251
+ var onAccentExplicit = pal.onAccentExplicit === true;
1235
1252
 
1236
1253
  var cfg = (t.buttons && typeof t.buttons === 'object') ? t.buttons : {};
1237
1254
 
@@ -1258,7 +1275,12 @@
1258
1275
  button's icon all read the token this produces, so none of them can
1259
1276
  drift from what the debug panel and the cabinet quote. Note the
1260
1277
  measurement is against cardBg — an accent that is fine inside a filled
1261
- button can still be unreadable on the card behind it. */
1278
+ button can still be unreadable on the card behind it.
1279
+
1280
+ Still corrected in 0.5.10, deliberately: the ACCENT is the owner's, but
1281
+ the LINK colour is derived from it — nobody typed "links are this
1282
+ colour". The accent itself is never repainted, so the owner still sees
1283
+ the brand colour everywhere they actually chose it. */
1262
1284
  var linkRes = ensureContrast(accent, cardBg, 4.5);
1263
1285
 
1264
1286
  var out = {
@@ -1269,11 +1291,35 @@
1269
1291
  color: linkRes.color,
1270
1292
  ratio: linkRes.ratio,
1271
1293
  adjusted: linkRes.adjusted,
1294
+ // A link is always derived, so this is normally false — the rule
1295
+ // already lifted it. It can still be true on a mid-tone custom card
1296
+ // where even the better of white and #161616 falls short of 4.5.
1297
+ low: (typeof linkRes.ratio === 'number' && linkRes.ratio < 4.5),
1272
1298
  against: cardBg
1273
1299
  },
1274
1300
  buttons: {}
1275
1301
  };
1276
1302
 
1303
+ /* "Did the owner explicitly set this colour?" — the gate on the whole
1304
+ 0.5.10 rule, and it cannot be a plain `str(c.fg)` test. sanitizeCssValue
1305
+ silently returns the FALLBACK for a hostile or malformed value, so a
1306
+ value that failed the grammar would otherwise be painted as "the owner's
1307
+ choice" with no correction at all — the injected-colour case would lose
1308
+ its contrast check as well as its value. Sanitising against a sentinel
1309
+ tells the two apart: a value that survives is the owner's, anything else
1310
+ falls through to the derived path exactly as before 0.5.10. */
1311
+ var NOT_SET = '\x00';
1312
+ function explicitColor(v) {
1313
+ var got = sanitizeCssValue('color', v, NOT_SET);
1314
+ return got === NOT_SET ? null : got;
1315
+ }
1316
+ // A measured ratio is `null` for a colour the arithmetic cannot read (a CSS
1317
+ // name, an rgb() string). That is "no verdict", not "below the floor", so it
1318
+ // must not raise the warning flag — the panel would print a note with no number.
1319
+ function below(ratio, floor) {
1320
+ return typeof ratio === 'number' && ratio < floor;
1321
+ }
1322
+
1277
1323
  for (var i = 0; i < BTN_ROLES.length; i++) {
1278
1324
  var role = BTN_ROLES[i];
1279
1325
  var c = (cfg[role] && typeof cfg[role] === 'object') ? cfg[role] : {};
@@ -1283,40 +1329,86 @@
1283
1329
  var rec;
1284
1330
 
1285
1331
  if (variant === 'filled') {
1286
- // bg: author's, else the accent. fg: author's, else onAccent for the
1287
- // default accent fill, then checked against the resolved bg.
1332
+ // bg: author's, else the accent always painted as given either way.
1288
1333
  var bg = sanitizeCssValue('color', c.bg, accent);
1289
- var wantFg = sanitizeCssValue('color', c.fg, onAccent);
1290
- var fgRes = ensureContrast(wantFg, bg, 4.5);
1291
1334
  var bd = sanitizeCssValue('color', c.border, bg);
1335
+ // fg: an EXPLICIT colour (the button's own, or an owner-set onAccent
1336
+ // for the mode) is painted as set and only measured. Only the derived
1337
+ // default goes through the >= 4.5 rule.
1338
+ var setFg = explicitColor(c.fg) || (onAccentExplicit ? onAccent : null);
1339
+ var fgColor, fgRatio, fgAdjusted;
1340
+ if (setFg) {
1341
+ fgColor = setFg;
1342
+ fgRatio = contrastRatio(setFg, bg);
1343
+ fgAdjusted = false;
1344
+ } else {
1345
+ var fgRes = ensureContrast(onAccent, bg, 4.5);
1346
+ fgColor = fgRes.color;
1347
+ fgRatio = fgRes.ratio;
1348
+ fgAdjusted = fgRes.adjusted;
1349
+ }
1292
1350
  rec = {
1293
1351
  variant: 'filled',
1294
1352
  bg: bg,
1295
- fg: fgRes.color,
1353
+ fg: fgColor,
1296
1354
  border: bd,
1297
1355
  borderWidth: bw,
1298
- ratio: fgRes.ratio,
1299
- adjusted: fgRes.adjusted,
1356
+ ratio: fgRatio,
1357
+ adjusted: fgAdjusted,
1358
+ // Warn-only: true when the PAINTED text falls under 4.5:1 —
1359
+ // usually a typed colour (the owner's own trade-off), but also a
1360
+ // CORRECTED one on a mid-tone fill near #767676, where neither
1361
+ // white nor #161616 reaches 4.5 and the better of the two still fails.
1362
+ low: below(fgRatio, 4.5),
1300
1363
  against: bg
1301
1364
  };
1302
1365
  } else {
1303
1366
  // outline: transparent fill, so everything is measured against the CARD.
1304
- // border >= 3:1 with the card (stepped), text = the border colour with
1305
- // the >= 4.5 rule against the card.
1306
- var wantBd = sanitizeCssValue('color', c.border, accent);
1307
- var bdRes = stepToContrast(wantBd, cardBg, 3);
1308
- var wantTx = str(c.fg) ? sanitizeCssValue('color', c.fg, bdRes.color) : bdRes.color;
1309
- var txRes = ensureContrast(wantTx, cardBg, 4.5);
1367
+ // A border the owner TYPED is painted as typed and only measured; one
1368
+ // DERIVED from the accent is still stepped to >= 3:1 so a default-themed
1369
+ // button cannot vanish into the card.
1370
+ var setBd = explicitColor(c.border);
1371
+ var bdColor, bdRatio, bdAdjusted;
1372
+ if (setBd) {
1373
+ bdColor = setBd;
1374
+ bdRatio = contrastRatio(setBd, cardBg);
1375
+ bdAdjusted = false;
1376
+ } else {
1377
+ var bdRes = stepToContrast(accent, cardBg, 3);
1378
+ bdColor = bdRes.color;
1379
+ bdRatio = bdRes.ratio;
1380
+ bdAdjusted = bdRes.adjusted;
1381
+ }
1382
+ // Text: the owner's when set, otherwise the resolved border colour put
1383
+ // through the >= 4.5 rule — that text is derived even when the border
1384
+ // it came from was typed, so it is still corrected.
1385
+ var setTx = explicitColor(c.fg);
1386
+ var txColor, txRatio, txAdjusted;
1387
+ if (setTx) {
1388
+ txColor = setTx;
1389
+ txRatio = contrastRatio(setTx, cardBg);
1390
+ txAdjusted = false;
1391
+ } else {
1392
+ var txRes = ensureContrast(bdColor, cardBg, 4.5);
1393
+ txColor = txRes.color;
1394
+ txRatio = txRes.ratio;
1395
+ txAdjusted = txRes.adjusted;
1396
+ }
1310
1397
  rec = {
1311
1398
  variant: 'outline',
1312
1399
  bg: sanitizeCssValue('color', c.bg, 'transparent'),
1313
- fg: txRes.color,
1314
- border: bdRes.color,
1400
+ fg: txColor,
1401
+ border: bdColor,
1315
1402
  borderWidth: bw,
1316
- ratio: txRes.ratio,
1317
- adjusted: txRes.adjusted || bdRes.adjusted,
1318
- borderRatio: bdRes.ratio,
1319
- borderAdjusted: bdRes.adjusted,
1403
+ ratio: txRatio,
1404
+ // `adjusted` now tracks the TEXT alone. A typed border that was kept
1405
+ // must not make the row say «исправлено» about a colour nobody
1406
+ // touched; the border reports itself through borderAdjusted.
1407
+ adjusted: txAdjusted,
1408
+ low: below(txRatio, 4.5),
1409
+ borderRatio: bdRatio,
1410
+ borderAdjusted: bdAdjusted,
1411
+ borderLow: below(bdRatio, 3),
1320
1412
  against: cardBg
1321
1413
  };
1322
1414
  }
@@ -1381,6 +1473,11 @@
1381
1473
  function buildThemeCss(cfg) {
1382
1474
  var theme = (cfg && cfg.theme) || {};
1383
1475
  var dk = (theme.dark && typeof theme.dark === 'object') ? theme.dark : {};
1476
+ // theme.light is the mirror of theme.dark and exists for one value: an
1477
+ // onAccent the owner set for the light mode. Without it a light-mode
1478
+ // filled button had no way to state its text colour once for all three
1479
+ // buttons, and theme.dark could do what theme.light could not.
1480
+ var lt = (theme.light && typeof theme.light === 'object') ? theme.light : {};
1384
1481
 
1385
1482
  var radius = resolveRadius(theme);
1386
1483
  var font = resolveFont(theme);
@@ -1390,6 +1487,11 @@
1390
1487
  sanitizeCssValue('color', theme.bg, LIGHT.bg),
1391
1488
  sanitizeCssValue('color', theme.ink, LIGHT.ink));
1392
1489
  if (str(theme.accent)) light.accent = sanitizeCssValue('color', theme.accent, LIGHT.accent);
1490
+ // An onAccent that SURVIVES the grammar is the owner's word on the filled
1491
+ // buttons' text and is painted as given; one that fails it falls back to
1492
+ // the built-in default, which is derived and still gets corrected.
1493
+ var ltOn = sanitizeCssValue('color', lt.onAccent, null);
1494
+ if (ltOn) { light.onAccent = ltOn; light.onAccentExplicit = true; }
1393
1495
 
1394
1496
  // Dark: theme.dark overrides on top of the built-in dark palette.
1395
1497
  // A light-only theme.accent deliberately does NOT carry into dark — the
@@ -1398,7 +1500,8 @@
1398
1500
  sanitizeCssValue('color', dk.bg, DARK.bg),
1399
1501
  sanitizeCssValue('color', dk.ink, DARK.ink));
1400
1502
  if (str(dk.accent)) dark.accent = sanitizeCssValue('color', dk.accent, DARK.accent);
1401
- if (str(dk.onAccent)) dark.onAccent = sanitizeCssValue('color', dk.onAccent, DARK.onAccent);
1503
+ var dkOn = sanitizeCssValue('color', dk.onAccent, null);
1504
+ if (dkOn) { dark.onAccent = dkOn; dark.onAccentExplicit = true; }
1402
1505
 
1403
1506
  // Buttons are resolved per mode against that mode's real card colour, so a
1404
1507
  // custom dark card changes the outline border the same way a custom light
@@ -1407,6 +1510,16 @@
1407
1510
  var darkBtn = resolveButtonStyles(theme, 'dark', dark);
1408
1511
  light.__buttons = lightBtn.buttons;
1409
1512
  dark.__buttons = darkBtn.buttons;
1513
+
1514
+ // `--ck-on-accent` paints the text of every filled surface that is NOT a
1515
+ // banner button — the panel's «Сохранить выбор» and the floating button —
1516
+ // straight from the palette. A derived onAccent therefore has to pass the
1517
+ // same >= 4.5 rule against the accent, or a white accent gets white text
1518
+ // (the owner saw exactly that). Done AFTER the buttons are resolved, so
1519
+ // the accept button still records `adjusted:true` for the same correction.
1520
+ // An onAccent the owner set is painted as set — the 0.5.10 rule.
1521
+ if (!light.onAccentExplicit) light.onAccent = ensureContrast(light.onAccent, light.accent, 4.5).color;
1522
+ if (!dark.onAccentExplicit) dark.onAccent = ensureContrast(dark.onAccent, dark.accent, 4.5).color;
1410
1523
  light.__link = lightBtn.link;
1411
1524
  dark.__link = darkBtn.link;
1412
1525