@citolab/qti-components 8.0.1 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/cdn/index.global.js +1 -1
  2. package/cdn/index.js +648 -816
  3. package/dist/base.d.ts +61 -4
  4. package/dist/base.js +7 -1
  5. package/dist/{chunk-V5G7X53Y.js → chunk-3IACYIU2.js} +2 -2
  6. package/dist/chunk-4DB4SC5K.js +100 -0
  7. package/dist/chunk-4DB4SC5K.js.map +1 -0
  8. package/dist/{chunk-F4BNJWYN.js → chunk-4XR2JHCP.js} +646 -677
  9. package/dist/chunk-4XR2JHCP.js.map +1 -0
  10. package/dist/{chunk-7V4HJFHW.js → chunk-6ITD55QE.js} +2 -2
  11. package/dist/{chunk-3AJOQCDU.js → chunk-6V3A7NQP.js} +2 -2
  12. package/dist/{chunk-WVVMNF74.js → chunk-6XJUP55A.js} +248 -234
  13. package/dist/chunk-6XJUP55A.js.map +1 -0
  14. package/dist/{chunk-XU7HNT4F.js → chunk-CF3DXJWM.js} +145 -119
  15. package/dist/{chunk-XU7HNT4F.js.map → chunk-CF3DXJWM.js.map} +1 -1
  16. package/dist/chunk-EKXZI26R.js +917 -0
  17. package/dist/chunk-EKXZI26R.js.map +1 -0
  18. package/dist/{chunk-IJD53WY6.js → chunk-HDEG6BAZ.js} +15 -15
  19. package/dist/{chunk-IJD53WY6.js.map → chunk-HDEG6BAZ.js.map} +1 -1
  20. package/dist/{chunk-LVBIQDPM.js → chunk-IXPDMYKH.js} +147 -101
  21. package/dist/chunk-IXPDMYKH.js.map +1 -0
  22. package/dist/chunk-IYC7LZV6.js +7 -0
  23. package/dist/chunk-IYC7LZV6.js.map +1 -0
  24. package/dist/{chunk-NA2N2WSG.js → chunk-MK2I3MQB.js} +244 -193
  25. package/dist/chunk-MK2I3MQB.js.map +1 -0
  26. package/dist/{chunk-CAO4JRLY.js → chunk-NZLW6EX7.js} +2 -2
  27. package/dist/{chunk-RXGDQDOV.js → chunk-Q52IJELT.js} +479 -393
  28. package/dist/chunk-Q52IJELT.js.map +1 -0
  29. package/dist/{chunk-QECOPVPO.js → chunk-TXIB3SKC.js} +60 -59
  30. package/dist/chunk-TXIB3SKC.js.map +1 -0
  31. package/dist/{computed.types.d-rKLSPvVG.d.ts → computed.types.d-DpKIYO6e.d.ts} +2 -0
  32. package/dist/corrections.d.ts +12 -29
  33. package/dist/corrections.js +9 -10
  34. package/dist/elements.d.ts +86 -87
  35. package/dist/elements.js +5 -6
  36. package/dist/index.d.ts +8 -39
  37. package/dist/index.js +29 -16
  38. package/dist/{interaction.d-D4MstYRz.d.ts → interaction.d-CJiMv8H7.d.ts} +13 -2
  39. package/dist/interactions.d.ts +11 -1299
  40. package/dist/interactions.js +4 -5
  41. package/dist/item.css +370 -532
  42. package/dist/item.d.ts +3 -3
  43. package/dist/item.js +6 -6
  44. package/dist/loader.js +2 -2
  45. package/dist/processing.d.ts +61 -11
  46. package/dist/processing.js +10 -2
  47. package/dist/qti-components-jsx.d.ts +293 -69
  48. package/dist/qti-test.d-_YAlhDbt.d.ts +702 -0
  49. package/dist/qti-text-entry-interaction.d-BGdfV9qG.d.ts +1567 -0
  50. package/dist/test.d.ts +11 -678
  51. package/dist/test.js +7 -8
  52. package/dist/transformers.d.ts +1 -1
  53. package/dist/transformers.js +1 -1
  54. package/package.json +11 -10
  55. package/dist/chunk-64XSIBGO.js +0 -241
  56. package/dist/chunk-64XSIBGO.js.map +0 -1
  57. package/dist/chunk-7HC5SMCU.js +0 -102
  58. package/dist/chunk-7HC5SMCU.js.map +0 -1
  59. package/dist/chunk-F4BNJWYN.js.map +0 -1
  60. package/dist/chunk-H3J7LFRO.js +0 -2243
  61. package/dist/chunk-H3J7LFRO.js.map +0 -1
  62. package/dist/chunk-LVBIQDPM.js.map +0 -1
  63. package/dist/chunk-NA2N2WSG.js.map +0 -1
  64. package/dist/chunk-QECOPVPO.js.map +0 -1
  65. package/dist/chunk-RXGDQDOV.js.map +0 -1
  66. package/dist/chunk-WVVMNF74.js.map +0 -1
  67. package/dist/chunk-X3E5HXG2.js +0 -20
  68. package/dist/chunk-X3E5HXG2.js.map +0 -1
  69. /package/dist/{chunk-V5G7X53Y.js.map → chunk-3IACYIU2.js.map} +0 -0
  70. /package/dist/{chunk-7V4HJFHW.js.map → chunk-6ITD55QE.js.map} +0 -0
  71. /package/dist/{chunk-3AJOQCDU.js.map → chunk-6V3A7NQP.js.map} +0 -0
  72. /package/dist/{chunk-CAO4JRLY.js.map → chunk-NZLW6EX7.js.map} +0 -0
package/dist/item.css CHANGED
@@ -2363,16 +2363,15 @@
2363
2363
  Handed to qti-simple-choice.styles.ts, which owns the label's box; this was an
2364
2364
  `::part(label) { display: inline !important }`. */
2365
2365
  --qti-choice-label-display: inline;
2366
+ }
2366
2367
 
2367
- & qti-simple-choice {
2368
+ qti-choice-interaction qti-simple-choice {
2368
2369
  padding: var(--qti-padding-box);
2369
2370
  width: -moz-fit-content;
2370
2371
  width: fit-content;
2371
2372
  cursor: pointer;
2372
2373
  }
2373
-
2374
- &.qti-input-control-hidden {
2375
- & qti-simple-choice {
2374
+ qti-choice-interaction.qti-input-control-hidden qti-simple-choice {
2376
2375
  background-color: var(--button-background-color, var(--qti-component-background-color));
2377
2376
  background-image: var(--button-background-image, var(--qti-component-background-image));
2378
2377
  border-width: var(--button-border-width, var(--qti-component-border-width));
@@ -2384,14 +2383,11 @@
2384
2383
  opacity: var(--button-opacity, var(--qti-component-opacity));
2385
2384
  filter: var(--button-filter, var(--qti-component-filter));
2386
2385
  --qti-control-display: none;
2387
- &:focus {
2386
+ }
2387
+ :is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice):focus {
2388
2388
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
2389
2389
  }
2390
- }
2391
- }
2392
-
2393
- &:not(.qti-input-control-hidden) {
2394
- & qti-simple-choice {
2390
+ qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice {
2395
2391
  background-color: var(--button-background-color, var(--qti-component-background-color));
2396
2392
  background-image: var(--button-background-image, var(--qti-component-background-image));
2397
2393
  border-width: var(--button-border-width, var(--qti-component-border-width));
@@ -2406,52 +2402,45 @@
2406
2402
  gap: 0.5rem;
2407
2403
  outline: none;
2408
2404
  cursor: pointer;
2405
+ }
2409
2406
 
2410
- &:focus {
2407
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):focus {
2411
2408
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
2412
2409
  }
2413
2410
 
2414
- &::part(control-mark) {
2411
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice)::part(control-mark) {
2415
2412
  width: calc(var(--qti-control-size) * 0.625);
2416
2413
  height: calc(var(--qti-control-size) * 0.625);
2417
2414
  }
2418
- }
2419
- }
2420
- }
2421
2415
  }
2422
2416
  @layer qti-components.interactions.states {
2423
- qti-choice-interaction {
2424
- &.qti-input-control-hidden qti-simple-choice {
2425
2417
  /* A selected choice, repainted through the button role's slots rather than by re-declaring the
2426
2418
  properties — the choice is `@mixin button`, so switching a variable is all a state has to do.
2427
2419
  This used to be `@mixin act` followed by two direct declarations, and half of `act` was dead
2428
2420
  on arrival: it set `background-color: var(--qti-bg-active)` and the very next line overwrote
2429
2421
  it with the selected fill. Only its border-colour ever survived, which is written out here. */
2430
- &:state(checked) {
2422
+ :is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice):is(:state(checked), [data-state~='checked']) {
2431
2423
  --button-border-color: var(--qti-border-active);
2432
2424
  --button-background-color: var(--qti-selected-bg);
2433
2425
  --button-color: var(--qti-selected-color);
2434
2426
  }
2435
2427
 
2436
- &:state(readonly) {
2428
+ :is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice):is(:state(readonly), [data-state~='readonly']) {
2437
2429
  cursor: pointer;
2438
2430
  }
2439
2431
 
2440
- &:state(disabled) {
2432
+ :is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice):is(:state(disabled), [data-state~='disabled']) {
2441
2433
  cursor: not-allowed;
2442
2434
  }
2443
- }
2444
-
2445
- &:not(.qti-input-control-hidden) qti-simple-choice {
2446
- &:state(readonly) {
2435
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(readonly), [data-state~='readonly']) {
2447
2436
  cursor: pointer;
2448
2437
  }
2449
2438
 
2450
- &:state(disabled) {
2439
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(disabled), [data-state~='disabled']) {
2451
2440
  cursor: not-allowed;
2452
2441
  }
2453
2442
 
2454
- &:state(radio)::part(control) {
2443
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(radio), [data-state~='radio'])::part(control) {
2455
2444
  background-color: var(--radio-background-color, var(--qti-component-background-color));
2456
2445
  background-image: var(--radio-background-image, var(--qti-component-background-image));
2457
2446
  border-width: var(--radio-border-width, var(--qti-component-border-width));
@@ -2469,13 +2458,12 @@
2469
2458
  border-radius: var(--radio-border-radius, 100%);
2470
2459
  }
2471
2460
 
2472
- &:state(radio):state(checked)::part(control),
2473
- &:state(checkbox):state(checked)::part(control) {
2461
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(radio), [data-state~='radio']):is(:state(checked), [data-state~='checked'])::part(control),:is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(checkbox), [data-state~='checkbox']):is(:state(checked), [data-state~='checked'])::part(control) {
2474
2462
  border-color: var(--qti-primary);
2475
2463
  background-color: var(--qti-primary);
2476
2464
  }
2477
2465
 
2478
- &:state(radio):state(checked)::part(control-mark) {
2466
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(radio), [data-state~='radio']):is(:state(checked), [data-state~='checked'])::part(control-mark) {
2479
2467
  /* 0.5 reproduces today's 8px at the default 1rem — see mark-size for why this is a fraction. */
2480
2468
  width: calc(var(--qti-control-size) * 0.5);
2481
2469
  height: calc(var(--qti-control-size) * 0.5);
@@ -2483,7 +2471,7 @@
2483
2471
  border-radius: 100%;
2484
2472
  }
2485
2473
 
2486
- &:state(checkbox)::part(control) {
2474
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(checkbox), [data-state~='checkbox'])::part(control) {
2487
2475
  background-color: var(--checkbox-background-color, var(--qti-component-background-color));
2488
2476
  background-image: var(--checkbox-background-image, var(--qti-component-background-image));
2489
2477
  border-width: var(--checkbox-border-width, var(--qti-component-border-width));
@@ -2498,27 +2486,17 @@
2498
2486
  height: var(--qti-control-size);
2499
2487
  }
2500
2488
 
2501
- &:state(checkbox):state(checked)::part(control-mark) {
2489
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(checkbox), [data-state~='checkbox']):is(:state(checked), [data-state~='checked'])::part(control-mark) {
2502
2490
  background-color: var(--checkbox-mark-color, white);
2503
2491
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' width='100%' height='100%' viewBox='0 0 24 24'%3E%3Cpath d='M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z'/%3E%3C/svg%3E");
2504
2492
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' width='100%' height='100%' viewBox='0 0 24 24'%3E%3Cpath d='M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z'/%3E%3C/svg%3E");
2505
2493
  }
2506
- }
2507
- }
2508
2494
  }
2509
2495
  @layer qti-components.interactions.corrections {
2510
- qti-choice-interaction {
2511
- & qti-simple-choice {
2512
- &:state(correct-response),
2513
- &[data-correct-response='true'] {
2514
- &::after {
2496
+ :is(:is(qti-choice-interaction qti-simple-choice):is(:state(correct-response), [data-state~='correct-response']),[data-correct-response='true']:is(qti-choice-interaction qti-simple-choice))::after {
2515
2497
  content: '\02714';
2516
2498
  color: var(--qti-correct);
2517
2499
  }
2518
- }
2519
- }
2520
-
2521
- &.qti-input-control-hidden qti-simple-choice {
2522
2500
  /*
2523
2501
  * Paint only. This used to set `border: 3px` over a 1px base and add `padding-right: 30px`,
2524
2502
  * so revealing the answer key grew every correct choice and reflowed the list around it.
@@ -2527,12 +2505,12 @@
2527
2505
  * costs no layout. The 30px that kept the label clear of the tick is reserved in the base
2528
2506
  * rule for this variant, whether or not a tick ever appears.
2529
2507
  */
2530
- &:state(correct-response),
2531
- &[data-correct-response='true'] {
2508
+ :is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice):is(:state(correct-response), [data-state~='correct-response']),[data-correct-response='true']:is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice) {
2532
2509
  border-color: var(--qti-correct) !important;
2533
2510
  box-shadow: inset 0 0 0 2px var(--qti-correct);
2511
+ }
2534
2512
 
2535
- &::after {
2513
+ :is(:is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice):is(:state(correct-response), [data-state~='correct-response']),[data-correct-response='true']:is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice))::after {
2536
2514
  content: '\02714';
2537
2515
  color: var(--qti-correct);
2538
2516
  position: absolute;
@@ -2541,94 +2519,74 @@
2541
2519
  font-size: 1.2em;
2542
2520
  font-weight: bold;
2543
2521
  }
2544
- }
2545
2522
 
2546
2523
  /* Candidate judgement follows checked/correct-response paint so it can
2547
2524
  recolour the same stable box without changing its geometry. */
2548
- &:state(candidate-correct) {
2525
+ :is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice):is(:state(candidate-correct), [data-state~='candidate-correct']) {
2549
2526
  --button-background-color: var(--qti-correct-light);
2550
2527
  --button-color: var(--qti-correct);
2551
2528
  --button-border-color: var(--qti-correct);
2552
2529
  --button-box-shadow: none;
2553
2530
  }
2554
2531
 
2555
- &:state(candidate-incorrect) {
2532
+ :is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
2556
2533
  --button-background-color: var(--qti-incorrect-light);
2557
2534
  --button-color: var(--qti-incorrect);
2558
2535
  --button-border-color: var(--qti-incorrect);
2559
2536
  --button-box-shadow: none;
2560
2537
  }
2561
- }
2562
-
2563
- &:not(.qti-input-control-hidden) qti-simple-choice {
2564
- &:state(candidate-correct) {
2538
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(candidate-correct), [data-state~='candidate-correct']) {
2565
2539
  background-color: transparent;
2540
+ }
2566
2541
 
2567
- &:state(radio):state(checked)::part(control),
2568
- &:state(checkbox):state(checked)::part(control) {
2542
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(candidate-correct), [data-state~='candidate-correct']):is(:state(radio), [data-state~='radio']):is(:state(checked), [data-state~='checked'])::part(control),:is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(candidate-correct), [data-state~='candidate-correct']):is(:state(checkbox), [data-state~='checkbox']):is(:state(checked), [data-state~='checked'])::part(control) {
2569
2543
  border-color: var(--qti-correct);
2570
2544
  background-color: var(--qti-correct);
2571
2545
  }
2572
- }
2573
2546
 
2574
- &:state(candidate-incorrect) {
2547
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
2575
2548
  background-color: transparent;
2549
+ }
2576
2550
 
2577
- &:state(radio):state(checked)::part(control),
2578
- &:state(checkbox):state(checked)::part(control) {
2551
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']):is(:state(radio), [data-state~='radio']):is(:state(checked), [data-state~='checked'])::part(control),:is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']):is(:state(checkbox), [data-state~='checkbox']):is(:state(checked), [data-state~='checked'])::part(control) {
2579
2552
  border-color: var(--qti-incorrect);
2580
2553
  background-color: var(--qti-incorrect);
2581
2554
  }
2582
- }
2583
- }
2584
- }
2585
-
2586
- .full-correct-response qti-choice-interaction {
2587
- qti-simple-choice {
2588
- &:state(radio):state(checked)::part(control),
2589
- &:state(checkbox):state(checked)::part(control) {
2555
+ :is(:is(.full-correct-response qti-choice-interaction) qti-simple-choice):is(:state(radio), [data-state~='radio']):is(:state(checked), [data-state~='checked'])::part(control),:is(:is(.full-correct-response qti-choice-interaction) qti-simple-choice):is(:state(checkbox), [data-state~='checkbox']):is(:state(checked), [data-state~='checked'])::part(control) {
2590
2556
  background-color: var(--qti-answer-bg);
2591
2557
  color: var(--qti-answer-fg);
2592
2558
  border-color: var(--qti-answer-border);
2593
2559
  }
2594
2560
 
2595
- &:state(radio):state(checked)::part(control-mark),
2596
- &:state(checkbox):state(checked)::part(control-mark) {
2561
+ :is(:is(.full-correct-response qti-choice-interaction) qti-simple-choice):is(:state(radio), [data-state~='radio']):is(:state(checked), [data-state~='checked'])::part(control-mark),:is(:is(.full-correct-response qti-choice-interaction) qti-simple-choice):is(:state(checkbox), [data-state~='checkbox']):is(:state(checked), [data-state~='checked'])::part(control-mark) {
2597
2562
  background-color: var(--qti-answer-fg);
2598
2563
  }
2599
- }
2600
2564
 
2601
- &.qti-input-control-hidden qti-simple-choice:state(checked) {
2565
+ .qti-input-control-hidden:is(.full-correct-response qti-choice-interaction) qti-simple-choice:is(:state(checked), [data-state~='checked']) {
2602
2566
  background-color: var(--qti-answer-bg);
2603
2567
  color: var(--qti-answer-fg);
2604
2568
  border-color: var(--qti-answer-border);
2605
2569
  }
2606
- }
2607
2570
  }
2608
2571
  @layer qti-components.interactions.base {
2609
- qti-graphic-gap-match-interaction {
2610
- &.qti-selections-light {
2611
- &:state(dragzone-active)::part(drags) {
2572
+ qti-graphic-gap-match-interaction.qti-selections-light:is(:state(dragzone-active), [data-state~='dragzone-active'])::part(drags) {
2612
2573
  background-color: var(--qti-selections-light-bg);
2613
2574
  border-color: var(--qti-selections-light-border);
2614
2575
  }
2615
- &:state(dragzone-enabled)::part(drags) {
2576
+ qti-graphic-gap-match-interaction.qti-selections-light:is(:state(dragzone-enabled), [data-state~='dragzone-enabled'])::part(drags) {
2616
2577
  background-color: var(--qti-selections-light-bg);
2617
2578
  }
2618
- }
2619
- &.qti-selections-dark {
2620
- &:state(dragzone-active)::part(drags) {
2579
+ qti-graphic-gap-match-interaction.qti-selections-dark:is(:state(dragzone-active), [data-state~='dragzone-active'])::part(drags) {
2621
2580
  background-color: var(--qti-selections-dark-bg);
2622
2581
  border-color: var(--qti-selections-dark-border);
2623
2582
  }
2624
- &:state(dragzone-enabled)::part(drags) {
2583
+ qti-graphic-gap-match-interaction.qti-selections-dark:is(:state(dragzone-enabled), [data-state~='dragzone-enabled'])::part(drags) {
2625
2584
  background-color: var(--qti-selections-dark-bg);
2626
2585
  }
2627
- }
2628
- &:state(dragzone-active)::part(drags) {
2586
+ qti-graphic-gap-match-interaction:is(:state(dragzone-active), [data-state~='dragzone-active'])::part(drags) {
2629
2587
  background-color: var(--qti-bg-active);
2630
2588
  }
2631
- &:state(dragzone-enabled)::part(drags) {
2589
+ qti-graphic-gap-match-interaction:is(:state(dragzone-enabled), [data-state~='dragzone-enabled'])::part(drags) {
2632
2590
  background-color: var(--qti-bg-active);
2633
2591
  }
2634
2592
  /*
@@ -2643,17 +2601,14 @@
2643
2601
  /* A chip here is a tile filling its hotspot, so it centres its content — see
2644
2602
  qti-gap-img.styles.ts and qti-gap-text.styles.ts. gap-match's chips hug their text instead,
2645
2603
  which is why the justification is a variable rather than the element's default. */
2646
- & qti-gap-img,
2647
- & qti-gap-text {
2604
+ qti-graphic-gap-match-interaction qti-gap-img,qti-graphic-gap-match-interaction qti-gap-text {
2648
2605
  --qti-drag-justify-content: center;
2649
2606
  cursor: grab;
2650
2607
  }
2651
2608
  /* The placed chip, in the hotspot's shadow. Plain too, to match the bank. Focus ring only. */
2652
- & qti-associable-hotspot::part(drag) {
2653
- &:focus {
2609
+ :is(qti-graphic-gap-match-interaction qti-associable-hotspot::part(drag)):focus {
2654
2610
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
2655
2611
  }
2656
- }
2657
2612
  /*
2658
2613
  * The hotspot centres its chip in qti-associable-hotspot.styles.ts.
2659
2614
  *
@@ -2667,7 +2622,7 @@
2667
2622
  * Adding a rule would be a redundant restatement of this line. Left as a note rather than code
2668
2623
  * because the absence is the kind of thing that reads as an oversight.
2669
2624
  */
2670
- & qti-associable-hotspot {
2625
+ qti-graphic-gap-match-interaction qti-associable-hotspot {
2671
2626
  border: 2px solid transparent;
2672
2627
  /* No .qti-selections-* branches in here. They used to sit inside both of these blocks as
2673
2628
  *descendant* selectors — and the class is on the interaction, an ancestor of this hotspot,
@@ -2675,19 +2630,20 @@
2675
2630
  see the :state(filled) rules below.
2676
2631
  A drop target lighting up mid-drag is not a "selection" anyway — Appendix B calls this
2677
2632
  Selections theming, and the selection is the chip once it has landed. */
2678
- &[enabled] {
2633
+ }
2634
+ [enabled]:is(qti-graphic-gap-match-interaction qti-associable-hotspot) {
2679
2635
  background-color: var(--qti-bg-active);
2680
2636
  }
2681
- &[active] {
2637
+ [active]:is(qti-graphic-gap-match-interaction qti-associable-hotspot) {
2682
2638
  background-color: var(--qti-bg-active);
2683
2639
  }
2684
- &[disabled] {
2640
+ [disabled]:is(qti-graphic-gap-match-interaction qti-associable-hotspot) {
2685
2641
  cursor: not-allowed;
2686
- &:not(:empty) {
2642
+ }
2643
+ [disabled]:is(qti-graphic-gap-match-interaction qti-associable-hotspot):not(:empty) {
2687
2644
  cursor: default !important;
2688
2645
  }
2689
- }
2690
- &:not(:empty) {
2646
+ :is(qti-graphic-gap-match-interaction qti-associable-hotspot):not(:empty) {
2691
2647
  border-color: var(--qti-border) !important;
2692
2648
  }
2693
2649
  /*
@@ -2709,30 +2665,28 @@
2709
2665
 
2710
2666
  /* `flex: 1` left with the rest of the drop's layout; dropRegion in qti-base gives a hosted
2711
2667
  chip `flex: 0 0 auto`, which is the invariance rule this was quietly fighting. */
2712
- &:not(:empty) > * {
2668
+ :is(qti-graphic-gap-match-interaction qti-associable-hotspot):not(:empty) > * {
2713
2669
  transform: rotate(0);
2714
2670
  box-shadow: 0 0 0 1px #e5e7eb;
2715
2671
  }
2716
- }
2717
2672
  /* `:not(:empty)` tested light-DOM children. The hotspot renders its chips into its own shadow
2718
2673
  root now, so it is always "empty" to a selector — `:state(filled)` is the occupancy hook. */
2719
- &.qti-selections-light qti-associable-hotspot:state(filled) {
2674
+ qti-graphic-gap-match-interaction.qti-selections-light qti-associable-hotspot:is(:state(filled), [data-state~='filled']) {
2720
2675
  background-color: var(--qti-selections-light-bg) !important;
2721
2676
  border-color: var(--qti-selections-light-border) !important;
2722
2677
  }
2723
- &.qti-selections-dark qti-associable-hotspot:state(filled) {
2678
+ qti-graphic-gap-match-interaction.qti-selections-dark qti-associable-hotspot:is(:state(filled), [data-state~='filled']) {
2724
2679
  background-color: var(--qti-selections-dark-bg) !important;
2725
2680
  border-color: var(--qti-selections-dark-border) !important;
2726
2681
  }
2727
- & img {
2682
+ qti-graphic-gap-match-interaction img {
2728
2683
  margin: 0;
2729
2684
  padding: 0;
2730
2685
  }
2731
- }
2732
2686
  }
2733
2687
  @layer qti-components.interactions.states {
2734
- qti-graphic-gap-match-interaction qti-gap-img:state(placeholder),
2735
- qti-graphic-gap-match-interaction qti-gap-text:state(placeholder) {
2688
+ qti-graphic-gap-match-interaction qti-gap-img:is(:state(placeholder), [data-state~='placeholder']),
2689
+ qti-graphic-gap-match-interaction qti-gap-text:is(:state(placeholder), [data-state~='placeholder']) {
2736
2690
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
2737
2691
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
2738
2692
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -2743,8 +2697,8 @@
2743
2697
  pointer-events: none;
2744
2698
  }
2745
2699
 
2746
- qti-graphic-gap-match-interaction qti-gap-img:state(dragging),
2747
- qti-graphic-gap-match-interaction qti-gap-text:state(dragging) {
2700
+ qti-graphic-gap-match-interaction qti-gap-img:is(:state(dragging), [data-state~='dragging']),
2701
+ qti-graphic-gap-match-interaction qti-gap-text:is(:state(dragging), [data-state~='dragging']) {
2748
2702
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
2749
2703
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
2750
2704
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -2758,26 +2712,26 @@
2758
2712
  pointer-events: none;
2759
2713
  }
2760
2714
 
2761
- qti-graphic-gap-match-interaction qti-gap-img:state(return-anchor),
2762
- qti-graphic-gap-match-interaction qti-gap-text:state(return-anchor) {
2715
+ qti-graphic-gap-match-interaction qti-gap-img:is(:state(return-anchor), [data-state~='return-anchor']),
2716
+ qti-graphic-gap-match-interaction qti-gap-text:is(:state(return-anchor), [data-state~='return-anchor']) {
2763
2717
  border-color: var(--qti-drop-highlight-border-color);
2764
2718
  box-shadow: inset 0 0 0 var(--qti-drop-highlight-border-width, 2px)
2765
2719
  color-mix(in srgb, var(--qti-drop-highlight-border-color) 55%, transparent);
2766
2720
  }
2767
2721
 
2768
- qti-graphic-gap-match-interaction qti-gap-img:state(placeholder) > *,
2769
- qti-graphic-gap-match-interaction qti-gap-text:state(placeholder) > *,
2770
- qti-graphic-gap-match-interaction qti-gap-img:state(dragging) > *,
2771
- qti-graphic-gap-match-interaction qti-gap-text:state(dragging) > * {
2722
+ qti-graphic-gap-match-interaction qti-gap-img:is(:state(placeholder), [data-state~='placeholder']) > *,
2723
+ qti-graphic-gap-match-interaction qti-gap-text:is(:state(placeholder), [data-state~='placeholder']) > *,
2724
+ qti-graphic-gap-match-interaction qti-gap-img:is(:state(dragging), [data-state~='dragging']) > *,
2725
+ qti-graphic-gap-match-interaction qti-gap-text:is(:state(dragging), [data-state~='dragging']) > * {
2772
2726
  visibility: hidden;
2773
2727
  }
2774
2728
 
2775
- qti-graphic-gap-match-interaction qti-associable-hotspot:state(hover),
2776
- qti-graphic-gap-match-interaction qti-associable-hotspot:state(active) {
2729
+ qti-graphic-gap-match-interaction qti-associable-hotspot:is(:state(hover), [data-state~='hover']),
2730
+ qti-graphic-gap-match-interaction qti-associable-hotspot:is(:state(active), [data-state~='active']) {
2777
2731
  background-color: var(--qti-drop-highlight-bg);
2778
2732
  }
2779
2733
 
2780
- qti-graphic-gap-match-interaction qti-associable-hotspot:state(enabled) {
2734
+ qti-graphic-gap-match-interaction qti-associable-hotspot:is(:state(enabled), [data-state~='enabled']) {
2781
2735
  background-color: var(--qti-drop-highlight-enabled-bg);
2782
2736
  }
2783
2737
  }
@@ -2795,16 +2749,16 @@
2795
2749
  * Moved here from qti-corrections.css, whose unscoped `:state(drag)` rule covered these chips
2796
2750
  * along with everyone else's.
2797
2751
  */
2798
- qti-graphic-gap-match-interaction :state(drag):state(candidate-correct),
2799
- qti-graphic-gap-match-interaction qti-associable-hotspot::part(drag):state(candidate-correct) {
2752
+ qti-graphic-gap-match-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-correct), [data-state~='candidate-correct']),
2753
+ qti-graphic-gap-match-interaction qti-associable-hotspot::part(drag):is(:state(candidate-correct), [data-state~='candidate-correct']) {
2800
2754
  background-color: var(--qti-correct-light);
2801
2755
  color: var(--qti-correct);
2802
2756
  border-color: var(--qti-correct);
2803
2757
  box-shadow: none;
2804
2758
  }
2805
2759
 
2806
- qti-graphic-gap-match-interaction :state(drag):state(candidate-incorrect),
2807
- qti-graphic-gap-match-interaction qti-associable-hotspot::part(drag):state(candidate-incorrect) {
2760
+ qti-graphic-gap-match-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
2761
+ qti-graphic-gap-match-interaction qti-associable-hotspot::part(drag):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
2808
2762
  background-color: var(--qti-incorrect-light);
2809
2763
  color: var(--qti-incorrect);
2810
2764
  border-color: var(--qti-incorrect);
@@ -2824,8 +2778,9 @@
2824
2778
  opacity: var(--text-opacity, var(--qti-component-opacity));
2825
2779
  filter: var(--text-filter, var(--qti-component-filter));
2826
2780
  cursor: text;
2781
+ }
2827
2782
 
2828
- &::part(input) {
2783
+ qti-text-entry-interaction::part(input) {
2829
2784
  color: var(--qti-fg);
2830
2785
  font-family: inherit;
2831
2786
  font-size: 1rem;
@@ -2836,31 +2791,28 @@
2836
2791
  border: none;
2837
2792
  background-color: transparent;
2838
2793
  }
2839
- }
2840
2794
  }
2841
2795
  @layer qti-components.interactions.corrections {
2842
- qti-text-entry-interaction {
2843
- &:state(candidate-partially-correct) {
2796
+ qti-text-entry-interaction:is(:state(candidate-partially-correct), [data-state~='candidate-partially-correct']) {
2844
2797
  --text-background-color: var(--qti-partially-correct-light);
2845
2798
  --text-color: var(--qti-partially-correct);
2846
2799
  --text-border-color: var(--qti-partially-correct);
2847
2800
  --text-box-shadow: none;
2848
2801
  }
2849
2802
 
2850
- &:state(candidate-correct) {
2803
+ qti-text-entry-interaction:is(:state(candidate-correct), [data-state~='candidate-correct']) {
2851
2804
  --text-background-color: var(--qti-correct-light);
2852
2805
  --text-color: var(--qti-correct);
2853
2806
  --text-border-color: var(--qti-correct);
2854
2807
  --text-box-shadow: none;
2855
2808
  }
2856
2809
 
2857
- &:state(candidate-incorrect) {
2810
+ qti-text-entry-interaction:is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
2858
2811
  --text-background-color: var(--qti-incorrect-light);
2859
2812
  --text-color: var(--qti-incorrect);
2860
2813
  --text-border-color: var(--qti-incorrect);
2861
2814
  --text-box-shadow: none;
2862
2815
  }
2863
- }
2864
2816
 
2865
2817
  /* The answer key, moved here from the shared qti-corrections.css: it names this interaction, so
2866
2818
  this file owns it. */
@@ -2868,21 +2820,20 @@
2868
2820
  background-color: var(--qti-answer-bg);
2869
2821
  color: var(--qti-answer-fg);
2870
2822
  border-color: var(--qti-answer-border);
2823
+ }
2871
2824
 
2872
- &::part(input) {
2825
+ :is(.full-correct-response qti-text-entry-interaction)::part(input) {
2873
2826
  color: var(--qti-answer-fg);
2874
2827
  }
2875
2828
 
2876
- &::part(answer) {
2829
+ :is(.full-correct-response qti-text-entry-interaction)::part(answer) {
2877
2830
  content: '';
2878
2831
  background-color: var(--qti-answer-fg);
2879
2832
  mask: var(--qti-check-mask) no-repeat center / contain;
2880
2833
  -webkit-mask: var(--qti-check-mask) no-repeat center / contain;
2881
2834
  }
2882
- }
2883
2835
  }
2884
2836
  @layer qti-components.interactions.base {
2885
- qti-extended-text-interaction {
2886
2837
  /*
2887
2838
  * One block, and the ORDER inside it matters.
2888
2839
  *
@@ -2896,7 +2847,7 @@
2896
2847
  * slot is unset, so it falls through to --qti-component-color, which is `inherit` — the same
2897
2848
  * colour the textarea already had.
2898
2849
  */
2899
- &::part(textarea) {
2850
+ qti-extended-text-interaction::part(textarea) {
2900
2851
  color: var(--qti-fg);
2901
2852
  font-family: inherit;
2902
2853
  font-size: 1rem;
@@ -2954,13 +2905,12 @@
2954
2905
  * that makes the preserved indentation legible. Typography only — the box does not move, so
2955
2906
  * the layout-invariance contract holds.
2956
2907
  */
2957
- &[format='preformatted']::part(textarea) {
2908
+ qti-extended-text-interaction[format='preformatted']::part(textarea) {
2958
2909
  font-family: monospace;
2959
2910
  -moz-tab-size: 2;
2960
2911
  -o-tab-size: 2;
2961
2912
  tab-size: 2;
2962
2913
  }
2963
- }
2964
2914
  }
2965
2915
  /*
2966
2916
  * Corrections for the one interaction that cannot judge itself.
@@ -2981,8 +2931,7 @@
2981
2931
  * positioned by the component and painted by ::part(correction-*) in qti-corrections.css.
2982
2932
  */
2983
2933
  @layer qti-components.interactions.corrections {
2984
- qti-extended-text-interaction {
2985
- &:state(candidate-correct)::part(textarea) {
2934
+ qti-extended-text-interaction:is(:state(candidate-correct), [data-state~='candidate-correct'])::part(textarea) {
2986
2935
  --text-background-color: var(--qti-correct-light);
2987
2936
  --text-color: var(--qti-correct);
2988
2937
  --text-border-color: var(--qti-correct);
@@ -2995,20 +2944,19 @@
2995
2944
  verbatim from the dead class-based rules, where it had never rendered and so was never seen to
2996
2945
  be wrong. With --qti-fg now correctly resolving to `inherit`, it would have been more visibly
2997
2946
  wrong still. */
2998
- &:state(candidate-partially-correct)::part(textarea) {
2947
+ qti-extended-text-interaction:is(:state(candidate-partially-correct), [data-state~='candidate-partially-correct'])::part(textarea) {
2999
2948
  --text-background-color: var(--qti-partially-correct-light);
3000
2949
  --text-color: var(--qti-partially-correct);
3001
2950
  --text-border-color: var(--qti-partially-correct);
3002
2951
  --text-box-shadow: none;
3003
2952
  }
3004
2953
 
3005
- &:state(candidate-incorrect)::part(textarea) {
2954
+ qti-extended-text-interaction:is(:state(candidate-incorrect), [data-state~='candidate-incorrect'])::part(textarea) {
3006
2955
  --text-background-color: var(--qti-incorrect-light);
3007
2956
  --text-color: var(--qti-incorrect);
3008
2957
  --text-border-color: var(--qti-incorrect);
3009
2958
  --text-box-shadow: none;
3010
2959
  }
3011
- }
3012
2960
  }
3013
2961
  /* This interaction's chip vocabulary — the bank chip, the placed chip, and the floating clone. One
3014
2962
  such block per drag-drop interaction, opt-in; graphic-gap-match deliberately declares none. See
@@ -3026,7 +2974,7 @@
3026
2974
  sat on document.body it was in a different tree from the adopted stylesheet, and nothing — no
3027
2975
  part, no attribute — could style it. */
3028
2976
  @layer qti-components.global {
3029
- qti-gap-match-interaction :state(drag),
2977
+ qti-gap-match-interaction :is(:state(drag), [data-state~='drag']),
3030
2978
  [data-drag-clone][data-drag-interaction='qti-gap-match-interaction'] {
3031
2979
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
3032
2980
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
@@ -3077,7 +3025,7 @@
3077
3025
  filter: var(--drag-filter, var(--qti-component-filter));
3078
3026
  }
3079
3027
 
3080
- qti-gap-match-interaction :state(drag)::part(control)::before,
3028
+ qti-gap-match-interaction :is(:state(drag), [data-state~='drag'])::part(control)::before,
3081
3029
  qti-gap-match-interaction::part(drag-control)::before,
3082
3030
  qti-gap-match-interaction ::part(drag-control)::before,
3083
3031
  [data-drag-clone][data-drag-interaction='qti-gap-match-interaction']::part(control)::before {
@@ -3093,20 +3041,19 @@
3093
3041
  }
3094
3042
  }
3095
3043
  @layer qti-components.interactions.base {
3096
- qti-gap-match-interaction {
3097
3044
  /* No qti-selections-light / -dark here. Appendix B.1 of the QTI 3 implementation guide defines
3098
3045
  that class pair for qti-hotspot-interaction and qti-graphic-gap-match-interaction only, and no
3099
3046
  item in this repo ever set it on a gap-match interaction. What was here overrode the neutral
3100
3047
  drag-bank highlight below with --qti-light-* and --qti-dark-* colours that nothing could reach. */
3101
- &:state(dragzone-active)::part(drags) {
3048
+ qti-gap-match-interaction:is(:state(dragzone-active), [data-state~='dragzone-active'])::part(drags) {
3102
3049
  background-color: var(--qti-bg-active);
3103
3050
  }
3104
- &:state(dragzone-enabled)::part(drags) {
3051
+ qti-gap-match-interaction:is(:state(dragzone-enabled), [data-state~='dragzone-enabled'])::part(drags) {
3105
3052
  background-color: var(--qti-bg-active);
3106
3053
  }
3107
3054
  /* Editor fake-drag projected into a filled <qti-gap>'s shadow. Same `drag`
3108
3055
  styling so the editor preview matches runtime when a gap is filled. */
3109
- & qti-gap::part(drag) {
3056
+ qti-gap-match-interaction qti-gap::part(drag) {
3110
3057
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
3111
3058
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
3112
3059
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -3154,20 +3101,20 @@
3154
3101
  color: var(--drag-color, var(--qti-component-color));
3155
3102
  opacity: var(--drag-opacity, var(--qti-component-opacity));
3156
3103
  filter: var(--drag-filter, var(--qti-component-filter));
3157
- &:hover {
3104
+ }
3158
3105
  @media (hover: hover) {
3106
+ :is(qti-gap-match-interaction qti-gap::part(drag)):hover {
3159
3107
  background-color: color-mix(
3160
3108
  in srgb,
3161
3109
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
3162
3110
  black
3163
3111
  );
3164
- }
3165
3112
  }
3166
- &:focus {
3113
+ }
3114
+ :is(qti-gap-match-interaction qti-gap::part(drag)):focus {
3167
3115
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3168
3116
  }
3169
- }
3170
- & qti-gap-text {
3117
+ qti-gap-match-interaction qti-gap-text {
3171
3118
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
3172
3119
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
3173
3120
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -3215,25 +3162,25 @@
3215
3162
  color: var(--drag-color, var(--qti-component-color));
3216
3163
  opacity: var(--drag-opacity, var(--qti-component-opacity));
3217
3164
  filter: var(--drag-filter, var(--qti-component-filter));
3218
- &[dragging] {
3165
+ }
3166
+ [dragging]:is(qti-gap-match-interaction qti-gap-text) {
3219
3167
  pointer-events: none;
3220
3168
  rotate: -2deg;
3221
3169
  --drag-box-shadow: 0 8px 12px rgb(0 0 0 / 20%), 0 4px 8px rgb(0 0 0 / 10%);
3222
3170
  }
3223
- &:hover {
3224
3171
  @media (hover: hover) {
3172
+ :is(qti-gap-match-interaction qti-gap-text):hover {
3225
3173
  background-color: color-mix(
3226
3174
  in srgb,
3227
3175
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
3228
3176
  black
3229
3177
  );
3230
- }
3231
3178
  }
3232
- &:focus {
3179
+ }
3180
+ :is(qti-gap-match-interaction qti-gap-text):focus {
3233
3181
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3234
3182
  }
3235
- }
3236
- & qti-gap {
3183
+ qti-gap-match-interaction qti-gap {
3237
3184
  background-color: var(--drop-background-color, var(--qti-component-background-color));
3238
3185
  background-image: var(--drop-background-image, var(--qti-component-background-image));
3239
3186
  border-width: var(--drop-border-width, var(--qti-component-border-width));
@@ -3261,22 +3208,23 @@
3261
3208
  * every word on the line the instant a chip landed — the exact reflow qti-gap.styles.ts and
3262
3209
  * drag-drop.invariance.spec.ts exist to prevent.
3263
3210
  */
3264
- &:state(filled):is(:not([match-max]), [match-max='1']) {
3211
+ }
3212
+ :is(qti-gap-match-interaction qti-gap):is(:state(filled), [data-state~='filled']):is(:not([match-max]),[match-max='1']) {
3265
3213
  --drop-border-color: transparent;
3266
3214
  }
3267
3215
 
3268
- &[disabled] {
3216
+ [disabled]:is(qti-gap-match-interaction qti-gap) {
3269
3217
  cursor: not-allowed;
3270
3218
  /* `:not(:empty)` tested light-DOM children; the gap renders its chips into its own shadow
3271
3219
  root now, so it is always "empty" to a selector. `:state(filled)` is the occupancy hook. */
3272
- &:state(filled) {
3220
+ }
3221
+ [disabled]:is(qti-gap-match-interaction qti-gap):is(:state(filled), [data-state~='filled']) {
3273
3222
  cursor: default !important;
3274
3223
  }
3275
- }
3276
- &[enabled] {
3224
+ [enabled]:is(qti-gap-match-interaction qti-gap) {
3277
3225
  background-color: var(--qti-bg-active);
3278
3226
  }
3279
- &[active] {
3227
+ [active]:is(qti-gap-match-interaction qti-gap) {
3280
3228
  background-color: var(--qti-bg-active);
3281
3229
  }
3282
3230
  /* The gap's own box — inline-flex, centred, baseline-aligned — is in qti-gap.styles.ts. */
@@ -3305,17 +3253,15 @@
3305
3253
  */
3306
3254
  /* `flex: 0 0 auto` is dropRegion's, in qti-base — a drop hosts a chip, it never sizes one.
3307
3255
  The rotate reset and the outline are paint. */
3308
- &::part(drag) {
3256
+ :is(qti-gap-match-interaction qti-gap)::part(drag) {
3309
3257
  transform: rotate(0);
3310
3258
  /* Through the drag slot so a correction (which sets --drag-box-shadow) composes over it
3311
3259
  instead of this leaking onto a corrected chip. */
3312
3260
  --drag-box-shadow: 0 0 0 1px #e5e7eb;
3313
3261
  }
3314
- }
3315
- }
3316
3262
 
3317
3263
  /* The source and placed chip share one visual component. */
3318
- qti-gap-match-interaction qti-gap-text:state(drag),
3264
+ qti-gap-match-interaction qti-gap-text:is(:state(drag), [data-state~='drag']),
3319
3265
  qti-gap-match-interaction qti-gap::part(drag) {
3320
3266
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
3321
3267
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
@@ -3418,7 +3364,7 @@
3418
3364
  filter: var(--drag-filter, var(--qti-component-filter));
3419
3365
  }
3420
3366
 
3421
- qti-gap-match-interaction qti-gap-text:state(placeholder) {
3367
+ qti-gap-match-interaction qti-gap-text:is(:state(placeholder), [data-state~='placeholder']) {
3422
3368
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
3423
3369
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
3424
3370
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -3429,7 +3375,7 @@
3429
3375
  pointer-events: none;
3430
3376
  }
3431
3377
 
3432
- qti-gap-match-interaction qti-gap-text:state(dragging) {
3378
+ qti-gap-match-interaction qti-gap-text:is(:state(dragging), [data-state~='dragging']) {
3433
3379
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
3434
3380
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
3435
3381
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -3443,18 +3389,18 @@
3443
3389
  pointer-events: none;
3444
3390
  }
3445
3391
 
3446
- qti-gap-match-interaction qti-gap-text:state(return-anchor) {
3392
+ qti-gap-match-interaction qti-gap-text:is(:state(return-anchor), [data-state~='return-anchor']) {
3447
3393
  border-color: var(--qti-drop-highlight-border-color);
3448
3394
  box-shadow: inset 0 0 0 var(--qti-drop-highlight-border-width, 2px)
3449
3395
  color-mix(in srgb, var(--qti-drop-highlight-border-color) 55%, transparent);
3450
3396
  }
3451
3397
 
3452
- qti-gap-match-interaction qti-gap:state(hover),
3453
- qti-gap-match-interaction qti-gap:state(active) {
3398
+ qti-gap-match-interaction qti-gap:is(:state(hover), [data-state~='hover']),
3399
+ qti-gap-match-interaction qti-gap:is(:state(active), [data-state~='active']) {
3454
3400
  background-color: var(--qti-drop-highlight-bg);
3455
3401
  }
3456
3402
 
3457
- qti-gap-match-interaction qti-gap:state(enabled) {
3403
+ qti-gap-match-interaction qti-gap:is(:state(enabled), [data-state~='enabled']) {
3458
3404
  background-color: var(--qti-drop-highlight-enabled-bg);
3459
3405
  }
3460
3406
  }
@@ -3470,16 +3416,16 @@
3470
3416
  DIRECTLY, which outranks the paint binding that reads --drag-background-color, so the solid fill
3471
3417
  they asked for never rendered — a corrected gap-text has always been the pale tint. Left in place
3472
3418
  they would have come alive the moment the shared rule left, and turned solid. */
3473
- qti-gap-match-interaction :state(drag):state(candidate-correct),
3474
- qti-gap::part(drag):state(candidate-correct) {
3419
+ qti-gap-match-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-correct), [data-state~='candidate-correct']),
3420
+ qti-gap::part(drag):is(:state(candidate-correct), [data-state~='candidate-correct']) {
3475
3421
  --drag-background-color: var(--qti-correct-light);
3476
3422
  --drag-color: var(--qti-correct);
3477
3423
  --drag-border-color: var(--qti-correct);
3478
3424
  --drag-box-shadow: none;
3479
3425
  }
3480
3426
 
3481
- qti-gap-match-interaction :state(drag):state(candidate-incorrect),
3482
- qti-gap::part(drag):state(candidate-incorrect) {
3427
+ qti-gap-match-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
3428
+ qti-gap::part(drag):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
3483
3429
  --drag-background-color: var(--qti-incorrect-light);
3484
3430
  --drag-color: var(--qti-incorrect);
3485
3431
  --drag-border-color: var(--qti-incorrect);
@@ -3565,12 +3511,12 @@
3565
3511
  * Selected: a bold, saturated ring — the Fig-54 weight jump. Same var, so poly and box shapes
3566
3512
  * move together; poly additionally swaps to the bold outline mask.
3567
3513
  */
3568
- qti-hotspot-interaction qti-hotspot-choice:state(checked) {
3514
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(checked), [data-state~='checked']) {
3569
3515
  --hs-ring: var(--qti-hotspot-ring-color, var(--qti-selected-bg));
3570
3516
  --hs-width: calc(var(--qti-hotspot-ring-width, 2px) * 2);
3571
3517
  }
3572
3518
 
3573
- qti-hotspot-interaction qti-hotspot-choice[shape='poly']:state(checked) {
3519
+ qti-hotspot-interaction qti-hotspot-choice[shape='poly']:is(:state(checked), [data-state~='checked']) {
3574
3520
  mask-image: var(--qti-shape-outline-bold);
3575
3521
  -webkit-mask-image: var(--qti-shape-outline-bold);
3576
3522
  }
@@ -3604,13 +3550,13 @@
3604
3550
  * In `states` so it outranks the base ring and the `:hover` strengthen: hover must NOT reveal, or
3605
3551
  * sweeping the pointer hands back the answer regions one at a time.
3606
3552
  */
3607
- qti-hotspot-interaction.qti-unselected-hidden qti-hotspot-choice:not(:state(checked)):not(:focus-visible) {
3553
+ qti-hotspot-interaction.qti-unselected-hidden qti-hotspot-choice:not(:is(:state(checked), [data-state~='checked'])):not(:focus-visible) {
3608
3554
  --hs-ring: transparent;
3609
3555
  --hs-halo: transparent;
3610
3556
  }
3611
3557
 
3612
- qti-hotspot-interaction qti-hotspot-choice:state(disabled),
3613
- qti-hotspot-interaction qti-hotspot-choice:state(readonly) {
3558
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(disabled), [data-state~='disabled']),
3559
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(readonly), [data-state~='readonly']) {
3614
3560
  cursor: not-allowed;
3615
3561
  pointer-events: none;
3616
3562
  }
@@ -3620,19 +3566,19 @@
3620
3566
  * Correction and answer-key views recolour the same ring — one var, so circle and poly stay in
3621
3567
  * step. Answer-key first, so a candidate's own judgement wins when both are present.
3622
3568
  */
3623
- qti-hotspot-interaction qti-hotspot-choice:state(correct-response) {
3569
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(correct-response), [data-state~='correct-response']) {
3624
3570
  --qti-hotspot-ring-color: var(--qti-answer-border);
3625
3571
  }
3626
3572
 
3627
- qti-hotspot-interaction qti-hotspot-choice:state(candidate-correct) {
3573
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(candidate-correct), [data-state~='candidate-correct']) {
3628
3574
  --qti-hotspot-ring-color: var(--qti-correct);
3629
3575
  }
3630
3576
 
3631
- qti-hotspot-interaction qti-hotspot-choice:state(candidate-incorrect) {
3577
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
3632
3578
  --qti-hotspot-ring-color: var(--qti-incorrect);
3633
3579
  }
3634
3580
 
3635
- .full-correct-response qti-hotspot-interaction qti-hotspot-choice:state(checked) {
3581
+ .full-correct-response qti-hotspot-interaction qti-hotspot-choice:is(:state(checked), [data-state~='checked']) {
3636
3582
  --qti-hotspot-ring-color: var(--qti-answer-border);
3637
3583
  }
3638
3584
 
@@ -3642,18 +3588,18 @@
3642
3588
  * candidate-* hotspot is usually also :state(checked) and would be bold anyway; correct-response
3643
3589
  * on an unselected region is the case this covers.)
3644
3590
  */
3645
- qti-hotspot-interaction qti-hotspot-choice:state(correct-response),
3646
- qti-hotspot-interaction qti-hotspot-choice:state(candidate-correct),
3647
- qti-hotspot-interaction qti-hotspot-choice:state(candidate-incorrect),
3648
- .full-correct-response qti-hotspot-interaction qti-hotspot-choice:state(checked) {
3591
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(correct-response), [data-state~='correct-response']),
3592
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(candidate-correct), [data-state~='candidate-correct']),
3593
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
3594
+ .full-correct-response qti-hotspot-interaction qti-hotspot-choice:is(:state(checked), [data-state~='checked']) {
3649
3595
  --hs-ring: var(--qti-hotspot-ring-color);
3650
3596
  --hs-width: calc(var(--qti-hotspot-ring-width, 2px) * 2);
3651
3597
  }
3652
3598
 
3653
- qti-hotspot-interaction qti-hotspot-choice[shape='poly']:state(correct-response),
3654
- qti-hotspot-interaction qti-hotspot-choice[shape='poly']:state(candidate-correct),
3655
- qti-hotspot-interaction qti-hotspot-choice[shape='poly']:state(candidate-incorrect),
3656
- .full-correct-response qti-hotspot-interaction qti-hotspot-choice[shape='poly']:state(checked) {
3599
+ qti-hotspot-interaction qti-hotspot-choice[shape='poly']:is(:state(correct-response), [data-state~='correct-response']),
3600
+ qti-hotspot-interaction qti-hotspot-choice[shape='poly']:is(:state(candidate-correct), [data-state~='candidate-correct']),
3601
+ qti-hotspot-interaction qti-hotspot-choice[shape='poly']:is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
3602
+ .full-correct-response qti-hotspot-interaction qti-hotspot-choice[shape='poly']:is(:state(checked), [data-state~='checked']) {
3657
3603
  mask-image: var(--qti-shape-outline-bold);
3658
3604
  -webkit-mask-image: var(--qti-shape-outline-bold);
3659
3605
  }
@@ -3678,118 +3624,89 @@
3678
3624
  * target's active border: pay the space once, up front, so the state has only to repaint. Every
3679
3625
  * hottext is now the size a selected one used to be, which is also a larger target to click.
3680
3626
  */
3681
- qti-hottext {
3627
+ }
3628
+ qti-hottext-interaction qti-hottext {
3682
3629
  padding: var(--qti-hottext-selected-padding);
3683
3630
  }
3684
-
3685
- &.qti-input-control-hidden {
3686
- qti-hottext {
3631
+ qti-hottext-interaction.qti-input-control-hidden qti-hottext {
3687
3632
  border: var(--qti-border);
3688
3633
  border-radius: var(--qti-border-radius);
3689
3634
 
3690
3635
  background: var(--qti-bg);
3636
+ }
3691
3637
 
3692
3638
 
3693
- &:focus {
3639
+ :is(qti-hottext-interaction.qti-input-control-hidden qti-hottext):focus {
3694
3640
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3695
3641
  }
3696
- }
3697
- }
3698
-
3699
- &.qti-unselected-hidden {
3700
- qti-hottext {
3642
+ qti-hottext-interaction.qti-unselected-hidden qti-hottext {
3701
3643
  border: 0;
3702
3644
  border-radius: var(--qti-border-radius);
3703
3645
  background-color: transparent;
3704
3646
  outline: none;
3705
3647
  cursor: pointer;
3648
+ }
3706
3649
 
3707
3650
 
3708
- &:focus {
3651
+ :is(qti-hottext-interaction.qti-unselected-hidden qti-hottext):focus {
3709
3652
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3710
3653
  }
3711
- }
3712
- }
3713
-
3714
- &:not(.qti-input-control-hidden, .qti-unselected-hidden) {
3715
- qti-hottext {
3654
+ qti-hottext-interaction:not(.qti-input-control-hidden,.qti-unselected-hidden) qti-hottext {
3716
3655
  border: var(--qti-border);
3717
3656
  border-radius: var(--qti-border-radius);
3657
+ }
3718
3658
 
3719
3659
 
3720
- &:focus {
3660
+ :is(qti-hottext-interaction:not(.qti-input-control-hidden,.qti-unselected-hidden) qti-hottext):focus {
3721
3661
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3722
3662
  }
3723
- }
3724
- }
3725
- }
3726
3663
  }
3727
3664
  @layer qti-components.interactions.states {
3728
- qti-hottext-interaction {
3729
- &.qti-input-control-hidden qti-hottext {
3730
- &:state(checked) {
3665
+ :is(qti-hottext-interaction.qti-input-control-hidden qti-hottext):is(:state(checked), [data-state~='checked']) {
3731
3666
  border-color: var(--qti-border-active);
3732
3667
  background-color: var(--qti-selected-bg);
3733
3668
  color: var(--qti-selected-color);
3734
3669
  }
3735
3670
 
3736
- &:state(readonly) {
3671
+ :is(qti-hottext-interaction.qti-input-control-hidden qti-hottext):is(:state(readonly), [data-state~='readonly']) {
3737
3672
  cursor: pointer;
3738
3673
  }
3739
3674
 
3740
- &:state(disabled) {
3675
+ :is(qti-hottext-interaction.qti-input-control-hidden qti-hottext):is(:state(disabled), [data-state~='disabled']) {
3741
3676
  cursor: not-allowed;
3742
3677
  }
3743
- }
3744
-
3745
- &.qti-unselected-hidden qti-hottext,
3746
- &:not(.qti-input-control-hidden, .qti-unselected-hidden) qti-hottext {
3747
- &:state(checked) {
3678
+ :is(qti-hottext-interaction.qti-unselected-hidden qti-hottext,qti-hottext-interaction:not(.qti-input-control-hidden,.qti-unselected-hidden) qti-hottext):is(:state(checked), [data-state~='checked']) {
3748
3679
  background-color: var(--qti-selected-bg);
3749
3680
  color: var(--qti-selected-color);
3750
3681
  }
3751
3682
 
3752
- &:state(readonly) {
3683
+ :is(qti-hottext-interaction.qti-unselected-hidden qti-hottext,qti-hottext-interaction:not(.qti-input-control-hidden,.qti-unselected-hidden) qti-hottext):is(:state(readonly), [data-state~='readonly']) {
3753
3684
  cursor: pointer;
3754
3685
  }
3755
3686
 
3756
- &:state(disabled) {
3687
+ :is(qti-hottext-interaction.qti-unselected-hidden qti-hottext,qti-hottext-interaction:not(.qti-input-control-hidden,.qti-unselected-hidden) qti-hottext):is(:state(disabled), [data-state~='disabled']) {
3757
3688
  cursor: not-allowed;
3758
3689
  }
3759
- }
3760
- }
3761
3690
  }
3762
3691
  @layer qti-components.interactions.corrections {
3763
- qti-hottext-interaction {
3764
- qti-hottext {
3765
- &:state(correct-response),
3766
- &[data-correct-response='true'] {
3767
- &::after {
3692
+ :is(:is(qti-hottext-interaction qti-hottext):is(:state(correct-response), [data-state~='correct-response']),[data-correct-response='true']:is(qti-hottext-interaction qti-hottext))::after {
3768
3693
  content: '\02714';
3769
3694
  color: var(--qti-correct);
3770
3695
  }
3771
- }
3772
- }
3773
-
3774
- &.qti-input-control-hidden qti-hottext {
3775
- &:state(candidate-correct) {
3696
+ :is(qti-hottext-interaction.qti-input-control-hidden qti-hottext):is(:state(candidate-correct), [data-state~='candidate-correct']) {
3776
3697
  background-color: var(--qti-correct-light);
3777
3698
  color: var(--qti-correct);
3778
3699
  border-color: var(--qti-correct);
3779
3700
  box-shadow: none;
3780
3701
  }
3781
3702
 
3782
- &:state(candidate-incorrect) {
3703
+ :is(qti-hottext-interaction.qti-input-control-hidden qti-hottext):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
3783
3704
  background-color: var(--qti-incorrect-light);
3784
3705
  color: var(--qti-incorrect);
3785
3706
  border-color: var(--qti-incorrect);
3786
3707
  box-shadow: none;
3787
3708
  }
3788
- }
3789
-
3790
- &.qti-unselected-hidden qti-hottext,
3791
- &:not(.qti-input-control-hidden, .qti-unselected-hidden) qti-hottext {
3792
- &:state(candidate-correct) {
3709
+ :is(qti-hottext-interaction.qti-unselected-hidden qti-hottext,qti-hottext-interaction:not(.qti-input-control-hidden,.qti-unselected-hidden) qti-hottext):is(:state(candidate-correct), [data-state~='candidate-correct']) {
3793
3710
  background-color: var(--qti-correct-light);
3794
3711
  color: var(--qti-correct);
3795
3712
  border-color: var(--qti-correct);
@@ -3797,21 +3714,19 @@
3797
3714
  box-shadow: inset 0 0 0 var(--qti-border-thickness) var(--qti-correct);
3798
3715
  }
3799
3716
 
3800
- &:state(candidate-incorrect) {
3717
+ :is(qti-hottext-interaction.qti-unselected-hidden qti-hottext,qti-hottext-interaction:not(.qti-input-control-hidden,.qti-unselected-hidden) qti-hottext):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
3801
3718
  background-color: var(--qti-incorrect-light);
3802
3719
  color: var(--qti-incorrect);
3803
3720
  border-color: var(--qti-incorrect);
3804
3721
  box-shadow: none;
3805
3722
  box-shadow: inset 0 0 0 var(--qti-border-thickness) var(--qti-incorrect);
3806
3723
  }
3807
- }
3808
- }
3809
3724
 
3810
- .full-correct-response qti-hottext-interaction.qti-input-control-hidden qti-hottext:state(checked),
3811
- .full-correct-response qti-hottext-interaction.qti-unselected-hidden qti-hottext:state(checked),
3725
+ .full-correct-response qti-hottext-interaction.qti-input-control-hidden qti-hottext:is(:state(checked), [data-state~='checked']),
3726
+ .full-correct-response qti-hottext-interaction.qti-unselected-hidden qti-hottext:is(:state(checked), [data-state~='checked']),
3812
3727
  .full-correct-response
3813
3728
  qti-hottext-interaction:not(.qti-input-control-hidden, .qti-unselected-hidden)
3814
- qti-hottext:state(checked) {
3729
+ qti-hottext:is(:state(checked), [data-state~='checked']) {
3815
3730
  background-color: var(--qti-answer-bg);
3816
3731
  color: var(--qti-answer-fg);
3817
3732
  border-color: var(--qti-answer-border);
@@ -3822,7 +3737,8 @@
3822
3737
  padding-inline-end: var(--qti-gap);
3823
3738
 
3824
3739
  /* The icon must not become the inline-flex container's first baseline item. */
3825
- &::before {
3740
+ }
3741
+ :is(.full-correct-response qti-hottext-interaction.qti-input-control-hidden qti-hottext:is(:state(checked), [data-state~='checked']),.full-correct-response qti-hottext-interaction.qti-unselected-hidden qti-hottext:is(:state(checked), [data-state~='checked']),.full-correct-response qti-hottext-interaction:not(.qti-input-control-hidden,.qti-unselected-hidden) qti-hottext:is(:state(checked), [data-state~='checked']))::before {
3826
3742
  content: '';
3827
3743
  background-color: var(--qti-answer-fg);
3828
3744
  mask: var(--qti-check-mask) no-repeat center / contain;
@@ -3836,7 +3752,6 @@
3836
3752
  width: 1em;
3837
3753
  height: 1em;
3838
3754
  }
3839
- }
3840
3755
  }
3841
3756
  @layer qti-components.interactions.base {
3842
3757
  qti-inline-choice-interaction {
@@ -3858,7 +3773,8 @@
3858
3773
  --field-color: var(--qti-fg);
3859
3774
 
3860
3775
  /* ── Trigger: transparent inside the field border ───────────────────────── */
3861
- &::part(trigger) {
3776
+ }
3777
+ qti-inline-choice-interaction::part(trigger) {
3862
3778
  padding: var(--qti-padding-box);
3863
3779
  background: transparent;
3864
3780
  border: none;
@@ -3868,27 +3784,25 @@
3868
3784
  font-size: inherit;
3869
3785
  }
3870
3786
 
3871
- &::part(trigger):focus-visible {
3787
+ qti-inline-choice-interaction::part(trigger):focus-visible {
3872
3788
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3873
3789
  outline: none;
3874
3790
  }
3875
3791
 
3876
- &:state(disabled)::part(trigger),
3877
- &[disabled]::part(trigger) {
3792
+ qti-inline-choice-interaction:is(:state(disabled), [data-state~='disabled'])::part(trigger),qti-inline-choice-interaction[disabled]::part(trigger) {
3878
3793
  cursor: not-allowed;
3879
3794
  }
3880
3795
 
3881
- &:state(readonly)::part(trigger),
3882
- &[readonly]::part(trigger) {
3796
+ qti-inline-choice-interaction:is(:state(readonly), [data-state~='readonly'])::part(trigger),qti-inline-choice-interaction[readonly]::part(trigger) {
3883
3797
  cursor: pointer;
3884
3798
  }
3885
3799
 
3886
3800
  /* ── Chevron icon: color only; transform/mask/size live in shadow DOM ────── */
3887
- &::part(dropdown-icon) {
3801
+ qti-inline-choice-interaction::part(dropdown-icon) {
3888
3802
  color: inherit;
3889
3803
  }
3890
3804
 
3891
- &::part(dropdown-icon-open) {
3805
+ qti-inline-choice-interaction::part(dropdown-icon-open) {
3892
3806
  color: inherit;
3893
3807
  }
3894
3808
 
@@ -3906,7 +3820,7 @@
3906
3820
  * above it. Two spacing tokens for one box is the whole bug — the vocabulary says pick one and
3907
3821
  * spend it whole (see the CONTRACT note on --qti-padding-box / -inline / --qti-gap).
3908
3822
  */
3909
- &::part(menu) {
3823
+ qti-inline-choice-interaction::part(menu) {
3910
3824
  background-color: var(--menu-background-color, var(--qti-component-background-color));
3911
3825
  background-image: var(--menu-background-image, var(--qti-component-background-image));
3912
3826
  border-width: var(--menu-border-width, var(--qti-component-border-width));
@@ -3940,18 +3854,18 @@
3940
3854
  * position-try fallback applied", so fixing that needs a signal from script, and it is not worth
3941
3855
  * one for the rarer case.
3942
3856
  */
3943
- &:state(open) {
3857
+ qti-inline-choice-interaction:is(:state(open), [data-state~='open']) {
3944
3858
  border-end-start-radius: 0;
3945
3859
  border-end-end-radius: 0;
3946
3860
  }
3947
3861
 
3948
- &::part(menu):popover-open {
3862
+ qti-inline-choice-interaction::part(menu):popover-open {
3949
3863
  border-start-start-radius: 0;
3950
3864
  border-start-end-radius: 0;
3951
3865
  }
3952
3866
 
3953
3867
  /* ── Option rows ─────────────────────────────────────────────────────────── */
3954
- &::part(option) {
3868
+ qti-inline-choice-interaction::part(option) {
3955
3869
  padding: var(--qti-padding-box);
3956
3870
  border: 0;
3957
3871
  /* Square, because a row now spans the full width of the menu. The menu's own radius still
@@ -3962,19 +3876,19 @@
3962
3876
  cursor: pointer;
3963
3877
  }
3964
3878
 
3965
- &::part(option):focus-visible {
3879
+ qti-inline-choice-interaction::part(option):focus-visible {
3966
3880
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3967
3881
  outline: none;
3968
3882
  }
3969
3883
 
3970
- &::part(option-selected) {
3884
+ qti-inline-choice-interaction::part(option-selected) {
3971
3885
  background-color: var(--qti-bg-active);
3972
3886
  }
3973
3887
 
3974
3888
  /* ── Slotted qti-inline-choice elements (light DOM) ──────────────────────── */
3975
3889
  /* The option row's own box — flex, centred, single-line, clipped — is in
3976
3890
  qti-inline-choice.styles.ts. */
3977
- & qti-inline-choice {
3891
+ qti-inline-choice-interaction qti-inline-choice {
3978
3892
  width: 100%;
3979
3893
  box-sizing: border-box;
3980
3894
  padding: var(--qti-padding-box);
@@ -3990,19 +3904,19 @@
3990
3904
  text-overflow: ellipsis;
3991
3905
  }
3992
3906
 
3993
- & qti-inline-choice:focus-visible {
3907
+ qti-inline-choice-interaction qti-inline-choice:focus-visible {
3994
3908
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3995
3909
  outline: none;
3996
3910
  }
3997
3911
 
3998
- & qti-inline-choice:state(checked) {
3912
+ qti-inline-choice-interaction qti-inline-choice:is(:state(checked), [data-state~='checked']) {
3999
3913
  background-color: var(--qti-bg-active);
4000
3914
  }
4001
3915
 
4002
3916
  /* ── Validation message: a warning box, painted through the contract. Its non-neutral bits
4003
3917
  (fg text, amber-tinted fill, amber border) are --message-* overrides; width/style/radius
4004
3918
  come from the defaults. ── */
4005
- &::part(message) {
3919
+ qti-inline-choice-interaction::part(message) {
4006
3920
  background-color: var(--message-background-color, var(--qti-component-background-color));
4007
3921
  background-image: var(--message-background-image, var(--qti-component-background-image));
4008
3922
  border-width: var(--message-border-width, var(--qti-component-border-width));
@@ -4018,7 +3932,6 @@
4018
3932
  --message-border-color: var(--qti-warning);
4019
3933
  padding: var(--qti-padding-box);
4020
3934
  }
4021
- }
4022
3935
  }
4023
3936
  @layer qti-components.interactions.corrections {
4024
3937
  /* The answer key, moved here from the shared qti-corrections.css: it names this interaction, so
@@ -4027,35 +3940,32 @@
4027
3940
  background-color: var(--qti-answer-bg);
4028
3941
  color: var(--qti-answer-fg);
4029
3942
  border-color: var(--qti-answer-border);
3943
+ }
4030
3944
 
4031
- &::part(value) {
3945
+ :is(.full-correct-response qti-inline-choice-interaction)::part(value) {
4032
3946
  color: var(--qti-answer-fg);
4033
3947
  }
4034
- }
4035
-
4036
- qti-inline-choice-interaction {
4037
- &:state(candidate-correct) {
3948
+ qti-inline-choice-interaction:is(:state(candidate-correct), [data-state~='candidate-correct']) {
4038
3949
  --field-background-color: var(--qti-correct-light);
4039
3950
  --field-color: var(--qti-correct);
4040
3951
  --field-border-color: var(--qti-correct);
4041
3952
  --field-box-shadow: none;
3953
+ }
4042
3954
 
4043
- &::part(trigger) {
3955
+ qti-inline-choice-interaction:is(:state(candidate-correct), [data-state~='candidate-correct'])::part(trigger) {
4044
3956
  background: transparent;
4045
3957
  }
4046
- }
4047
3958
 
4048
- &:state(candidate-incorrect) {
3959
+ qti-inline-choice-interaction:is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
4049
3960
  --field-background-color: var(--qti-incorrect-light);
4050
3961
  --field-color: var(--qti-incorrect);
4051
3962
  --field-border-color: var(--qti-incorrect);
4052
3963
  --field-box-shadow: none;
3964
+ }
4053
3965
 
4054
- &::part(trigger) {
3966
+ qti-inline-choice-interaction:is(:state(candidate-incorrect), [data-state~='candidate-incorrect'])::part(trigger) {
4055
3967
  background: transparent;
4056
3968
  }
4057
- }
4058
- }
4059
3969
  }
4060
3970
  /* This interaction's chip vocabulary — the bank chip, the placed chip, and the floating clone. One
4061
3971
  such block per drag-drop interaction, opt-in; graphic-gap-match deliberately declares none. See
@@ -4073,7 +3983,7 @@
4073
3983
  sat on document.body it was in a different tree from the adopted stylesheet, and nothing — no
4074
3984
  part, no attribute — could style it. */
4075
3985
  @layer qti-components.global {
4076
- qti-match-interaction :state(drag),
3986
+ qti-match-interaction :is(:state(drag), [data-state~='drag']),
4077
3987
  [data-drag-clone][data-drag-interaction='qti-match-interaction'] {
4078
3988
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
4079
3989
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
@@ -4124,7 +4034,7 @@
4124
4034
  filter: var(--drag-filter, var(--qti-component-filter));
4125
4035
  }
4126
4036
 
4127
- qti-match-interaction :state(drag)::part(control)::before,
4037
+ qti-match-interaction :is(:state(drag), [data-state~='drag'])::part(control)::before,
4128
4038
  qti-match-interaction::part(drag-control)::before,
4129
4039
  qti-match-interaction ::part(drag-control)::before,
4130
4040
  [data-drag-clone][data-drag-interaction='qti-match-interaction']::part(control)::before {
@@ -4191,19 +4101,19 @@
4191
4101
  color: var(--drag-color, var(--qti-component-color));
4192
4102
  opacity: var(--drag-opacity, var(--qti-component-opacity));
4193
4103
  filter: var(--drag-filter, var(--qti-component-filter));
4194
- &:hover {
4104
+ }
4195
4105
  @media (hover: hover) {
4106
+ :is(qti-simple-associable-choice::part(drag)):hover {
4196
4107
  background-color: color-mix(
4197
4108
  in srgb,
4198
4109
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
4199
4110
  black
4200
4111
  );
4201
- }
4202
4112
  }
4203
- &:focus {
4113
+ }
4114
+ :is(qti-simple-associable-choice::part(drag)):focus {
4204
4115
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
4205
4116
  }
4206
- }
4207
4117
 
4208
4118
  qti-match-interaction:not(.qti-match-tabular) {
4209
4119
  /*
@@ -4225,9 +4135,11 @@
4225
4135
 
4226
4136
  /* The bank and the target grid are laid out in qti-match-interaction.styles.ts. Only the gaps
4227
4137
  are still set here — gap is spacing. */
4228
- & qti-simple-match-set:first-of-type {
4138
+ }
4139
+ qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:first-of-type {
4229
4140
  gap: var(--qti-gap);
4230
- & qti-simple-associable-choice {
4141
+ }
4142
+ :is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:first-of-type) qti-simple-associable-choice {
4231
4143
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
4232
4144
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
4233
4145
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -4275,44 +4187,41 @@
4275
4187
  color: var(--drag-color, var(--qti-component-color));
4276
4188
  opacity: var(--drag-opacity, var(--qti-component-opacity));
4277
4189
  filter: var(--drag-filter, var(--qti-component-filter));
4278
- &[dragging] {
4190
+ }
4191
+ [dragging]:is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:first-of-type) qti-simple-associable-choice) {
4279
4192
  pointer-events: none;
4280
4193
  rotate: -2deg;
4281
4194
  --drag-box-shadow: 0 8px 12px rgb(0 0 0 / 20%), 0 4px 8px rgb(0 0 0 / 10%);
4282
4195
  }
4283
- &:hover {
4284
4196
  @media (hover: hover) {
4197
+ :is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:first-of-type) qti-simple-associable-choice):hover {
4285
4198
  background-color: color-mix(
4286
4199
  in srgb,
4287
4200
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
4288
4201
  black
4289
4202
  );
4290
- }
4291
4203
  }
4292
- &:focus {
4204
+ }
4205
+ :is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:first-of-type) qti-simple-associable-choice):focus {
4293
4206
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
4294
4207
  }
4295
- }
4296
- }
4297
- & qti-simple-match-set:last-of-type {
4208
+ qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type {
4298
4209
  gap: var(--qti-gap);
4299
- & > qti-simple-associable-choice {
4300
- & img {
4210
+ }
4211
+ :is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type) > qti-simple-associable-choice) img {
4301
4212
  max-width: 100%;
4302
4213
  height: auto;
4303
4214
  }
4304
- &[disabled] {
4305
- &::part(drop) {
4215
+ [disabled]:is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type) > qti-simple-associable-choice)::part(drop) {
4306
4216
  cursor: not-allowed;
4307
4217
  }
4308
- }
4309
4218
  /*
4310
4219
  * No `[active]` highlight. Lifting a chip used to draw a solid `--qti-border-active` border
4311
4220
  * around every drop container at once, which says "any of these" — the least useful thing to
4312
4221
  * say. What the candidate wants to see is the hole the chip left, and that is painted on the
4313
4222
  * chip's own `:state(dragging)` / `:state(placeholder)`.
4314
4223
  */
4315
- &::part(drop) {
4224
+ :is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type) > qti-simple-associable-choice)::part(drop) {
4316
4225
  background-color: var(--drop-background-color, var(--qti-component-background-color));
4317
4226
  background-image: var(--drop-background-image, var(--qti-component-background-image));
4318
4227
  border-width: var(--drop-border-width, var(--qti-component-border-width));
@@ -4330,20 +4239,22 @@
4330
4239
  used to LEAK: `dragging` sets --component-box-shadow, one shared namespace, so it
4331
4240
  inherited onto every chip nested in the drop. That half is now structural — `dragging`
4332
4241
  sets --drag-box-shadow, which a drop does not read.) */
4333
- &:focus {
4242
+ }
4243
+ :is(:is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type) > qti-simple-associable-choice)::part(drop)):focus {
4334
4244
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
4335
4245
  }
4246
+ :is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type) > qti-simple-associable-choice)::part(drop) {
4336
4247
  margin-top: var(--qti-gap);
4337
4248
  gap: var(--qti-gap);
4338
4249
  }
4339
- & > *:not(qti-simple-associable-choice) {
4250
+ :is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type) > qti-simple-associable-choice) > *:not(qti-simple-associable-choice) {
4340
4251
  pointer-events: none;
4341
4252
  }
4342
4253
  /* A chip nested in a drop target renders no drop region of its own. That is the component's
4343
4254
  own rule — qti-simple-associable-choice.styles.ts hides [part~='drop'] for both
4344
4255
  :host(:state(drag)) and :host([part~='drag']), which covers the placed clone this
4345
4256
  selector could not reach. */
4346
- & > qti-simple-associable-choice {
4257
+ :is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type) > qti-simple-associable-choice) > qti-simple-associable-choice {
4347
4258
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
4348
4259
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
4349
4260
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -4391,26 +4302,23 @@
4391
4302
  color: var(--drag-color, var(--qti-component-color));
4392
4303
  opacity: var(--drag-opacity, var(--qti-component-opacity));
4393
4304
  filter: var(--drag-filter, var(--qti-component-filter));
4394
- &:hover {
4305
+ }
4395
4306
  @media (hover: hover) {
4307
+ :is(:is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type) > qti-simple-associable-choice) > qti-simple-associable-choice):hover {
4396
4308
  background-color: color-mix(
4397
4309
  in srgb,
4398
4310
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
4399
4311
  black
4400
4312
  );
4401
- }
4402
4313
  }
4403
- &:focus {
4314
+ }
4315
+ :is(:is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type) > qti-simple-associable-choice) > qti-simple-associable-choice):focus {
4404
4316
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
4405
4317
  }
4406
- }
4407
- }
4408
- }
4409
- }
4410
4318
 
4411
4319
  qti-match-interaction:not(.qti-match-tabular)
4412
4320
  qti-simple-match-set:first-of-type
4413
- qti-simple-associable-choice:state(drag),
4321
+ qti-simple-associable-choice:is(:state(drag), [data-state~='drag']),
4414
4322
  qti-match-interaction:not(.qti-match-tabular) qti-simple-associable-choice::part(drag) {
4415
4323
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
4416
4324
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
@@ -4460,35 +4368,31 @@
4460
4368
  opacity: var(--drag-opacity, var(--qti-component-opacity));
4461
4369
  filter: var(--drag-filter, var(--qti-component-filter));
4462
4370
  }
4463
-
4464
- qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type > qti-simple-associable-choice {
4465
4371
  /* The label's `align-content` and the drop's `flex-grow` are the target card's own box, and live
4466
4372
  in qti-simple-associable-choice.styles.ts under :host(:state(droppable)). `box-sizing` went with
4467
4373
  them: border-box is already the reset, and content-box is what dropRegion sets. */
4468
- &::part(label) {
4374
+ :is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type > qti-simple-associable-choice)::part(label) {
4469
4375
  border: var(--qti-border);
4470
4376
  border-top-left-radius: var(--qti-border-radius);
4471
4377
  border-top-right-radius: var(--qti-border-radius);
4472
4378
  padding: 0.5rem;
4473
4379
  }
4474
4380
 
4475
- &::part(drop) {
4381
+ :is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type > qti-simple-associable-choice)::part(drop) {
4476
4382
  margin-top: 0;
4477
4383
  border-top: none;
4478
4384
  border-top-left-radius: 0;
4479
4385
  border-top-right-radius: 0;
4480
4386
  background: none;
4481
4387
  }
4482
-
4483
- }
4484
4388
  }
4485
4389
  @layer qti-components.interactions.states {
4486
4390
  qti-match-interaction:not(.qti-match-tabular)
4487
4391
  qti-simple-match-set:first-of-type
4488
- qti-simple-associable-choice:state(dragging),
4392
+ qti-simple-associable-choice:is(:state(dragging), [data-state~='dragging']),
4489
4393
  qti-match-interaction:not(.qti-match-tabular)
4490
4394
  qti-simple-match-set:first-of-type
4491
- qti-simple-associable-choice:state(placeholder) {
4395
+ qti-simple-associable-choice:is(:state(placeholder), [data-state~='placeholder']) {
4492
4396
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
4493
4397
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
4494
4398
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -4501,42 +4405,42 @@
4501
4405
 
4502
4406
  qti-match-interaction:not(.qti-match-tabular)
4503
4407
  qti-simple-match-set:first-of-type
4504
- qti-simple-associable-choice:state(dragging)
4408
+ qti-simple-associable-choice:is(:state(dragging), [data-state~='dragging'])
4505
4409
  > *,
4506
4410
  qti-match-interaction:not(.qti-match-tabular)
4507
4411
  qti-simple-match-set:first-of-type
4508
- qti-simple-associable-choice:state(placeholder)
4412
+ qti-simple-associable-choice:is(:state(placeholder), [data-state~='placeholder'])
4509
4413
  > * {
4510
4414
  visibility: hidden;
4511
4415
  }
4512
4416
 
4513
4417
  qti-match-interaction:not(.qti-match-tabular)
4514
4418
  qti-simple-match-set:first-of-type
4515
- qti-simple-associable-choice:state(dragging)::part(label),
4419
+ qti-simple-associable-choice:is(:state(dragging), [data-state~='dragging'])::part(label),
4516
4420
  qti-match-interaction:not(.qti-match-tabular)
4517
4421
  qti-simple-match-set:first-of-type
4518
- qti-simple-associable-choice:state(placeholder)::part(label) {
4422
+ qti-simple-associable-choice:is(:state(placeholder), [data-state~='placeholder'])::part(label) {
4519
4423
  visibility: hidden;
4520
4424
  }
4521
4425
 
4522
4426
  qti-match-interaction:not(.qti-match-tabular)
4523
4427
  qti-simple-match-set:last-of-type
4524
- > qti-simple-associable-choice:state(hover)::part(drop),
4428
+ > qti-simple-associable-choice:is(:state(hover), [data-state~='hover'])::part(drop),
4525
4429
  qti-match-interaction:not(.qti-match-tabular)
4526
4430
  qti-simple-match-set:last-of-type
4527
- > qti-simple-associable-choice:state(active)::part(drop) {
4431
+ > qti-simple-associable-choice:is(:state(active), [data-state~='active'])::part(drop) {
4528
4432
  background-color: var(--qti-drop-highlight-bg);
4529
4433
  }
4530
4434
 
4531
4435
  qti-match-interaction:not(.qti-match-tabular)
4532
4436
  qti-simple-match-set:last-of-type
4533
- > qti-simple-associable-choice:state(enabled)::part(drop) {
4437
+ > qti-simple-associable-choice:is(:state(enabled), [data-state~='enabled'])::part(drop) {
4534
4438
  background-color: var(--qti-drop-highlight-enabled-bg);
4535
4439
  }
4536
4440
 
4537
4441
  qti-match-interaction:not(.qti-match-tabular)
4538
4442
  qti-simple-match-set:first-of-type
4539
- qti-simple-associable-choice:state(dragging) {
4443
+ qti-simple-associable-choice:is(:state(dragging), [data-state~='dragging']) {
4540
4444
  border-color: var(--qti-drop-highlight-border-color);
4541
4445
  box-shadow: inset 0 0 0 var(--qti-drop-highlight-border-width, 2px)
4542
4446
  color-mix(in srgb, var(--qti-drop-highlight-border-color) 55%, transparent);
@@ -4544,7 +4448,7 @@
4544
4448
 
4545
4449
  qti-match-interaction:not(.qti-match-tabular)
4546
4450
  qti-simple-match-set:first-of-type
4547
- qti-simple-associable-choice:state(return-anchor) {
4451
+ qti-simple-associable-choice:is(:state(return-anchor), [data-state~='return-anchor']) {
4548
4452
  border-color: var(--qti-drop-highlight-border-color);
4549
4453
  box-shadow: inset 0 0 0 var(--qti-drop-highlight-border-width, 2px)
4550
4454
  color-mix(in srgb, var(--qti-drop-highlight-border-color) 55%, transparent);
@@ -4561,16 +4465,16 @@
4561
4465
  same reason as gap-match's: the shared rule's DIRECT `background-color` outranked the binding
4562
4466
  that reads them, so their solid fill never rendered. They out-specify this block, so leaving them
4563
4467
  would have flipped a corrected placed chip from the pale tint to solid. */
4564
- qti-match-interaction:not(.qti-match-tabular) :state(drag):state(candidate-correct),
4565
- qti-match-interaction:not(.qti-match-tabular) qti-simple-associable-choice::part(drag):state(candidate-correct) {
4468
+ qti-match-interaction:not(.qti-match-tabular) :is(:state(drag), [data-state~='drag']):is(:state(candidate-correct), [data-state~='candidate-correct']),
4469
+ qti-match-interaction:not(.qti-match-tabular) qti-simple-associable-choice::part(drag):is(:state(candidate-correct), [data-state~='candidate-correct']) {
4566
4470
  --drag-background-color: var(--qti-correct-light);
4567
4471
  --drag-color: var(--qti-correct);
4568
4472
  --drag-border-color: var(--qti-correct);
4569
4473
  --drag-box-shadow: none;
4570
4474
  }
4571
4475
 
4572
- qti-match-interaction:not(.qti-match-tabular) :state(drag):state(candidate-incorrect),
4573
- qti-match-interaction:not(.qti-match-tabular) qti-simple-associable-choice::part(drag):state(candidate-incorrect) {
4476
+ qti-match-interaction:not(.qti-match-tabular) :is(:state(drag), [data-state~='drag']):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
4477
+ qti-match-interaction:not(.qti-match-tabular) qti-simple-associable-choice::part(drag):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
4574
4478
  --drag-background-color: var(--qti-incorrect-light);
4575
4479
  --drag-color: var(--qti-incorrect);
4576
4480
  --drag-border-color: var(--qti-incorrect);
@@ -4581,18 +4485,17 @@
4581
4485
  qti-match-interaction.styles.ts. */
4582
4486
  }
4583
4487
  @layer qti-components.interactions.base {
4584
- qti-match-interaction.qti-match-tabular {
4585
- &::part(corner) {
4488
+ qti-match-interaction.qti-match-tabular::part(corner) {
4586
4489
  border: var(--qti-border-thickness) var(--qti-border-style) var(--qti-border-color);
4587
4490
  background-color: var(--qti-bg-active);
4588
4491
  }
4589
4492
 
4590
- &::part(input-cell) {
4493
+ qti-match-interaction.qti-match-tabular::part(input-cell) {
4591
4494
  border: var(--qti-border-thickness) var(--qti-border-style) var(--qti-border-color);
4592
4495
  padding: var(--qti-padding-box);
4593
4496
  transition: background-color 0.1s ease;
4594
4497
  }
4595
- &::part(input-cell):hover {
4498
+ qti-match-interaction.qti-match-tabular::part(input-cell):hover {
4596
4499
  background-color: var(--qti-bg-active);
4597
4500
  }
4598
4501
 
@@ -4603,7 +4506,7 @@
4603
4506
  ElementInternals and cannot carry `:state()` — and `::part(x):state(y)` needs the state on
4604
4507
  the part's own element. They therefore carry variant *part tokens*, which deliberately
4605
4508
  reuse the state vocabulary: radio | checkbox, checked, correct | incorrect. */
4606
- &::part(control radio) {
4509
+ qti-match-interaction.qti-match-tabular::part(control radio) {
4607
4510
  background-color: var(--radio-background-color, var(--qti-component-background-color));
4608
4511
  background-image: var(--radio-background-image, var(--qti-component-background-image));
4609
4512
  border-width: var(--radio-border-width, var(--qti-component-border-width));
@@ -4620,7 +4523,7 @@
4620
4523
  edge, so a brand can square a radio off without having to out-specify this rule. */
4621
4524
  border-radius: var(--radio-border-radius, 100%);
4622
4525
  }
4623
- &::part(control checkbox) {
4526
+ qti-match-interaction.qti-match-tabular::part(control checkbox) {
4624
4527
  background-color: var(--checkbox-background-color, var(--qti-component-background-color));
4625
4528
  background-image: var(--checkbox-background-image, var(--qti-component-background-image));
4626
4529
  border-width: var(--checkbox-border-width, var(--qti-component-border-width));
@@ -4634,7 +4537,7 @@
4634
4537
  width: var(--qti-control-size);
4635
4538
  height: var(--qti-control-size);
4636
4539
  }
4637
- &::part(control-mark) {
4540
+ qti-match-interaction.qti-match-tabular::part(control-mark) {
4638
4541
  width: calc(var(--qti-control-size) * 0.625);
4639
4542
  height: calc(var(--qti-control-size) * 0.625);
4640
4543
  }
@@ -4642,19 +4545,18 @@
4642
4545
  outer box and the inner indicator independently. Scope the base inner-fill rules to
4643
4546
  `control-mark` so the classic dot-in-ring look stays intact; themes wanting the inverted
4644
4547
  look (outer filled, e.g. Kennisnet) add their own `::part(control checked)` rules. */
4645
- &::part(control-mark radio checked) {
4548
+ qti-match-interaction.qti-match-tabular::part(control-mark radio checked) {
4646
4549
  /* 0.5 reproduces today's 8px at the default 1rem — see mark-size for why this is a fraction. */
4647
4550
  width: calc(var(--qti-control-size) * 0.5);
4648
4551
  height: calc(var(--qti-control-size) * 0.5);
4649
4552
  background-color: var(--radio-mark-color, white);
4650
4553
  border-radius: 100%;
4651
4554
  }
4652
- &::part(control-mark checkbox checked) {
4555
+ qti-match-interaction.qti-match-tabular::part(control-mark checkbox checked) {
4653
4556
  background-color: var(--checkbox-mark-color, white);
4654
4557
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' width='100%' height='100%' viewBox='0 0 24 24'%3E%3Cpath d='M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z'/%3E%3C/svg%3E");
4655
4558
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' width='100%' height='100%' viewBox='0 0 24 24'%3E%3Cpath d='M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z'/%3E%3C/svg%3E");
4656
4559
  }
4657
- }
4658
4560
 
4659
4561
  /* Header cells — the two slotted <qti-simple-match-set>s have display:contents
4660
4562
  so their child choices act as row/column header cells. Written as a fully-
@@ -4684,38 +4586,34 @@
4684
4586
  qti-match-interaction.qti-match-tabular qti-simple-match-set:last-of-type qti-simple-associable-choice {
4685
4587
  text-align: center;
4686
4588
  }
4687
-
4688
- qti-match-interaction.qti-match-tabular {
4689
4589
  /* The 1px gap is the grid's own background showing through, which is what draws the cell rules.
4690
4590
  Clipping it to the rounded corners is layout and lives in qti-match-interaction-tabular.styles.ts. */
4691
- &::part(grid) {
4591
+ qti-match-interaction.qti-match-tabular::part(grid) {
4692
4592
  gap: 1px;
4693
4593
  background-color: var(--qti-border-color);
4694
4594
  border: var(--qti-border-thickness) solid var(--qti-border-color);
4695
4595
  border-radius: var(--qti-border-radius);
4696
4596
  }
4697
4597
 
4698
- &::part(corner),
4699
- &::part(input-cell) {
4598
+ qti-match-interaction.qti-match-tabular::part(corner),qti-match-interaction.qti-match-tabular::part(input-cell) {
4700
4599
  border: none;
4701
4600
  background-color: white;
4702
4601
  }
4703
4602
 
4704
- &::part(control checked) {
4603
+ qti-match-interaction.qti-match-tabular::part(control checked) {
4705
4604
  border-color: var(--qti-primary);
4706
4605
  background-color: var(--qti-primary);
4707
4606
  }
4708
4607
 
4709
- &::part(control-mark radio checked) {
4608
+ qti-match-interaction.qti-match-tabular::part(control-mark radio checked) {
4710
4609
  background-color: white;
4711
4610
  width: calc(var(--qti-control-size) * 0.5);
4712
4611
  height: calc(var(--qti-control-size) * 0.5);
4713
4612
  }
4714
4613
 
4715
- &::part(control-mark checkbox checked) {
4614
+ qti-match-interaction.qti-match-tabular::part(control-mark checkbox checked) {
4716
4615
  background-color: white;
4717
4616
  }
4718
- }
4719
4617
 
4720
4618
  qti-match-interaction.qti-match-tabular qti-simple-match-set qti-simple-associable-choice {
4721
4619
  border: none;
@@ -4723,39 +4621,33 @@
4723
4621
  }
4724
4622
  }
4725
4623
  @layer qti-components.interactions.corrections {
4726
- qti-match-interaction.qti-match-tabular {
4727
4624
  /* Correct / incorrect overlays — border on the outer box, and a matching fill on the inner
4728
4625
  indicator when checked. */
4729
- &::part(control correct) {
4626
+ qti-match-interaction.qti-match-tabular::part(control correct) {
4730
4627
  border-color: var(--qti-correct);
4731
4628
  }
4732
- &::part(control incorrect) {
4629
+ qti-match-interaction.qti-match-tabular::part(control incorrect) {
4733
4630
  border-color: var(--qti-incorrect);
4734
4631
  }
4735
4632
 
4736
- &::part(control checked correct) {
4633
+ qti-match-interaction.qti-match-tabular::part(control checked correct) {
4737
4634
  border-color: var(--qti-correct);
4738
4635
  background-color: var(--qti-correct);
4739
4636
  }
4740
4637
 
4741
- &::part(control checked incorrect) {
4638
+ qti-match-interaction.qti-match-tabular::part(control checked incorrect) {
4742
4639
  border-color: var(--qti-incorrect);
4743
4640
  background-color: var(--qti-incorrect);
4744
4641
  }
4745
- }
4746
-
4747
- .full-correct-response qti-match-interaction.qti-match-tabular {
4748
- &::part(control checked) {
4642
+ :is(.full-correct-response qti-match-interaction.qti-match-tabular)::part(control checked) {
4749
4643
  background-color: var(--qti-answer-bg);
4750
4644
  color: var(--qti-answer-fg);
4751
4645
  border-color: var(--qti-answer-border);
4752
4646
  }
4753
4647
 
4754
- &::part(control-mark radio checked),
4755
- &::part(control-mark checkbox checked) {
4648
+ :is(.full-correct-response qti-match-interaction.qti-match-tabular)::part(control-mark radio checked),:is(.full-correct-response qti-match-interaction.qti-match-tabular)::part(control-mark checkbox checked) {
4756
4649
  background-color: var(--qti-answer-fg);
4757
4650
  }
4758
- }
4759
4651
  }
4760
4652
  /* This interaction's chip vocabulary — the bank chip, the placed chip, and the floating clone. One
4761
4653
  such block per drag-drop interaction, opt-in; graphic-gap-match deliberately declares none. See
@@ -4773,7 +4665,7 @@
4773
4665
  sat on document.body it was in a different tree from the adopted stylesheet, and nothing — no
4774
4666
  part, no attribute — could style it. */
4775
4667
  @layer qti-components.global {
4776
- qti-order-interaction :state(drag),
4668
+ qti-order-interaction :is(:state(drag), [data-state~='drag']),
4777
4669
  [data-drag-clone][data-drag-interaction='qti-order-interaction'] {
4778
4670
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
4779
4671
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
@@ -4824,7 +4716,7 @@
4824
4716
  filter: var(--drag-filter, var(--qti-component-filter));
4825
4717
  }
4826
4718
 
4827
- qti-order-interaction :state(drag)::part(control)::before,
4719
+ qti-order-interaction :is(:state(drag), [data-state~='drag'])::part(control)::before,
4828
4720
  qti-order-interaction::part(drag-control)::before,
4829
4721
  qti-order-interaction ::part(drag-control)::before,
4830
4722
  [data-drag-clone][data-drag-interaction='qti-order-interaction']::part(control)::before {
@@ -4840,18 +4732,13 @@
4840
4732
  }
4841
4733
  }
4842
4734
  @layer qti-components.interactions.base {
4843
- qti-order-interaction {
4844
- &:state(dragzone-active)::part(drags) {
4735
+ qti-order-interaction:is(:state(dragzone-active), [data-state~='dragzone-active'])::part(drags) {
4845
4736
  background-color: var(--qti-bg-active);
4846
4737
  }
4847
- &:state(dragzone-enabled)::part(drags) {
4738
+ qti-order-interaction:is(:state(dragzone-enabled), [data-state~='dragzone-enabled'])::part(drags) {
4848
4739
  background-color: var(--qti-bg-active);
4849
4740
  }
4850
- &::part(drag),
4851
- & qti-simple-choice,
4852
- /* Editor fake-drag projected into the interaction's drop-target shadow.
4853
- Same `drag` styling so editor previews match runtime visuals. */
4854
- &::part(drag) {
4741
+ qti-order-interaction::part(drag),qti-order-interaction qti-simple-choice,qti-order-interaction::part(drag) {
4855
4742
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
4856
4743
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
4857
4744
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -4899,30 +4786,30 @@
4899
4786
  color: var(--drag-color, var(--qti-component-color));
4900
4787
  opacity: var(--drag-opacity, var(--qti-component-opacity));
4901
4788
  filter: var(--drag-filter, var(--qti-component-filter));
4902
- &[dragging] {
4789
+ }
4790
+ [dragging]:is(qti-order-interaction::part(drag),qti-order-interaction qti-simple-choice,qti-order-interaction::part(drag)) {
4903
4791
  pointer-events: none;
4904
4792
  rotate: -2deg;
4905
4793
  --drag-box-shadow: 0 8px 12px rgb(0 0 0 / 20%), 0 4px 8px rgb(0 0 0 / 10%);
4906
4794
  }
4907
- &:hover {
4908
4795
  @media (hover: hover) {
4796
+ :is(qti-order-interaction::part(drag),qti-order-interaction qti-simple-choice,qti-order-interaction::part(drag)):hover {
4909
4797
  background-color: color-mix(
4910
4798
  in srgb,
4911
4799
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
4912
4800
  black
4913
4801
  );
4914
- }
4915
4802
  }
4916
- &:focus {
4803
+ }
4804
+ :is(qti-order-interaction::part(drag),qti-order-interaction qti-simple-choice,qti-order-interaction::part(drag)):focus {
4917
4805
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
4918
4806
  }
4919
- }
4920
4807
  /* Editor fake-drag projected into the interaction's drop-target shadow.
4921
4808
  Same `drag` styling so editor previews match runtime visuals.
4922
4809
  Kept as its own block (separate from the runtime-drag selector list
4923
4810
  above) because a CSS comment between commas inside a nested selector
4924
4811
  list is silently dropped by Chrome's nesting parser. */
4925
- &::part(drag) {
4812
+ qti-order-interaction::part(drag) {
4926
4813
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
4927
4814
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
4928
4815
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -4970,21 +4857,21 @@
4970
4857
  color: var(--drag-color, var(--qti-component-color));
4971
4858
  opacity: var(--drag-opacity, var(--qti-component-opacity));
4972
4859
  filter: var(--drag-filter, var(--qti-component-filter));
4973
- &:hover {
4860
+ }
4974
4861
  @media (hover: hover) {
4862
+ :is(qti-order-interaction::part(drag)):hover {
4975
4863
  background-color: color-mix(
4976
4864
  in srgb,
4977
4865
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
4978
4866
  black
4979
4867
  );
4980
- }
4981
4868
  }
4982
- &:focus {
4869
+ }
4870
+ :is(qti-order-interaction::part(drag)):focus {
4983
4871
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
4984
4872
  }
4985
- }
4986
4873
  /* The drag's box — flex, centred, clipped — is in qti-order-interaction.styles.ts. */
4987
- &::part(drag) {
4874
+ qti-order-interaction::part(drag) {
4988
4875
  /*
4989
4876
  * No `width: 100%`. It stretched a placed chip to its slot, which is why order was the one
4990
4877
  * entry in CHIP_BOX_KNOWN_BAD ("the placed chip stretches to fill its drop slot") — a 55px
@@ -5003,28 +4890,28 @@
5003
4890
  * The drop's flags are part tokens rather than attributes: an attribute selector cannot follow
5004
4891
  * `::part()`, and a <div> cannot carry a custom state (attachInternals throws). See setDropFlag.
5005
4892
  */
5006
- &::part(active) {
4893
+ qti-order-interaction::part(active) {
5007
4894
  background-color: var(--qti-bg-active);
5008
4895
  }
5009
4896
 
5010
- &::part(enabled) {
4897
+ qti-order-interaction::part(enabled) {
5011
4898
  background-color: var(--qti-bg-active);
5012
4899
  }
5013
4900
 
5014
4901
  /* A filled drop hides its dashed outline and drop-here glyph. Hides — the border keeps its
5015
4902
  width, or the column shifts 2px the instant a chip lands. */
5016
- &::part(filled) {
4903
+ qti-order-interaction::part(filled) {
5017
4904
  --qti-drop-border: var(--qti-border-thickness, 1px) solid transparent;
5018
4905
  --qti-drop-bg-img: none;
5019
4906
  }
5020
4907
 
5021
- &::part(drops) {
4908
+ qti-order-interaction::part(drops) {
5022
4909
  gap: var(--qti-gap);
5023
4910
  }
5024
- &::part(drags) {
4911
+ qti-order-interaction::part(drags) {
5025
4912
  gap: var(--qti-gap);
5026
4913
  }
5027
- &::part(drop) {
4914
+ qti-order-interaction::part(drop) {
5028
4915
  background-color: var(--drop-background-color, var(--qti-component-background-color));
5029
4916
  background-image: var(--drop-background-image, var(--qti-component-background-image));
5030
4917
  border-width: var(--drop-border-width, var(--qti-component-border-width));
@@ -5036,20 +4923,17 @@
5036
4923
  opacity: var(--drop-opacity, var(--qti-component-opacity));
5037
4924
  filter: var(--drop-filter, var(--qti-component-filter));
5038
4925
  border-style: var(--drop-border-style, dashed);
5039
- &[enabled] {
4926
+ }
4927
+ [enabled]:is(qti-order-interaction::part(drop)) {
5040
4928
  background-color: var(--qti-bg-active);
5041
4929
  }
5042
- &:focus {
4930
+ :is(qti-order-interaction::part(drop)):focus {
5043
4931
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
5044
4932
  }
5045
- }
5046
- &::part(active) {
4933
+ qti-order-interaction::part(active) {
5047
4934
  background-color: var(--qti-bg-active);
5048
4935
  }
5049
- }
5050
-
5051
- qti-order-interaction {
5052
- &::part(drops) {
4936
+ qti-order-interaction::part(drops) {
5053
4937
  margin: 0;
5054
4938
  border-radius: var(--qti-border-radius);
5055
4939
  }
@@ -5059,18 +4943,18 @@
5059
4943
  overhang the row by the border (42px row, 44px drop), which the drop's padding then doubled.
5060
4944
  It also defeated `:has(qti-simple-choice) [part~='drop'] { align-self: flex-start }` in
5061
4945
  qti-order-interaction.styles.ts. */
5062
- &::part(drop) {
4946
+ qti-order-interaction::part(drop) {
5063
4947
  border: var(--qti-drop-border, var(--qti-border-thickness) dashed var(--qti-border-color));
5064
4948
  border-radius: var(--qti-border-radius);
5065
4949
  background-color: transparent;
5066
4950
  background-image: var(--qti-drop-bg-img, revert-layer);
5067
4951
  }
5068
4952
 
5069
- &::part(active) {
4953
+ qti-order-interaction::part(active) {
5070
4954
  background-color: var(--qti-bg-active);
5071
4955
  }
5072
4956
 
5073
- &::part(enabled) {
4957
+ qti-order-interaction::part(enabled) {
5074
4958
  background-color: var(--qti-bg-active);
5075
4959
  }
5076
4960
 
@@ -5081,7 +4965,7 @@
5081
4965
  overlay (apps/e2e/src/stories/kennisnet/kennisnet.css), not here. In the base theme a placed
5082
4966
  chip is just the neutral chip (@mixin drag), and its corrections route through --drag-*
5083
4967
  like every other chip. */
5084
- & qti-simple-choice {
4968
+ qti-order-interaction qti-simple-choice {
5085
4969
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
5086
4970
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
5087
4971
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -5131,7 +5015,7 @@
5131
5015
  filter: var(--drag-filter, var(--qti-component-filter));
5132
5016
  }
5133
5017
 
5134
- &::part(container) {
5018
+ qti-order-interaction::part(container) {
5135
5019
  gap: 2rem;
5136
5020
  }
5137
5021
 
@@ -5148,16 +5032,12 @@
5148
5032
  * the bank above the drops" — that is what the qti-choices-top/bottom/left/right classes
5149
5033
  * already do, and wiring orientation to them is a behaviour change, not a move.
5150
5034
  */
5151
- &[orientation='vertical'] {
5152
- & qti-simple-choice,
5153
- &::part(drag) {
5035
+ qti-order-interaction[orientation='vertical'] qti-simple-choice,qti-order-interaction[orientation='vertical']::part(drag) {
5154
5036
  width: auto;
5155
5037
  }
5156
- }
5157
- }
5158
5038
  }
5159
5039
  @layer qti-components.interactions.states {
5160
- qti-order-interaction qti-simple-choice:state(placeholder) {
5040
+ qti-order-interaction qti-simple-choice:is(:state(placeholder), [data-state~='placeholder']) {
5161
5041
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
5162
5042
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
5163
5043
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -5168,7 +5048,7 @@
5168
5048
  pointer-events: none;
5169
5049
  }
5170
5050
 
5171
- qti-order-interaction qti-simple-choice:state(dragging) {
5051
+ qti-order-interaction qti-simple-choice:is(:state(dragging), [data-state~='dragging']) {
5172
5052
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
5173
5053
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
5174
5054
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -5182,7 +5062,7 @@
5182
5062
  pointer-events: none;
5183
5063
  }
5184
5064
 
5185
- qti-order-interaction qti-simple-choice:state(return-anchor) {
5065
+ qti-order-interaction qti-simple-choice:is(:state(return-anchor), [data-state~='return-anchor']) {
5186
5066
  border-color: var(--qti-drop-highlight-border-color);
5187
5067
  box-shadow: inset 0 0 0 var(--qti-drop-highlight-border-width, 2px)
5188
5068
  color-mix(in srgb, var(--qti-drop-highlight-border-color) 55%, transparent);
@@ -5220,16 +5100,16 @@
5220
5100
  `[data-drag-clone]:state(candidate-*)` went with it and is deliberately NOT reproduced — a clone
5221
5101
  carries no custom states at all, because cloneNode does not copy ElementInternals (measured), so
5222
5102
  that selector never matched anything. */
5223
- qti-order-interaction :state(drag):state(candidate-correct),
5224
- qti-order-interaction::part(drag):state(candidate-correct) {
5103
+ qti-order-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-correct), [data-state~='candidate-correct']),
5104
+ qti-order-interaction::part(drag):is(:state(candidate-correct), [data-state~='candidate-correct']) {
5225
5105
  --drag-background-color: var(--qti-correct-light);
5226
5106
  --drag-color: var(--qti-correct);
5227
5107
  --drag-border-color: var(--qti-correct);
5228
5108
  --drag-box-shadow: none;
5229
5109
  }
5230
5110
 
5231
- qti-order-interaction :state(drag):state(candidate-incorrect),
5232
- qti-order-interaction::part(drag):state(candidate-incorrect) {
5111
+ qti-order-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
5112
+ qti-order-interaction::part(drag):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
5233
5113
  --drag-background-color: var(--qti-incorrect-light);
5234
5114
  --drag-color: var(--qti-incorrect);
5235
5115
  --drag-border-color: var(--qti-incorrect);
@@ -5260,7 +5140,7 @@
5260
5140
  and text colour from the removed rule and nothing else. Deleting that rule without this block
5261
5141
  turned the associate bank chips transparent, square and un-grabbable — measured, not guessed. */
5262
5142
  @layer qti-components.global {
5263
- qti-associate-interaction :state(drag),
5143
+ qti-associate-interaction :is(:state(drag), [data-state~='drag']),
5264
5144
  [data-drag-clone][data-drag-interaction='qti-associate-interaction'] {
5265
5145
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
5266
5146
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
@@ -5311,7 +5191,7 @@
5311
5191
  filter: var(--drag-filter, var(--qti-component-filter));
5312
5192
  }
5313
5193
 
5314
- qti-associate-interaction :state(drag)::part(control)::before,
5194
+ qti-associate-interaction :is(:state(drag), [data-state~='drag'])::part(control)::before,
5315
5195
  qti-associate-interaction::part(drag-control)::before,
5316
5196
  qti-associate-interaction ::part(drag-control)::before,
5317
5197
  [data-drag-clone][data-drag-interaction='qti-associate-interaction']::part(control)::before {
@@ -5327,7 +5207,6 @@
5327
5207
  }
5328
5208
  }
5329
5209
  @layer qti-components.interactions.base {
5330
- qti-associate-interaction {
5331
5210
  /*
5332
5211
  * NO `--drag-border-color: transparent` here, and that absence is the rule.
5333
5212
  *
@@ -5343,20 +5222,17 @@
5343
5222
  * Associate was the only interaction doing this; match, order and gap-match all let the bank
5344
5223
  * chip take --qti-component-border-color, and now so does this one.
5345
5224
  */
5346
- & qti-simple-associable-choice {
5225
+ qti-associate-interaction qti-simple-associable-choice {
5347
5226
  padding: var(--qti-drag-padding);
5348
5227
  }
5349
5228
 
5350
- &:state(dragzone-active) slot[name='qti-simple-associable-choice'] {
5229
+ qti-associate-interaction:is(:state(dragzone-active), [data-state~='dragzone-active']) slot[name='qti-simple-associable-choice'] {
5351
5230
  background-color: var(--qti-bg-active);
5352
5231
  }
5353
- &:state(dragzone-enabled) slot[name='qti-simple-associable-choice'] {
5232
+ qti-associate-interaction:is(:state(dragzone-enabled), [data-state~='dragzone-enabled']) slot[name='qti-simple-associable-choice'] {
5354
5233
  background-color: var(--qti-bg-active);
5355
5234
  }
5356
- & qti-simple-associable-choice::part(drag),
5357
- /* A placed chip lives in a shadow root; `::part(drag)` is the only way in. This used to be
5358
- `::part(qti-simple-associable-choice)`, a part named after the tag. */
5359
- &::part(drag) {
5235
+ qti-associate-interaction qti-simple-associable-choice::part(drag),qti-associate-interaction::part(drag) {
5360
5236
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
5361
5237
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
5362
5238
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -5404,29 +5280,29 @@
5404
5280
  color: var(--drag-color, var(--qti-component-color));
5405
5281
  opacity: var(--drag-opacity, var(--qti-component-opacity));
5406
5282
  filter: var(--drag-filter, var(--qti-component-filter));
5407
- &:hover {
5283
+ }
5408
5284
  @media (hover: hover) {
5285
+ :is(qti-associate-interaction qti-simple-associable-choice::part(drag),qti-associate-interaction::part(drag)):hover {
5409
5286
  background-color: color-mix(
5410
5287
  in srgb,
5411
5288
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
5412
5289
  black
5413
5290
  );
5414
- }
5415
5291
  }
5416
- &:focus {
5292
+ }
5293
+ :is(qti-associate-interaction qti-simple-associable-choice::part(drag),qti-associate-interaction::part(drag)):focus {
5417
5294
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
5418
5295
  }
5419
- &[dragging] {
5296
+ [dragging]:is(qti-associate-interaction qti-simple-associable-choice::part(drag),qti-associate-interaction::part(drag)) {
5420
5297
  pointer-events: none;
5421
5298
  rotate: -2deg;
5422
5299
  --drag-box-shadow: 0 8px 12px rgb(0 0 0 / 20%), 0 4px 8px rgb(0 0 0 / 10%);
5423
5300
  }
5424
- }
5425
5301
  /* Editor fake-drag projected into the interaction's drop-target shadow.
5426
5302
  Kept as a separate block (not merged into the comma list above)
5427
5303
  because a CSS comment between commas in a nested selector list is
5428
5304
  silently dropped by Chrome's nesting parser. */
5429
- &::part(drag) {
5305
+ qti-associate-interaction::part(drag) {
5430
5306
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
5431
5307
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
5432
5308
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -5474,29 +5350,29 @@
5474
5350
  color: var(--drag-color, var(--qti-component-color));
5475
5351
  opacity: var(--drag-opacity, var(--qti-component-opacity));
5476
5352
  filter: var(--drag-filter, var(--qti-component-filter));
5477
- &:hover {
5353
+ }
5478
5354
  @media (hover: hover) {
5355
+ :is(qti-associate-interaction::part(drag)):hover {
5479
5356
  background-color: color-mix(
5480
5357
  in srgb,
5481
5358
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
5482
5359
  black
5483
5360
  );
5484
- }
5485
5361
  }
5486
- &:focus {
5362
+ }
5363
+ :is(qti-associate-interaction::part(drag)):focus {
5487
5364
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
5488
5365
  }
5489
- }
5490
5366
  /* `display: grid` lives in qti-associate-interaction.styles.ts, which already declares it on
5491
5367
  [part~='drop'] alongside the drop's min-height/min-width. */
5492
5368
  /* A filled drop hides its dashed edge — see the note in qti-gap-match-interaction.css. Every
5493
5369
  associate drop holds exactly one chip (the div never carries match-max), so no capacity guard
5494
5370
  is needed here. Colour only: the width stays, or the row shifts when a chip lands. */
5495
- &::part(filled) {
5371
+ qti-associate-interaction::part(filled) {
5496
5372
  --drop-border-color: transparent;
5497
5373
  }
5498
5374
 
5499
- &::part(drop) {
5375
+ qti-associate-interaction::part(drop) {
5500
5376
  background-color: var(--drop-background-color, var(--qti-component-background-color));
5501
5377
  background-image: var(--drop-background-image, var(--qti-component-background-image));
5502
5378
  border-width: var(--drop-border-width, var(--qti-component-border-width));
@@ -5509,7 +5385,7 @@
5509
5385
  filter: var(--drop-filter, var(--qti-component-filter));
5510
5386
  border-style: var(--drop-border-style, dashed);
5511
5387
  }
5512
- &::part(drop):focus {
5388
+ qti-associate-interaction::part(drop):focus {
5513
5389
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
5514
5390
  }
5515
5391
 
@@ -5524,14 +5400,13 @@
5524
5400
  * Deleted here: `&::part(drop)[dragging] { @mixin act }`. An attribute selector after ::part()
5525
5401
  * never matches; that rule has never applied.
5526
5402
  */
5527
- &::part(active) {
5403
+ qti-associate-interaction::part(active) {
5528
5404
  background-color: var(--qti-bg-active);
5529
5405
  }
5530
5406
 
5531
- &::part(enabled) {
5407
+ qti-associate-interaction::part(enabled) {
5532
5408
  background-color: var(--qti-bg-active);
5533
5409
  }
5534
- }
5535
5410
  }
5536
5411
  @layer qti-components.interactions.states {
5537
5412
  qti-simple-associable-choice::part(drag),
@@ -5594,7 +5469,7 @@
5594
5469
  background-color: var(--qti-drop-highlight-enabled-bg);
5595
5470
  }
5596
5471
 
5597
- qti-associate-interaction qti-simple-associable-choice:state(placeholder) {
5472
+ qti-associate-interaction qti-simple-associable-choice:is(:state(placeholder), [data-state~='placeholder']) {
5598
5473
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
5599
5474
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
5600
5475
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -5606,7 +5481,7 @@
5606
5481
  pointer-events: none;
5607
5482
  }
5608
5483
 
5609
- qti-associate-interaction qti-simple-associable-choice:state(dragging) {
5484
+ qti-associate-interaction qti-simple-associable-choice:is(:state(dragging), [data-state~='dragging']) {
5610
5485
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
5611
5486
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
5612
5487
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -5620,17 +5495,17 @@
5620
5495
  pointer-events: none;
5621
5496
  }
5622
5497
 
5623
- qti-associate-interaction qti-simple-associable-choice:state(placeholder) > *,
5624
- qti-associate-interaction qti-simple-associable-choice:state(dragging) > * {
5498
+ qti-associate-interaction qti-simple-associable-choice:is(:state(placeholder), [data-state~='placeholder']) > *,
5499
+ qti-associate-interaction qti-simple-associable-choice:is(:state(dragging), [data-state~='dragging']) > * {
5625
5500
  visibility: hidden;
5626
5501
  }
5627
5502
 
5628
- qti-associate-interaction qti-simple-associable-choice:state(placeholder)::part(label),
5629
- qti-associate-interaction qti-simple-associable-choice:state(dragging)::part(label) {
5503
+ qti-associate-interaction qti-simple-associable-choice:is(:state(placeholder), [data-state~='placeholder'])::part(label),
5504
+ qti-associate-interaction qti-simple-associable-choice:is(:state(dragging), [data-state~='dragging'])::part(label) {
5630
5505
  visibility: hidden;
5631
5506
  }
5632
5507
 
5633
- qti-associate-interaction qti-simple-associable-choice:state(return-anchor) {
5508
+ qti-associate-interaction qti-simple-associable-choice:is(:state(return-anchor), [data-state~='return-anchor']) {
5634
5509
  border-color: var(--qti-drop-highlight-border-color);
5635
5510
  box-shadow: inset 0 0 0 var(--qti-drop-highlight-border-width, 2px)
5636
5511
  color-mix(in srgb, var(--qti-drop-highlight-border-color) 55%, transparent);
@@ -5643,18 +5518,18 @@
5643
5518
  `[data-drag-clone]:state(candidate-*)` went with it and is deliberately NOT reproduced — a clone
5644
5519
  carries no custom states at all, because cloneNode does not copy ElementInternals (measured), so
5645
5520
  that selector never matched anything. */
5646
- qti-associate-interaction :state(drag):state(candidate-correct),
5647
- qti-simple-associable-choice::part(drag):state(candidate-correct),
5648
- qti-associate-interaction::part(drag):state(candidate-correct) {
5521
+ qti-associate-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-correct), [data-state~='candidate-correct']),
5522
+ qti-simple-associable-choice::part(drag):is(:state(candidate-correct), [data-state~='candidate-correct']),
5523
+ qti-associate-interaction::part(drag):is(:state(candidate-correct), [data-state~='candidate-correct']) {
5649
5524
  --drag-background-color: var(--qti-correct-light);
5650
5525
  --drag-color: var(--qti-correct);
5651
5526
  --drag-border-color: var(--qti-correct);
5652
5527
  --drag-box-shadow: none;
5653
5528
  }
5654
5529
 
5655
- qti-associate-interaction :state(drag):state(candidate-incorrect),
5656
- qti-simple-associable-choice::part(drag):state(candidate-incorrect),
5657
- qti-associate-interaction::part(drag):state(candidate-incorrect) {
5530
+ qti-associate-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
5531
+ qti-simple-associable-choice::part(drag):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
5532
+ qti-associate-interaction::part(drag):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
5658
5533
  --drag-background-color: var(--qti-incorrect-light);
5659
5534
  --drag-color: var(--qti-incorrect);
5660
5535
  --drag-border-color: var(--qti-incorrect);
@@ -5671,8 +5546,9 @@
5671
5546
  --qti-graphic-order-pin-fill: var(--qti-background, #fff);
5672
5547
  --qti-graphic-order-pin-number-size: calc(var(--qti-graphic-order-pin-size) * 0.5);
5673
5548
  --qti-graphic-order-pin-svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath d='M192 500C145 434 106 386 77 344C49 305 32 272 32 224C32 135.6 103.6 64 192 64s160 71.6 160 160c0 48-17 81-45 120c-29 42-68 90-115 156z' fill='white' stroke='currentColor' stroke-width='28' stroke-linejoin='round'/%3E%3C/svg%3E");
5549
+ }
5674
5550
 
5675
- & qti-hotspot-choice {
5551
+ qti-graphic-order-interaction qti-hotspot-choice {
5676
5552
  width: 100%;
5677
5553
  height: 100%;
5678
5554
  background-color: color-mix(in srgb, var(--qti-border-color) 18%, transparent);
@@ -5681,63 +5557,58 @@
5681
5557
  transition:
5682
5558
  background-color var(--qti-motion-duration-fast, 0.15s) ease,
5683
5559
  border-color var(--qti-motion-duration-fast, 0.15s) ease;
5560
+ }
5684
5561
 
5685
- &[shape='circle'] {
5562
+ [shape='circle']:is(qti-graphic-order-interaction qti-hotspot-choice) {
5686
5563
  min-width: calc(var(--qti-graphic-order-pin-size) * 0.6);
5687
5564
  min-height: calc(var(--qti-graphic-order-pin-size) * 0.6);
5688
5565
  }
5689
5566
 
5690
- &[shape='poly'] {
5567
+ [shape='poly']:is(qti-graphic-order-interaction qti-hotspot-choice) {
5691
5568
  background-color: color-mix(in srgb, var(--qti-border-color) 18%, transparent);
5692
5569
  box-shadow: none;
5693
5570
  }
5694
5571
 
5695
- &[shape='circle']:hover,
5696
- &[shape='rect']:hover,
5697
- &[shape='ellipse']:hover {
5572
+ [shape='circle']:is(qti-graphic-order-interaction qti-hotspot-choice):hover,[shape='rect']:is(qti-graphic-order-interaction qti-hotspot-choice):hover,[shape='ellipse']:is(qti-graphic-order-interaction qti-hotspot-choice):hover {
5698
5573
  background-color: color-mix(in srgb, var(--qti-border-active) 12%, transparent);
5699
5574
  }
5700
5575
 
5701
- &[shape='poly']:hover {
5576
+ [shape='poly']:is(qti-graphic-order-interaction qti-hotspot-choice):hover {
5702
5577
  background-color: color-mix(in srgb, var(--qti-border-active) 20%, transparent);
5703
5578
  box-shadow: none;
5704
5579
  }
5705
5580
 
5706
- &[shape='circle']:focus-visible,
5707
- &[shape='rect']:focus-visible,
5708
- &[shape='ellipse']:focus-visible {
5581
+ [shape='circle']:is(qti-graphic-order-interaction qti-hotspot-choice):focus-visible,[shape='rect']:is(qti-graphic-order-interaction qti-hotspot-choice):focus-visible,[shape='ellipse']:is(qti-graphic-order-interaction qti-hotspot-choice):focus-visible {
5709
5582
  outline: var(--qti-focus-border-width) solid var(--qti-focus-color);
5710
5583
  outline-offset: 2px;
5711
5584
  }
5712
5585
 
5713
- &[shape='poly']:focus-visible {
5586
+ [shape='poly']:is(qti-graphic-order-interaction qti-hotspot-choice):focus-visible {
5714
5587
  outline: var(--qti-focus-border-width) solid var(--qti-focus-color);
5715
5588
  outline-offset: 2px;
5716
5589
  }
5717
5590
 
5718
- &[aria-ordervalue] {
5591
+ [aria-ordervalue]:is(qti-graphic-order-interaction qti-hotspot-choice) {
5719
5592
  border: 0 !important;
5720
5593
  box-shadow: none;
5721
5594
  background-color: color-mix(in srgb, var(--qti-selected-bg) 35%, transparent);
5722
5595
  }
5723
5596
 
5724
- &[aria-ordervalue]:hover,
5725
- &[aria-ordervalue]:focus-visible {
5597
+ [aria-ordervalue]:is(qti-graphic-order-interaction qti-hotspot-choice):hover,[aria-ordervalue]:is(qti-graphic-order-interaction qti-hotspot-choice):focus-visible {
5726
5598
  border-color: transparent !important;
5727
5599
  }
5728
- &[aria-ordercorrectvalue][aria-ordervalue]:not([shape='circle'])::after {
5600
+ [aria-ordercorrectvalue][aria-ordervalue]:is(qti-graphic-order-interaction qti-hotspot-choice):not([shape='circle'])::after {
5729
5601
  content: 'C=' attr(aria-ordercorrectvalue) ' R=' attr(aria-ordervalue) !important;
5730
5602
  color: var(--qti-correct);
5731
5603
  }
5732
- &[aria-ordercorrectvalue]:not([aria-ordervalue]):not([shape='circle'])::after {
5604
+ [aria-ordercorrectvalue]:is(qti-graphic-order-interaction qti-hotspot-choice):not([aria-ordervalue]):not([shape='circle'])::after {
5733
5605
  content: 'C=' attr(aria-ordercorrectvalue) !important;
5734
5606
  color: var(--qti-correct);
5735
5607
  }
5736
- }
5737
5608
 
5738
5609
  /* The pin's face. Where it sits — absolute, anchored to its hotspot, above the image — is
5739
5610
  placement, and lives in qti-graphic-order-interaction.styles.ts as ::slotted(). */
5740
- & .cito-graphic-order-marker {
5611
+ qti-graphic-order-interaction .cito-graphic-order-marker {
5741
5612
  width: var(--qti-graphic-order-pin-size);
5742
5613
  height: var(--qti-graphic-order-pin-size);
5743
5614
  padding-top: calc(var(--qti-graphic-order-pin-size) * 0.2);
@@ -5750,47 +5621,40 @@
5750
5621
  pointer-events: none;
5751
5622
  }
5752
5623
 
5753
- & .cito-graphic-order-marker--poly {
5624
+ qti-graphic-order-interaction .cito-graphic-order-marker--poly {
5754
5625
  width: calc(var(--qti-select-point-marker-size, 1.5rem) * 0.72);
5755
5626
  height: calc(var(--qti-select-point-marker-size, 1.5rem) * 0.72);
5756
5627
  font-size: calc(var(--qti-select-point-marker-size, 1.5rem) * 0.45);
5757
5628
  padding-top: calc(var(--qti-select-point-marker-size, 1.5rem) * 0.14);
5758
5629
  }
5759
- }
5760
5630
  }
5761
5631
  @layer qti-components.interactions.states {
5762
- qti-graphic-order-interaction {
5763
- & qti-hotspot-choice {
5764
- &:state(readonly) {
5632
+ :is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(readonly), [data-state~='readonly']) {
5765
5633
  cursor: pointer;
5766
5634
  }
5767
5635
 
5768
- &:state(disabled) {
5636
+ :is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(disabled), [data-state~='disabled']) {
5769
5637
  cursor: not-allowed;
5770
5638
  }
5771
5639
 
5772
- &[shape='circle']:state(checked),
5773
- &[shape='rect']:state(checked),
5774
- &[shape='ellipse']:state(checked) {
5640
+ [shape='circle']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(checked), [data-state~='checked']),[shape='rect']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(checked), [data-state~='checked']),[shape='ellipse']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(checked), [data-state~='checked']) {
5775
5641
  background-color: color-mix(in srgb, var(--qti-selected-bg) 35%, transparent);
5776
5642
  }
5777
5643
 
5778
- &[shape='poly']:state(checked) {
5644
+ [shape='poly']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(checked), [data-state~='checked']) {
5779
5645
  background-color: color-mix(in srgb, var(--qti-selected-bg) 40%, transparent);
5780
5646
  box-shadow: none;
5781
5647
  }
5782
5648
 
5783
- &[aria-ordervalue]:state(checked),
5784
- &[aria-ordervalue] {
5649
+ [aria-ordervalue]:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(checked), [data-state~='checked']),[aria-ordervalue]:is(qti-graphic-order-interaction qti-hotspot-choice) {
5785
5650
  border-color: transparent !important;
5786
5651
  }
5787
- }
5788
5652
 
5789
5653
  /* No qti-selections-light / -dark here. Appendix B.1 of the QTI 3 implementation guide defines
5790
5654
  that class pair for qti-hotspot-interaction and qti-graphic-gap-match-interaction only, and no
5791
5655
  item in this repo ever set it on a graphic-order interaction. What was here overrode the neutral
5792
5656
  drag-bank highlight below with --qti-light-* and --qti-dark-* colours that nothing could reach. */
5793
- &:state(dragzone-active)::part(drags) {
5657
+ qti-graphic-order-interaction:is(:state(dragzone-active), [data-state~='dragzone-active'])::part(drags) {
5794
5658
  /* Direct properties, not slots: ::part(drags) is a bare slot container with no paint role of
5795
5659
  its own (the other interactions paint it with `@mixin dropping`, also direct), so there is
5796
5660
  no --<role>-* to switch. Inlined from `@mixin act`. */
@@ -5798,85 +5662,67 @@
5798
5662
  background-color: var(--qti-bg-active);
5799
5663
  }
5800
5664
 
5801
- &:state(dragzone-enabled)::part(drags) {
5665
+ qti-graphic-order-interaction:is(:state(dragzone-enabled), [data-state~='dragzone-enabled'])::part(drags) {
5802
5666
  background-color: var(--qti-bg-active);
5803
5667
  }
5804
- }
5805
5668
  }
5806
5669
  @layer qti-components.interactions.corrections {
5807
- qti-graphic-order-interaction {
5808
- & qti-hotspot-choice {
5809
- &[shape='circle']:state(correct-response),
5810
- &[shape='rect']:state(correct-response),
5811
- &[shape='ellipse']:state(correct-response) {
5670
+ [shape='circle']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(correct-response), [data-state~='correct-response']),[shape='rect']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(correct-response), [data-state~='correct-response']),[shape='ellipse']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(correct-response), [data-state~='correct-response']) {
5812
5671
  background-color: color-mix(in srgb, var(--qti-answer-bg) 75%, transparent);
5813
5672
  }
5814
5673
 
5815
- &[shape='poly']:state(correct-response) {
5674
+ [shape='poly']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(correct-response), [data-state~='correct-response']) {
5816
5675
  background-color: color-mix(in srgb, var(--qti-answer-bg) 80%, transparent);
5817
5676
  box-shadow: none;
5818
5677
  }
5819
5678
 
5820
- &[shape='circle']:state(candidate-correct),
5821
- &[shape='rect']:state(candidate-correct),
5822
- &[shape='ellipse']:state(candidate-correct) {
5679
+ [shape='circle']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(candidate-correct), [data-state~='candidate-correct']),[shape='rect']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(candidate-correct), [data-state~='candidate-correct']),[shape='ellipse']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(candidate-correct), [data-state~='candidate-correct']) {
5823
5680
  background-color: color-mix(in srgb, var(--qti-correct) 35%, transparent);
5824
5681
  }
5825
5682
 
5826
- &[shape='poly']:state(candidate-correct) {
5683
+ [shape='poly']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(candidate-correct), [data-state~='candidate-correct']) {
5827
5684
  background-color: color-mix(in srgb, var(--qti-correct) 40%, transparent);
5828
5685
  box-shadow: none;
5829
5686
  }
5830
5687
 
5831
- &[shape='circle']:state(candidate-incorrect),
5832
- &[shape='rect']:state(candidate-incorrect),
5833
- &[shape='ellipse']:state(candidate-incorrect) {
5688
+ [shape='circle']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),[shape='rect']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),[shape='ellipse']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
5834
5689
  background-color: color-mix(in srgb, var(--qti-incorrect) 35%, transparent);
5835
5690
  }
5836
5691
 
5837
- &[shape='poly']:state(candidate-incorrect) {
5692
+ [shape='poly']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
5838
5693
  background-color: color-mix(in srgb, var(--qti-incorrect) 40%, transparent);
5839
5694
  box-shadow: none;
5840
5695
  }
5841
5696
 
5842
- &[aria-ordervalue]:state(correct-response),
5843
- &[aria-ordervalue]:state(candidate-correct),
5844
- &[aria-ordervalue]:state(candidate-incorrect) {
5697
+ [aria-ordervalue]:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(correct-response), [data-state~='correct-response']),[aria-ordervalue]:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(candidate-correct), [data-state~='candidate-correct']),[aria-ordervalue]:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
5845
5698
  border-color: transparent !important;
5846
5699
  }
5847
- }
5848
- }
5849
5700
  }
5850
5701
  @layer qti-components.interactions.base {
5851
- qti-graphic-associate-interaction {
5852
5702
  /*
5853
5703
  * qti-associable-hotspot does not use ActiveElementMixin, so it carries no
5854
5704
  * checked/disabled/readonly custom states. Selection and correction are drawn as
5855
5705
  * ::part(line) / ::part(point) instead. Give it a `part="drag"` and the
5856
5706
  * ActiveElementMixin states before adding state-dependent rules here.
5857
5707
  */
5858
- & qti-associable-hotspot {
5859
5708
  /* `[shape='square']` used to be in this selector. It is not a QTI shape — the spec has
5860
5709
  circle, default, ellipse, poly and rect — positionShapes has no branch for it, and no item
5861
5710
  in the repo sets it. */
5862
- &[shape='circle'] {
5711
+ [shape='circle']:is(qti-graphic-associate-interaction qti-associable-hotspot) {
5863
5712
  background-color: transparent;
5864
- &:focus {
5713
+ }
5714
+ [shape='circle']:is(qti-graphic-associate-interaction qti-associable-hotspot):focus {
5865
5715
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
5866
5716
  }
5867
- }
5868
- }
5869
- }
5870
5717
  }
5871
5718
  @layer qti-components.interactions.states {
5872
- qti-graphic-associate-interaction {
5873
5719
 
5874
5720
 
5875
5721
  /* No qti-selections-light / -dark here. Appendix B.1 of the QTI 3 implementation guide defines
5876
5722
  that class pair for qti-hotspot-interaction and qti-graphic-gap-match-interaction only, and no
5877
5723
  item in this repo ever set it on a graphic-associate interaction. What was here overrode the neutral
5878
5724
  drag-bank highlight below with --qti-light-* and --qti-dark-* colours that nothing could reach. */
5879
- &:state(dragzone-active)::part(drags) {
5725
+ qti-graphic-associate-interaction:is(:state(dragzone-active), [data-state~='dragzone-active'])::part(drags) {
5880
5726
  /* Direct properties, not slots: ::part(drags) is a bare slot container with no paint role of
5881
5727
  its own (the other interactions paint it with `@mixin dropping`, also direct), so there is
5882
5728
  no --<role>-* to switch. Inlined from `@mixin act`. */
@@ -5884,10 +5730,9 @@
5884
5730
  background-color: var(--qti-bg-active);
5885
5731
  }
5886
5732
 
5887
- &:state(dragzone-enabled)::part(drags) {
5733
+ qti-graphic-associate-interaction:is(:state(dragzone-enabled), [data-state~='dragzone-enabled'])::part(drags) {
5888
5734
  background-color: var(--qti-bg-active);
5889
5735
  }
5890
- }
5891
5736
  }
5892
5737
  @layer qti-components.interactions.base{
5893
5738
  qti-slider-interaction {
@@ -5911,34 +5756,27 @@ qti-slider-interaction {
5911
5756
  * itself (`[part~='correct']`), which is deeper than this host declaration.
5912
5757
  */
5913
5758
  --qti-select-point-marker-color: var(--qti-primary);
5914
-
5915
- &::part(point) {
5916
- &:focus {
5759
+ }
5760
+ :is(qti-select-point-interaction::part(point)):focus {
5917
5761
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
5918
5762
  }
5919
- }
5920
- }
5921
5763
  }
5922
5764
  @layer qti-components.interactions.corrections {
5923
- qti-select-point-interaction {
5924
5765
  /* Same knob as the resting colour above — these set `color` before, which stopped reaching the
5925
5766
  marker once it was no longer painted with `currentColor`. */
5926
- &::part(correct) {
5767
+ qti-select-point-interaction::part(correct) {
5927
5768
  --qti-select-point-marker-color: var(--qti-correct);
5928
5769
  }
5929
5770
 
5930
- &::part(incorrect) {
5771
+ qti-select-point-interaction::part(incorrect) {
5931
5772
  --qti-select-point-marker-color: var(--qti-incorrect);
5932
5773
  }
5933
- }
5934
5774
  }
5935
5775
  @layer qti-components.interactions.base{
5936
- qti-position-object-stage {
5937
- & qti-position-object-interaction {
5776
+ qti-position-object-stage qti-position-object-interaction {
5938
5777
  /* no styles necessary, only layout styles, defined in the component */
5939
5778
  }
5940
5779
  }
5941
- }
5942
5780
  @layer qti-components.interactions.base{
5943
5781
  /* `display: block` lives in qti-prompt.styles.ts, which already declares it on :host. */
5944
5782
  }
@@ -5998,14 +5836,14 @@ qti-position-object-stage {
5998
5836
  /* A cloned interaction presents the answer key rather than a candidate judgement. What is left
5999
5837
  here is the part no interaction owns: any chip, anywhere. The text-entry and inline-choice
6000
5838
  halves moved into their own files — those name one interaction each, so they belong to it. */
6001
- .full-correct-response :state(drag),
5839
+ .full-correct-response :is(:state(drag), [data-state~='drag']),
6002
5840
  .full-correct-response ::part(drag) {
6003
5841
  background-color: var(--qti-answer-bg);
6004
5842
  color: var(--qti-answer-fg);
6005
5843
  border-color: var(--qti-answer-border);
6006
5844
  }
6007
5845
 
6008
- .full-correct-response :state(drag)::part(control)::before,
5846
+ .full-correct-response :is(:state(drag), [data-state~='drag'])::part(control)::before,
6009
5847
  .full-correct-response ::part(drag-control)::before {
6010
5848
  content: '';
6011
5849
  background-color: var(--qti-answer-fg);
@@ -6098,7 +5936,7 @@ qti-feedback-block::part(feedback) {
6098
5936
  * paint. If the double indication ever reads as too much, the fix is here or in the mixin, not
6099
5937
  * a display value.
6100
5938
  */
6101
- :state(show-correct-response) {
5939
+ :is(:state(show-correct-response), [data-state~='show-correct-response']) {
6102
5940
  outline: var(--qti-border-thickness) dashed var(--qti-correct);
6103
5941
  outline-offset: 0.25rem;
6104
5942
  }