@citolab/qti-components 8.0.2 → 8.2.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 (64) hide show
  1. package/cdn/index.global.js +1 -1
  2. package/cdn/index.js +751 -813
  3. package/dist/base.d.ts +63 -6
  4. package/dist/base.js +7 -1
  5. package/dist/{chunk-CK7AV2UJ.js → chunk-43XBJ47H.js} +3 -3
  6. package/dist/{chunk-7V4HJFHW.js → chunk-6ITD55QE.js} +2 -2
  7. package/dist/{chunk-HQESINXH.js → chunk-7D3NKAGZ.js} +137 -3
  8. package/dist/chunk-7D3NKAGZ.js.map +1 -0
  9. package/dist/{chunk-HBSWJC3B.js → chunk-CRLXQZFA.js} +595 -260
  10. package/dist/chunk-CRLXQZFA.js.map +1 -0
  11. package/dist/{chunk-IJD53WY6.js → chunk-HDEG6BAZ.js} +15 -15
  12. package/dist/{chunk-IJD53WY6.js.map → chunk-HDEG6BAZ.js.map} +1 -1
  13. package/dist/{chunk-RSVRS5AK.js → chunk-LMGJOBJA.js} +4 -4
  14. package/dist/{chunk-ZII6M3SN.js → chunk-M7K5C522.js} +2 -2
  15. package/dist/{chunk-J5SQLKNP.js → chunk-OIUK5BFU.js} +22 -6
  16. package/dist/{chunk-J5SQLKNP.js.map → chunk-OIUK5BFU.js.map} +1 -1
  17. package/dist/{chunk-VAE6HBKR.js → chunk-OVBG223G.js} +88 -62
  18. package/dist/chunk-OVBG223G.js.map +1 -0
  19. package/dist/{chunk-TWPXES3S.js → chunk-Q6ZK4LRH.js} +30 -6
  20. package/dist/chunk-Q6ZK4LRH.js.map +1 -0
  21. package/dist/chunk-TLTYXWW7.js +7 -0
  22. package/dist/chunk-TLTYXWW7.js.map +1 -0
  23. package/dist/{chunk-NJQHUUTN.js → chunk-U7YCXDYC.js} +2 -2
  24. package/dist/{chunk-BQFWCZFH.js → chunk-UC3T3NI3.js} +2 -2
  25. package/dist/{chunk-7AVWRQIS.js → chunk-YLRMOX6C.js} +63 -77
  26. package/dist/chunk-YLRMOX6C.js.map +1 -0
  27. package/dist/{computed.types.d-DpKIYO6e.d.ts → computed.types.d-CJarqC6s.d.ts} +12 -0
  28. package/dist/corrections.d.ts +14 -4
  29. package/dist/corrections.js +8 -8
  30. package/dist/elements.d.ts +1 -2
  31. package/dist/elements.js +4 -4
  32. package/dist/index.d.ts +7 -7
  33. package/dist/index.js +30 -14
  34. package/dist/interactions.js +3 -3
  35. package/dist/item.css +451 -532
  36. package/dist/item.d.ts +1 -1
  37. package/dist/item.js +5 -5
  38. package/dist/loader.d.ts +1 -1
  39. package/dist/loader.js +2 -2
  40. package/dist/processing.d.ts +64 -14
  41. package/dist/processing.js +10 -2
  42. package/dist/qti-components-jsx.d.ts +8139 -7358
  43. package/dist/{qti-condition-expression.d-aMvBvpc6.d.ts → qti-condition-expression.d-CYwXOUb1.d.ts} +1 -1
  44. package/dist/{qti-expression.d-BietT_8b.d.ts → qti-expression.d-B_-m9emA.d.ts} +11 -1
  45. package/dist/{qti-feedback.d-DZbad0RN.d.ts → qti-feedback.d-B1EIF-fU.d.ts} +1 -0
  46. package/dist/{qti-test.d-_YAlhDbt.d.ts → qti-test.d-C6lCEMRB.d.ts} +38 -3
  47. package/dist/test.d.ts +5 -5
  48. package/dist/test.js +8 -6
  49. package/dist/transformers.d.ts +1 -1
  50. package/dist/transformers.js +1 -1
  51. package/package.json +9 -9
  52. package/dist/chunk-7AVWRQIS.js.map +0 -1
  53. package/dist/chunk-G6GFQDVN.js +0 -7
  54. package/dist/chunk-G6GFQDVN.js.map +0 -1
  55. package/dist/chunk-HBSWJC3B.js.map +0 -1
  56. package/dist/chunk-HQESINXH.js.map +0 -1
  57. package/dist/chunk-TWPXES3S.js.map +0 -1
  58. package/dist/chunk-VAE6HBKR.js.map +0 -1
  59. /package/dist/{chunk-CK7AV2UJ.js.map → chunk-43XBJ47H.js.map} +0 -0
  60. /package/dist/{chunk-7V4HJFHW.js.map → chunk-6ITD55QE.js.map} +0 -0
  61. /package/dist/{chunk-RSVRS5AK.js.map → chunk-LMGJOBJA.js.map} +0 -0
  62. /package/dist/{chunk-ZII6M3SN.js.map → chunk-M7K5C522.js.map} +0 -0
  63. /package/dist/{chunk-NJQHUUTN.js.map → chunk-U7YCXDYC.js.map} +0 -0
  64. /package/dist/{chunk-BQFWCZFH.js.map → chunk-UC3T3NI3.js.map} +0 -0
package/dist/item.css CHANGED
@@ -1,5 +1,6 @@
1
1
 
2
2
  @layer qti-base, qti-components, qti-extended;
3
+
3
4
  @layer qti-components.reset, qti-components.global, qti-components.interactions.base,
4
5
  qti-components.interactions.brand, qti-components.interactions.states,
5
6
  qti-components.interactions.corrections, qti-components.elements, qti-components.overrides;
@@ -14,6 +15,7 @@
14
15
  * Nothing is left unlayered: unlayered CSS outranks every layer, so a single stray rule here would
15
16
  * sit above the whole theme AND above the `qti-extended` slot consumers are told to author in.
16
17
  */
18
+
17
19
  @layer qti-base{
18
20
  /* https://www.imsglobal.org/sites/default/files/spec/qti/v3/guide/img/qti3p0.css */
19
21
  /* ============================
@@ -1446,6 +1448,7 @@
1446
1448
  text-orientation: upright;
1447
1449
  }
1448
1450
  }
1451
+
1449
1452
  @layer qti-base{
1450
1453
 
1451
1454
  .qti-layout-row {
@@ -1577,6 +1580,7 @@
1577
1580
  * block is appended after every declared layer, which would put brand paint above the state and
1578
1581
  * correction paint it is meant to sit under. Deleting the two names moved 7 of the 17 VRT baselines.
1579
1582
  */
1583
+
1580
1584
  @layer qti-components.reset{
1581
1585
  @scope (qti-item-body, .ProseMirror) {
1582
1586
  :scope,
@@ -1628,6 +1632,7 @@
1628
1632
  }
1629
1633
  }
1630
1634
  }
1635
+
1631
1636
  @layer qti-components.elements{
1632
1637
 
1633
1638
  /**
@@ -1699,6 +1704,7 @@
1699
1704
  * these blocks (--qti-correct reads --qti-success) is layer-independent. Nothing here renders
1700
1705
  * differently for being grouped this way.
1701
1706
  */
1707
+
1702
1708
  /* ============================================================================
1703
1709
  * global — the foundation every layer reads.
1704
1710
  * ========================================================================== */
@@ -1893,6 +1899,7 @@
1893
1899
  }
1894
1900
  }
1895
1901
  }
1902
+
1896
1903
  /* ============================================================================
1897
1904
  * interactions.base — interaction-local geometry. Neutral, no judgement colour.
1898
1905
  * ========================================================================== */
@@ -2064,6 +2071,7 @@
2064
2071
  --qti-drag-padding: var(--qti-spacing) 0.8em;
2065
2072
  }
2066
2073
  }
2074
+
2067
2075
  /* ============================================================================
2068
2076
  * interactions.states — state paint. Selection, drag, and drop-target highlight.
2069
2077
  * ========================================================================== */
@@ -2110,6 +2118,7 @@
2110
2118
  --qti-drop-highlight-border-width: 2px;
2111
2119
  }
2112
2120
  }
2121
+
2113
2122
  /* ============================================================================
2114
2123
  * interactions.corrections — answer-key and judgement paint.
2115
2124
  * ========================================================================== */
@@ -2134,15 +2143,18 @@
2134
2143
  /* QTI base — shared theme utilities and semantic mixins. Only styles >=2 interactions share.
2135
2144
  * They read the variables in qti-variables.css; a substrate re-colours by overriding those, not
2136
2145
  * by editing these. Split from qti-base.css. */
2146
+
2137
2147
  /* ============================================================================
2138
2148
  * Shared semantic mixins. Only styles ≥2 interactions share belong here.
2139
2149
  * A substrate expresses its look by overriding the --qti-* variables above
2140
2150
  * (value first), not by editing these.
2141
2151
  * ========================================================================== */
2152
+
2142
2153
  /*
2143
2154
  * Semantic PostCSS mixins. Interaction files wire these to their own parts and states; the mixins
2144
2155
  * own the shared visual vocabulary.
2145
2156
  */
2157
+
2146
2158
  /*
2147
2159
  * A candidate correction, painted through the corrected element's own role.
2148
2160
  *
@@ -2159,6 +2171,7 @@
2159
2171
  *
2160
2172
  * `candidate-tint` below survives for the one corrected element with no role: qti-hottext.
2161
2173
  */
2174
+
2162
2175
  /*
2163
2176
  * THE paint contract. Every visual component is, at rest, a bordered box on the theme's surface.
2164
2177
  * This mixin binds each PAINT property to a variable exactly once, so a state — or a theme —
@@ -2189,6 +2202,7 @@
2189
2202
  * rings) and a MULTI-SLOT box-shadow (so an element's resting lift, in-flight elevation and focus
2190
2203
  * ring compose instead of the last one winning — the documented four-rule box-shadow contention).
2191
2204
  */
2205
+
2192
2206
  /*
2193
2207
  * A drag: the movable token a candidate drags into a gap, a drop target or an order slot.
2194
2208
  *
@@ -2206,6 +2220,7 @@
2206
2220
  * Under content-box the clone's padding would be added to the width the drag code measured off the
2207
2221
  * bank drag, and it would jump the instant it was picked up.
2208
2222
  */
2223
+
2209
2224
  /*
2210
2225
  * The hole a drag leaves behind: lifted mid-drag, or spent because its copy is already placed.
2211
2226
  *
@@ -2216,6 +2231,7 @@
2216
2231
  * bank would reflow around a hole 20px narrower than the thing that left it. Measured in
2217
2232
  * drag-drop.invariance.spec.ts, "a spent drag keeps its footprint".
2218
2233
  */
2234
+
2219
2235
  /*
2220
2236
  * The drag handle glyph.
2221
2237
  *
@@ -2228,6 +2244,7 @@
2228
2244
  * else. This one is in flow and takes real width, which is why it reserves an explicit width/height
2229
2245
  * rather than leaning on the glyph's intrinsic size.
2230
2246
  */
2247
+
2231
2248
  /*
2232
2249
  * The radio and the checkbox: the other two things this theme draws as boxes, and so the other two
2233
2250
  * paint roles. A control is a bordered box on the theme's surface exactly like a chip is, and now it
@@ -2248,6 +2265,7 @@
2248
2265
  * not a paint slot. Centring the mark is the component's job — qti-simple-choice.styles.ts already
2249
2266
  * flex-centres [part='control'].
2250
2267
  */
2268
+
2251
2269
  /*
2252
2270
  * The mark inside a control at rest — the box the tick or dot is drawn into. Structural.
2253
2271
  *
@@ -2260,10 +2278,12 @@
2260
2278
  * border — so it would silently shrink by the border width and change with it. Multiplying the token
2261
2279
  * keeps the mark proportional to the control's outer size, which is the size anyone reasons about.
2262
2280
  */
2281
+
2263
2282
  /* A choice with its native control shown (radio/checkbox visible) is the button component with its
2264
2283
  border made TRANSPARENT — no visible box, but the 1px edge is still there, so it is the same box
2265
2284
  as the `.qti-input-control-hidden` choice (which keeps the border visible because the whole choice
2266
2285
  is the control). Only the border COLOUR differs between the two, never the geometry. */
2286
+
2267
2287
  /*
2268
2288
  * The mark a checked control shows: a dot for a radio, a tick for a checkbox.
2269
2289
  *
@@ -2272,6 +2292,7 @@
2272
2292
  * `control-selected`), so the two belong together: override --checkbox-background-color and you will
2273
2293
  * usually want --checkbox-mark-color with it. `white` is the resting value, not a fixed one.
2274
2294
  */
2295
+
2275
2296
  /*
2276
2297
  * `act`, `readonly` and `dis` used to live here. They are written out at their call sites now, for
2277
2298
  * two different reasons.
@@ -2293,16 +2314,20 @@
2293
2314
  *
2294
2315
  * `act-bor` was deleted outright: zero call sites anywhere in the repo.
2295
2316
  */
2317
+
2296
2318
  /* `foc` stays a mixin, deliberately, and is the one state still painting a property directly.
2297
2319
  * Twenty call sites read it, and it draws a `box-shadow` — the property the contract has NOT bound
2298
2320
  * (see `paint` above). Routing it through --<role>-box-shadow would not fix anything: focus would
2299
2321
  * then fight the resting lift and the in-flight elevation over one slot, last-declared winning,
2300
2322
  * which is the four-rule contention already documented in qti-variables.css. The intended fix
2301
2323
  * recorded there is a single `outline` on `:focus-visible`, and that is its own VRT-checked step. */
2324
+
2302
2325
  /* A choice — the clickable box a candidate picks. Nothing but the resting paint box; its padding is
2303
2326
  the interaction's (qti-choice-interaction.css), because a choice's box differs per interaction and
2304
2327
  a chip's does not. */
2328
+
2305
2329
  /* A text field: the paint box, plus the cursor that says you can type in it. */
2330
+
2306
2331
  /*
2307
2332
  * A drop target: the paint box with a DASHED edge, the one thing that distinguishes it from every
2308
2333
  * other role.
@@ -2317,9 +2342,11 @@
2317
2342
  * dashed edge onto any chip standing in the drop (it did — a chip in a drop went dashed). Now that
2318
2343
  * the chip reads --drag-border-style, the drop's value inherits past it harmlessly.
2319
2344
  */
2345
+
2320
2346
  /* The initial `display: none` moved to `validationMessage` in qti-base — the message is a shadow
2321
2347
  node, and `Interaction.reportValidity` already owns showing and hiding it with an inline style.
2322
2348
  What is left here is paint. */
2349
+
2323
2350
  /*
2324
2351
  * The three roles qti-inline-choice-interaction is built from. They are separate roles rather than
2325
2352
  * one, because they nest: the menu and the message live in the field's shadow root, so under a
@@ -2330,12 +2357,14 @@
2330
2357
  * `menu` the popover the options are listed in.
2331
2358
  * `message` the validation box. Its warning colouring is the interaction's, not the role's.
2332
2359
  */
2360
+
2333
2361
  /*
2334
2362
  * A drag in flight. The other `drag` modifier — applied with `drag`, never instead of it.
2335
2363
  *
2336
2364
  * `rotate` is paint: a transform never reflows, which is what lets a drag tilt under the cursor
2337
2365
  * without the bank shifting behind it.
2338
2366
  */
2367
+
2339
2368
  /*
2340
2369
  * A drag darkens a little under the pointer. The one hover affordance in the theme.
2341
2370
  *
@@ -2363,16 +2392,15 @@
2363
2392
  Handed to qti-simple-choice.styles.ts, which owns the label's box; this was an
2364
2393
  `::part(label) { display: inline !important }`. */
2365
2394
  --qti-choice-label-display: inline;
2395
+ }
2366
2396
 
2367
- & qti-simple-choice {
2397
+ qti-choice-interaction qti-simple-choice {
2368
2398
  padding: var(--qti-padding-box);
2369
2399
  width: -moz-fit-content;
2370
2400
  width: fit-content;
2371
2401
  cursor: pointer;
2372
2402
  }
2373
-
2374
- &.qti-input-control-hidden {
2375
- & qti-simple-choice {
2403
+ qti-choice-interaction.qti-input-control-hidden qti-simple-choice {
2376
2404
  background-color: var(--button-background-color, var(--qti-component-background-color));
2377
2405
  background-image: var(--button-background-image, var(--qti-component-background-image));
2378
2406
  border-width: var(--button-border-width, var(--qti-component-border-width));
@@ -2384,14 +2412,11 @@
2384
2412
  opacity: var(--button-opacity, var(--qti-component-opacity));
2385
2413
  filter: var(--button-filter, var(--qti-component-filter));
2386
2414
  --qti-control-display: none;
2387
- &:focus {
2415
+ }
2416
+ :is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice):focus {
2388
2417
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
2389
2418
  }
2390
- }
2391
- }
2392
-
2393
- &:not(.qti-input-control-hidden) {
2394
- & qti-simple-choice {
2419
+ qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice {
2395
2420
  background-color: var(--button-background-color, var(--qti-component-background-color));
2396
2421
  background-image: var(--button-background-image, var(--qti-component-background-image));
2397
2422
  border-width: var(--button-border-width, var(--qti-component-border-width));
@@ -2406,52 +2431,46 @@
2406
2431
  gap: 0.5rem;
2407
2432
  outline: none;
2408
2433
  cursor: pointer;
2434
+ }
2409
2435
 
2410
- &:focus {
2436
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):focus {
2411
2437
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
2412
2438
  }
2413
2439
 
2414
- &::part(control-mark) {
2440
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice)::part(control-mark) {
2415
2441
  width: calc(var(--qti-control-size) * 0.625);
2416
2442
  height: calc(var(--qti-control-size) * 0.625);
2417
2443
  }
2418
- }
2419
- }
2420
- }
2421
2444
  }
2445
+
2422
2446
  @layer qti-components.interactions.states {
2423
- qti-choice-interaction {
2424
- &.qti-input-control-hidden qti-simple-choice {
2425
2447
  /* A selected choice, repainted through the button role's slots rather than by re-declaring the
2426
2448
  properties — the choice is `@mixin button`, so switching a variable is all a state has to do.
2427
2449
  This used to be `@mixin act` followed by two direct declarations, and half of `act` was dead
2428
2450
  on arrival: it set `background-color: var(--qti-bg-active)` and the very next line overwrote
2429
2451
  it with the selected fill. Only its border-colour ever survived, which is written out here. */
2430
- &:state(checked) {
2452
+ :is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice):is(:state(checked), [data-state~='checked']) {
2431
2453
  --button-border-color: var(--qti-border-active);
2432
2454
  --button-background-color: var(--qti-selected-bg);
2433
2455
  --button-color: var(--qti-selected-color);
2434
2456
  }
2435
2457
 
2436
- &:state(readonly) {
2458
+ :is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice):is(:state(readonly), [data-state~='readonly']) {
2437
2459
  cursor: pointer;
2438
2460
  }
2439
2461
 
2440
- &:state(disabled) {
2462
+ :is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice):is(:state(disabled), [data-state~='disabled']) {
2441
2463
  cursor: not-allowed;
2442
2464
  }
2443
- }
2444
-
2445
- &:not(.qti-input-control-hidden) qti-simple-choice {
2446
- &:state(readonly) {
2465
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(readonly), [data-state~='readonly']) {
2447
2466
  cursor: pointer;
2448
2467
  }
2449
2468
 
2450
- &:state(disabled) {
2469
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(disabled), [data-state~='disabled']) {
2451
2470
  cursor: not-allowed;
2452
2471
  }
2453
2472
 
2454
- &:state(radio)::part(control) {
2473
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(radio), [data-state~='radio'])::part(control) {
2455
2474
  background-color: var(--radio-background-color, var(--qti-component-background-color));
2456
2475
  background-image: var(--radio-background-image, var(--qti-component-background-image));
2457
2476
  border-width: var(--radio-border-width, var(--qti-component-border-width));
@@ -2469,13 +2488,12 @@
2469
2488
  border-radius: var(--radio-border-radius, 100%);
2470
2489
  }
2471
2490
 
2472
- &:state(radio):state(checked)::part(control),
2473
- &:state(checkbox):state(checked)::part(control) {
2491
+ :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
2492
  border-color: var(--qti-primary);
2475
2493
  background-color: var(--qti-primary);
2476
2494
  }
2477
2495
 
2478
- &:state(radio):state(checked)::part(control-mark) {
2496
+ :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
2497
  /* 0.5 reproduces today's 8px at the default 1rem — see mark-size for why this is a fraction. */
2480
2498
  width: calc(var(--qti-control-size) * 0.5);
2481
2499
  height: calc(var(--qti-control-size) * 0.5);
@@ -2483,7 +2501,7 @@
2483
2501
  border-radius: 100%;
2484
2502
  }
2485
2503
 
2486
- &:state(checkbox)::part(control) {
2504
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(checkbox), [data-state~='checkbox'])::part(control) {
2487
2505
  background-color: var(--checkbox-background-color, var(--qti-component-background-color));
2488
2506
  background-image: var(--checkbox-background-image, var(--qti-component-background-image));
2489
2507
  border-width: var(--checkbox-border-width, var(--qti-component-border-width));
@@ -2498,27 +2516,18 @@
2498
2516
  height: var(--qti-control-size);
2499
2517
  }
2500
2518
 
2501
- &:state(checkbox):state(checked)::part(control-mark) {
2519
+ :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
2520
  background-color: var(--checkbox-mark-color, white);
2503
2521
  -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
2522
  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
2523
  }
2506
- }
2507
- }
2508
2524
  }
2525
+
2509
2526
  @layer qti-components.interactions.corrections {
2510
- qti-choice-interaction {
2511
- & qti-simple-choice {
2512
- &:state(correct-response),
2513
- &[data-correct-response='true'] {
2514
- &::after {
2527
+ :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
2528
  content: '\02714';
2516
2529
  color: var(--qti-correct);
2517
2530
  }
2518
- }
2519
- }
2520
-
2521
- &.qti-input-control-hidden qti-simple-choice {
2522
2531
  /*
2523
2532
  * Paint only. This used to set `border: 3px` over a 1px base and add `padding-right: 30px`,
2524
2533
  * so revealing the answer key grew every correct choice and reflowed the list around it.
@@ -2527,12 +2536,12 @@
2527
2536
  * costs no layout. The 30px that kept the label clear of the tick is reserved in the base
2528
2537
  * rule for this variant, whether or not a tick ever appears.
2529
2538
  */
2530
- &:state(correct-response),
2531
- &[data-correct-response='true'] {
2539
+ :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
2540
  border-color: var(--qti-correct) !important;
2533
2541
  box-shadow: inset 0 0 0 2px var(--qti-correct);
2542
+ }
2534
2543
 
2535
- &::after {
2544
+ :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
2545
  content: '\02714';
2537
2546
  color: var(--qti-correct);
2538
2547
  position: absolute;
@@ -2541,94 +2550,75 @@
2541
2550
  font-size: 1.2em;
2542
2551
  font-weight: bold;
2543
2552
  }
2544
- }
2545
2553
 
2546
2554
  /* Candidate judgement follows checked/correct-response paint so it can
2547
2555
  recolour the same stable box without changing its geometry. */
2548
- &:state(candidate-correct) {
2556
+ :is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice):is(:state(candidate-correct), [data-state~='candidate-correct']) {
2549
2557
  --button-background-color: var(--qti-correct-light);
2550
2558
  --button-color: var(--qti-correct);
2551
2559
  --button-border-color: var(--qti-correct);
2552
2560
  --button-box-shadow: none;
2553
2561
  }
2554
2562
 
2555
- &:state(candidate-incorrect) {
2563
+ :is(qti-choice-interaction.qti-input-control-hidden qti-simple-choice):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
2556
2564
  --button-background-color: var(--qti-incorrect-light);
2557
2565
  --button-color: var(--qti-incorrect);
2558
2566
  --button-border-color: var(--qti-incorrect);
2559
2567
  --button-box-shadow: none;
2560
2568
  }
2561
- }
2562
-
2563
- &:not(.qti-input-control-hidden) qti-simple-choice {
2564
- &:state(candidate-correct) {
2569
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(candidate-correct), [data-state~='candidate-correct']) {
2565
2570
  background-color: transparent;
2571
+ }
2566
2572
 
2567
- &:state(radio):state(checked)::part(control),
2568
- &:state(checkbox):state(checked)::part(control) {
2573
+ :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
2574
  border-color: var(--qti-correct);
2570
2575
  background-color: var(--qti-correct);
2571
2576
  }
2572
- }
2573
2577
 
2574
- &:state(candidate-incorrect) {
2578
+ :is(qti-choice-interaction:not(.qti-input-control-hidden) qti-simple-choice):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
2575
2579
  background-color: transparent;
2580
+ }
2576
2581
 
2577
- &:state(radio):state(checked)::part(control),
2578
- &:state(checkbox):state(checked)::part(control) {
2582
+ :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
2583
  border-color: var(--qti-incorrect);
2580
2584
  background-color: var(--qti-incorrect);
2581
2585
  }
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) {
2586
+ :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
2587
  background-color: var(--qti-answer-bg);
2591
2588
  color: var(--qti-answer-fg);
2592
2589
  border-color: var(--qti-answer-border);
2593
2590
  }
2594
2591
 
2595
- &:state(radio):state(checked)::part(control-mark),
2596
- &:state(checkbox):state(checked)::part(control-mark) {
2592
+ :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
2593
  background-color: var(--qti-answer-fg);
2598
2594
  }
2599
- }
2600
2595
 
2601
- &.qti-input-control-hidden qti-simple-choice:state(checked) {
2596
+ .qti-input-control-hidden:is(.full-correct-response qti-choice-interaction) qti-simple-choice:is(:state(checked), [data-state~='checked']) {
2602
2597
  background-color: var(--qti-answer-bg);
2603
2598
  color: var(--qti-answer-fg);
2604
2599
  border-color: var(--qti-answer-border);
2605
2600
  }
2606
- }
2607
2601
  }
2602
+
2608
2603
  @layer qti-components.interactions.base {
2609
- qti-graphic-gap-match-interaction {
2610
- &.qti-selections-light {
2611
- &:state(dragzone-active)::part(drags) {
2604
+ qti-graphic-gap-match-interaction.qti-selections-light:is(:state(dragzone-active), [data-state~='dragzone-active'])::part(drags) {
2612
2605
  background-color: var(--qti-selections-light-bg);
2613
2606
  border-color: var(--qti-selections-light-border);
2614
2607
  }
2615
- &:state(dragzone-enabled)::part(drags) {
2608
+ qti-graphic-gap-match-interaction.qti-selections-light:is(:state(dragzone-enabled), [data-state~='dragzone-enabled'])::part(drags) {
2616
2609
  background-color: var(--qti-selections-light-bg);
2617
2610
  }
2618
- }
2619
- &.qti-selections-dark {
2620
- &:state(dragzone-active)::part(drags) {
2611
+ qti-graphic-gap-match-interaction.qti-selections-dark:is(:state(dragzone-active), [data-state~='dragzone-active'])::part(drags) {
2621
2612
  background-color: var(--qti-selections-dark-bg);
2622
2613
  border-color: var(--qti-selections-dark-border);
2623
2614
  }
2624
- &:state(dragzone-enabled)::part(drags) {
2615
+ qti-graphic-gap-match-interaction.qti-selections-dark:is(:state(dragzone-enabled), [data-state~='dragzone-enabled'])::part(drags) {
2625
2616
  background-color: var(--qti-selections-dark-bg);
2626
2617
  }
2627
- }
2628
- &:state(dragzone-active)::part(drags) {
2618
+ qti-graphic-gap-match-interaction:is(:state(dragzone-active), [data-state~='dragzone-active'])::part(drags) {
2629
2619
  background-color: var(--qti-bg-active);
2630
2620
  }
2631
- &:state(dragzone-enabled)::part(drags) {
2621
+ qti-graphic-gap-match-interaction:is(:state(dragzone-enabled), [data-state~='dragzone-enabled'])::part(drags) {
2632
2622
  background-color: var(--qti-bg-active);
2633
2623
  }
2634
2624
  /*
@@ -2643,17 +2633,14 @@
2643
2633
  /* A chip here is a tile filling its hotspot, so it centres its content — see
2644
2634
  qti-gap-img.styles.ts and qti-gap-text.styles.ts. gap-match's chips hug their text instead,
2645
2635
  which is why the justification is a variable rather than the element's default. */
2646
- & qti-gap-img,
2647
- & qti-gap-text {
2636
+ qti-graphic-gap-match-interaction qti-gap-img,qti-graphic-gap-match-interaction qti-gap-text {
2648
2637
  --qti-drag-justify-content: center;
2649
2638
  cursor: grab;
2650
2639
  }
2651
2640
  /* 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 {
2641
+ :is(qti-graphic-gap-match-interaction qti-associable-hotspot::part(drag)):focus {
2654
2642
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
2655
2643
  }
2656
- }
2657
2644
  /*
2658
2645
  * The hotspot centres its chip in qti-associable-hotspot.styles.ts.
2659
2646
  *
@@ -2667,7 +2654,7 @@
2667
2654
  * Adding a rule would be a redundant restatement of this line. Left as a note rather than code
2668
2655
  * because the absence is the kind of thing that reads as an oversight.
2669
2656
  */
2670
- & qti-associable-hotspot {
2657
+ qti-graphic-gap-match-interaction qti-associable-hotspot {
2671
2658
  border: 2px solid transparent;
2672
2659
  /* No .qti-selections-* branches in here. They used to sit inside both of these blocks as
2673
2660
  *descendant* selectors — and the class is on the interaction, an ancestor of this hotspot,
@@ -2675,19 +2662,20 @@
2675
2662
  see the :state(filled) rules below.
2676
2663
  A drop target lighting up mid-drag is not a "selection" anyway — Appendix B calls this
2677
2664
  Selections theming, and the selection is the chip once it has landed. */
2678
- &[enabled] {
2665
+ }
2666
+ [enabled]:is(qti-graphic-gap-match-interaction qti-associable-hotspot) {
2679
2667
  background-color: var(--qti-bg-active);
2680
2668
  }
2681
- &[active] {
2669
+ [active]:is(qti-graphic-gap-match-interaction qti-associable-hotspot) {
2682
2670
  background-color: var(--qti-bg-active);
2683
2671
  }
2684
- &[disabled] {
2672
+ [disabled]:is(qti-graphic-gap-match-interaction qti-associable-hotspot) {
2685
2673
  cursor: not-allowed;
2686
- &:not(:empty) {
2674
+ }
2675
+ [disabled]:is(qti-graphic-gap-match-interaction qti-associable-hotspot):not(:empty) {
2687
2676
  cursor: default !important;
2688
2677
  }
2689
- }
2690
- &:not(:empty) {
2678
+ :is(qti-graphic-gap-match-interaction qti-associable-hotspot):not(:empty) {
2691
2679
  border-color: var(--qti-border) !important;
2692
2680
  }
2693
2681
  /*
@@ -2709,30 +2697,29 @@
2709
2697
 
2710
2698
  /* `flex: 1` left with the rest of the drop's layout; dropRegion in qti-base gives a hosted
2711
2699
  chip `flex: 0 0 auto`, which is the invariance rule this was quietly fighting. */
2712
- &:not(:empty) > * {
2700
+ :is(qti-graphic-gap-match-interaction qti-associable-hotspot):not(:empty) > * {
2713
2701
  transform: rotate(0);
2714
2702
  box-shadow: 0 0 0 1px #e5e7eb;
2715
2703
  }
2716
- }
2717
2704
  /* `:not(:empty)` tested light-DOM children. The hotspot renders its chips into its own shadow
2718
2705
  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) {
2706
+ qti-graphic-gap-match-interaction.qti-selections-light qti-associable-hotspot:is(:state(filled), [data-state~='filled']) {
2720
2707
  background-color: var(--qti-selections-light-bg) !important;
2721
2708
  border-color: var(--qti-selections-light-border) !important;
2722
2709
  }
2723
- &.qti-selections-dark qti-associable-hotspot:state(filled) {
2710
+ qti-graphic-gap-match-interaction.qti-selections-dark qti-associable-hotspot:is(:state(filled), [data-state~='filled']) {
2724
2711
  background-color: var(--qti-selections-dark-bg) !important;
2725
2712
  border-color: var(--qti-selections-dark-border) !important;
2726
2713
  }
2727
- & img {
2714
+ qti-graphic-gap-match-interaction img {
2728
2715
  margin: 0;
2729
2716
  padding: 0;
2730
2717
  }
2731
- }
2732
2718
  }
2719
+
2733
2720
  @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) {
2721
+ qti-graphic-gap-match-interaction qti-gap-img:is(:state(placeholder), [data-state~='placeholder']),
2722
+ qti-graphic-gap-match-interaction qti-gap-text:is(:state(placeholder), [data-state~='placeholder']) {
2736
2723
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
2737
2724
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
2738
2725
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -2743,8 +2730,8 @@
2743
2730
  pointer-events: none;
2744
2731
  }
2745
2732
 
2746
- qti-graphic-gap-match-interaction qti-gap-img:state(dragging),
2747
- qti-graphic-gap-match-interaction qti-gap-text:state(dragging) {
2733
+ qti-graphic-gap-match-interaction qti-gap-img:is(:state(dragging), [data-state~='dragging']),
2734
+ qti-graphic-gap-match-interaction qti-gap-text:is(:state(dragging), [data-state~='dragging']) {
2748
2735
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
2749
2736
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
2750
2737
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -2758,29 +2745,30 @@
2758
2745
  pointer-events: none;
2759
2746
  }
2760
2747
 
2761
- qti-graphic-gap-match-interaction qti-gap-img:state(return-anchor),
2762
- qti-graphic-gap-match-interaction qti-gap-text:state(return-anchor) {
2748
+ qti-graphic-gap-match-interaction qti-gap-img:is(:state(return-anchor), [data-state~='return-anchor']),
2749
+ qti-graphic-gap-match-interaction qti-gap-text:is(:state(return-anchor), [data-state~='return-anchor']) {
2763
2750
  border-color: var(--qti-drop-highlight-border-color);
2764
2751
  box-shadow: inset 0 0 0 var(--qti-drop-highlight-border-width, 2px)
2765
2752
  color-mix(in srgb, var(--qti-drop-highlight-border-color) 55%, transparent);
2766
2753
  }
2767
2754
 
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) > * {
2755
+ qti-graphic-gap-match-interaction qti-gap-img:is(:state(placeholder), [data-state~='placeholder']) > *,
2756
+ qti-graphic-gap-match-interaction qti-gap-text:is(:state(placeholder), [data-state~='placeholder']) > *,
2757
+ qti-graphic-gap-match-interaction qti-gap-img:is(:state(dragging), [data-state~='dragging']) > *,
2758
+ qti-graphic-gap-match-interaction qti-gap-text:is(:state(dragging), [data-state~='dragging']) > * {
2772
2759
  visibility: hidden;
2773
2760
  }
2774
2761
 
2775
- qti-graphic-gap-match-interaction qti-associable-hotspot:state(hover),
2776
- qti-graphic-gap-match-interaction qti-associable-hotspot:state(active) {
2762
+ qti-graphic-gap-match-interaction qti-associable-hotspot:is(:state(hover), [data-state~='hover']),
2763
+ qti-graphic-gap-match-interaction qti-associable-hotspot:is(:state(active), [data-state~='active']) {
2777
2764
  background-color: var(--qti-drop-highlight-bg);
2778
2765
  }
2779
2766
 
2780
- qti-graphic-gap-match-interaction qti-associable-hotspot:state(enabled) {
2767
+ qti-graphic-gap-match-interaction qti-associable-hotspot:is(:state(enabled), [data-state~='enabled']) {
2781
2768
  background-color: var(--qti-drop-highlight-enabled-bg);
2782
2769
  }
2783
2770
  }
2771
+
2784
2772
  @layer qti-components.interactions.corrections {
2785
2773
  /*
2786
2774
  * The one place this interaction joins the shared chip vocabulary: its chips take no card, but they
@@ -2795,22 +2783,23 @@
2795
2783
  * Moved here from qti-corrections.css, whose unscoped `:state(drag)` rule covered these chips
2796
2784
  * along with everyone else's.
2797
2785
  */
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) {
2786
+ qti-graphic-gap-match-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-correct), [data-state~='candidate-correct']),
2787
+ qti-graphic-gap-match-interaction qti-associable-hotspot::part(drag):is(:state(candidate-correct), [data-state~='candidate-correct']) {
2800
2788
  background-color: var(--qti-correct-light);
2801
2789
  color: var(--qti-correct);
2802
2790
  border-color: var(--qti-correct);
2803
2791
  box-shadow: none;
2804
2792
  }
2805
2793
 
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) {
2794
+ qti-graphic-gap-match-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
2795
+ qti-graphic-gap-match-interaction qti-associable-hotspot::part(drag):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
2808
2796
  background-color: var(--qti-incorrect-light);
2809
2797
  color: var(--qti-incorrect);
2810
2798
  border-color: var(--qti-incorrect);
2811
2799
  box-shadow: none;
2812
2800
  }
2813
2801
  }
2802
+
2814
2803
  @layer qti-components.interactions.base {
2815
2804
  qti-text-entry-interaction {
2816
2805
  background-color: var(--text-background-color, var(--qti-component-background-color));
@@ -2824,8 +2813,9 @@
2824
2813
  opacity: var(--text-opacity, var(--qti-component-opacity));
2825
2814
  filter: var(--text-filter, var(--qti-component-filter));
2826
2815
  cursor: text;
2816
+ }
2827
2817
 
2828
- &::part(input) {
2818
+ qti-text-entry-interaction::part(input) {
2829
2819
  color: var(--qti-fg);
2830
2820
  font-family: inherit;
2831
2821
  font-size: 1rem;
@@ -2836,31 +2826,29 @@
2836
2826
  border: none;
2837
2827
  background-color: transparent;
2838
2828
  }
2839
- }
2840
2829
  }
2830
+
2841
2831
  @layer qti-components.interactions.corrections {
2842
- qti-text-entry-interaction {
2843
- &:state(candidate-partially-correct) {
2832
+ qti-text-entry-interaction:is(:state(candidate-partially-correct), [data-state~='candidate-partially-correct']) {
2844
2833
  --text-background-color: var(--qti-partially-correct-light);
2845
2834
  --text-color: var(--qti-partially-correct);
2846
2835
  --text-border-color: var(--qti-partially-correct);
2847
2836
  --text-box-shadow: none;
2848
2837
  }
2849
2838
 
2850
- &:state(candidate-correct) {
2839
+ qti-text-entry-interaction:is(:state(candidate-correct), [data-state~='candidate-correct']) {
2851
2840
  --text-background-color: var(--qti-correct-light);
2852
2841
  --text-color: var(--qti-correct);
2853
2842
  --text-border-color: var(--qti-correct);
2854
2843
  --text-box-shadow: none;
2855
2844
  }
2856
2845
 
2857
- &:state(candidate-incorrect) {
2846
+ qti-text-entry-interaction:is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
2858
2847
  --text-background-color: var(--qti-incorrect-light);
2859
2848
  --text-color: var(--qti-incorrect);
2860
2849
  --text-border-color: var(--qti-incorrect);
2861
2850
  --text-box-shadow: none;
2862
2851
  }
2863
- }
2864
2852
 
2865
2853
  /* The answer key, moved here from the shared qti-corrections.css: it names this interaction, so
2866
2854
  this file owns it. */
@@ -2868,21 +2856,21 @@
2868
2856
  background-color: var(--qti-answer-bg);
2869
2857
  color: var(--qti-answer-fg);
2870
2858
  border-color: var(--qti-answer-border);
2859
+ }
2871
2860
 
2872
- &::part(input) {
2861
+ :is(.full-correct-response qti-text-entry-interaction)::part(input) {
2873
2862
  color: var(--qti-answer-fg);
2874
2863
  }
2875
2864
 
2876
- &::part(answer) {
2865
+ :is(.full-correct-response qti-text-entry-interaction)::part(answer) {
2877
2866
  content: '';
2878
2867
  background-color: var(--qti-answer-fg);
2879
2868
  mask: var(--qti-check-mask) no-repeat center / contain;
2880
2869
  -webkit-mask: var(--qti-check-mask) no-repeat center / contain;
2881
2870
  }
2882
- }
2883
2871
  }
2872
+
2884
2873
  @layer qti-components.interactions.base {
2885
- qti-extended-text-interaction {
2886
2874
  /*
2887
2875
  * One block, and the ORDER inside it matters.
2888
2876
  *
@@ -2896,7 +2884,7 @@
2896
2884
  * slot is unset, so it falls through to --qti-component-color, which is `inherit` — the same
2897
2885
  * colour the textarea already had.
2898
2886
  */
2899
- &::part(textarea) {
2887
+ qti-extended-text-interaction::part(textarea) {
2900
2888
  color: var(--qti-fg);
2901
2889
  font-family: inherit;
2902
2890
  font-size: 1rem;
@@ -2954,14 +2942,14 @@
2954
2942
  * that makes the preserved indentation legible. Typography only — the box does not move, so
2955
2943
  * the layout-invariance contract holds.
2956
2944
  */
2957
- &[format='preformatted']::part(textarea) {
2945
+ qti-extended-text-interaction[format='preformatted']::part(textarea) {
2958
2946
  font-family: monospace;
2959
2947
  -moz-tab-size: 2;
2960
2948
  -o-tab-size: 2;
2961
2949
  tab-size: 2;
2962
2950
  }
2963
- }
2964
2951
  }
2952
+
2965
2953
  /*
2966
2954
  * Corrections for the one interaction that cannot judge itself.
2967
2955
  *
@@ -2981,8 +2969,7 @@
2981
2969
  * positioned by the component and painted by ::part(correction-*) in qti-corrections.css.
2982
2970
  */
2983
2971
  @layer qti-components.interactions.corrections {
2984
- qti-extended-text-interaction {
2985
- &:state(candidate-correct)::part(textarea) {
2972
+ qti-extended-text-interaction:is(:state(candidate-correct), [data-state~='candidate-correct'])::part(textarea) {
2986
2973
  --text-background-color: var(--qti-correct-light);
2987
2974
  --text-color: var(--qti-correct);
2988
2975
  --text-border-color: var(--qti-correct);
@@ -2995,20 +2982,19 @@
2995
2982
  verbatim from the dead class-based rules, where it had never rendered and so was never seen to
2996
2983
  be wrong. With --qti-fg now correctly resolving to `inherit`, it would have been more visibly
2997
2984
  wrong still. */
2998
- &:state(candidate-partially-correct)::part(textarea) {
2985
+ qti-extended-text-interaction:is(:state(candidate-partially-correct), [data-state~='candidate-partially-correct'])::part(textarea) {
2999
2986
  --text-background-color: var(--qti-partially-correct-light);
3000
2987
  --text-color: var(--qti-partially-correct);
3001
2988
  --text-border-color: var(--qti-partially-correct);
3002
2989
  --text-box-shadow: none;
3003
2990
  }
3004
2991
 
3005
- &:state(candidate-incorrect)::part(textarea) {
2992
+ qti-extended-text-interaction:is(:state(candidate-incorrect), [data-state~='candidate-incorrect'])::part(textarea) {
3006
2993
  --text-background-color: var(--qti-incorrect-light);
3007
2994
  --text-color: var(--qti-incorrect);
3008
2995
  --text-border-color: var(--qti-incorrect);
3009
2996
  --text-box-shadow: none;
3010
2997
  }
3011
- }
3012
2998
  }
3013
2999
  /* This interaction's chip vocabulary — the bank chip, the placed chip, and the floating clone. One
3014
3000
  such block per drag-drop interaction, opt-in; graphic-gap-match deliberately declares none. See
@@ -3026,7 +3012,7 @@
3026
3012
  sat on document.body it was in a different tree from the adopted stylesheet, and nothing — no
3027
3013
  part, no attribute — could style it. */
3028
3014
  @layer qti-components.global {
3029
- qti-gap-match-interaction :state(drag),
3015
+ qti-gap-match-interaction :is(:state(drag), [data-state~='drag']),
3030
3016
  [data-drag-clone][data-drag-interaction='qti-gap-match-interaction'] {
3031
3017
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
3032
3018
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
@@ -3077,7 +3063,7 @@
3077
3063
  filter: var(--drag-filter, var(--qti-component-filter));
3078
3064
  }
3079
3065
 
3080
- qti-gap-match-interaction :state(drag)::part(control)::before,
3066
+ qti-gap-match-interaction :is(:state(drag), [data-state~='drag'])::part(control)::before,
3081
3067
  qti-gap-match-interaction::part(drag-control)::before,
3082
3068
  qti-gap-match-interaction ::part(drag-control)::before,
3083
3069
  [data-drag-clone][data-drag-interaction='qti-gap-match-interaction']::part(control)::before {
@@ -3092,21 +3078,21 @@
3092
3078
  -webkit-mask: var(--qti-grip-mask) no-repeat center / contain;
3093
3079
  }
3094
3080
  }
3081
+
3095
3082
  @layer qti-components.interactions.base {
3096
- qti-gap-match-interaction {
3097
3083
  /* No qti-selections-light / -dark here. Appendix B.1 of the QTI 3 implementation guide defines
3098
3084
  that class pair for qti-hotspot-interaction and qti-graphic-gap-match-interaction only, and no
3099
3085
  item in this repo ever set it on a gap-match interaction. What was here overrode the neutral
3100
3086
  drag-bank highlight below with --qti-light-* and --qti-dark-* colours that nothing could reach. */
3101
- &:state(dragzone-active)::part(drags) {
3087
+ qti-gap-match-interaction:is(:state(dragzone-active), [data-state~='dragzone-active'])::part(drags) {
3102
3088
  background-color: var(--qti-bg-active);
3103
3089
  }
3104
- &:state(dragzone-enabled)::part(drags) {
3090
+ qti-gap-match-interaction:is(:state(dragzone-enabled), [data-state~='dragzone-enabled'])::part(drags) {
3105
3091
  background-color: var(--qti-bg-active);
3106
3092
  }
3107
3093
  /* Editor fake-drag projected into a filled <qti-gap>'s shadow. Same `drag`
3108
3094
  styling so the editor preview matches runtime when a gap is filled. */
3109
- & qti-gap::part(drag) {
3095
+ qti-gap-match-interaction qti-gap::part(drag) {
3110
3096
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
3111
3097
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
3112
3098
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -3154,20 +3140,20 @@
3154
3140
  color: var(--drag-color, var(--qti-component-color));
3155
3141
  opacity: var(--drag-opacity, var(--qti-component-opacity));
3156
3142
  filter: var(--drag-filter, var(--qti-component-filter));
3157
- &:hover {
3143
+ }
3158
3144
  @media (hover: hover) {
3145
+ :is(qti-gap-match-interaction qti-gap::part(drag)):hover {
3159
3146
  background-color: color-mix(
3160
3147
  in srgb,
3161
3148
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
3162
3149
  black
3163
3150
  );
3164
- }
3165
3151
  }
3166
- &:focus {
3152
+ }
3153
+ :is(qti-gap-match-interaction qti-gap::part(drag)):focus {
3167
3154
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3168
3155
  }
3169
- }
3170
- & qti-gap-text {
3156
+ qti-gap-match-interaction qti-gap-text {
3171
3157
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
3172
3158
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
3173
3159
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -3215,25 +3201,25 @@
3215
3201
  color: var(--drag-color, var(--qti-component-color));
3216
3202
  opacity: var(--drag-opacity, var(--qti-component-opacity));
3217
3203
  filter: var(--drag-filter, var(--qti-component-filter));
3218
- &[dragging] {
3204
+ }
3205
+ [dragging]:is(qti-gap-match-interaction qti-gap-text) {
3219
3206
  pointer-events: none;
3220
3207
  rotate: -2deg;
3221
3208
  --drag-box-shadow: 0 8px 12px rgb(0 0 0 / 20%), 0 4px 8px rgb(0 0 0 / 10%);
3222
3209
  }
3223
- &:hover {
3224
3210
  @media (hover: hover) {
3211
+ :is(qti-gap-match-interaction qti-gap-text):hover {
3225
3212
  background-color: color-mix(
3226
3213
  in srgb,
3227
3214
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
3228
3215
  black
3229
3216
  );
3230
- }
3231
3217
  }
3232
- &:focus {
3218
+ }
3219
+ :is(qti-gap-match-interaction qti-gap-text):focus {
3233
3220
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3234
3221
  }
3235
- }
3236
- & qti-gap {
3222
+ qti-gap-match-interaction qti-gap {
3237
3223
  background-color: var(--drop-background-color, var(--qti-component-background-color));
3238
3224
  background-image: var(--drop-background-image, var(--qti-component-background-image));
3239
3225
  border-width: var(--drop-border-width, var(--qti-component-border-width));
@@ -3261,22 +3247,23 @@
3261
3247
  * every word on the line the instant a chip landed — the exact reflow qti-gap.styles.ts and
3262
3248
  * drag-drop.invariance.spec.ts exist to prevent.
3263
3249
  */
3264
- &:state(filled):is(:not([match-max]), [match-max='1']) {
3250
+ }
3251
+ :is(qti-gap-match-interaction qti-gap):is(:state(filled), [data-state~='filled']):is(:not([match-max]),[match-max='1']) {
3265
3252
  --drop-border-color: transparent;
3266
3253
  }
3267
3254
 
3268
- &[disabled] {
3255
+ [disabled]:is(qti-gap-match-interaction qti-gap) {
3269
3256
  cursor: not-allowed;
3270
3257
  /* `:not(:empty)` tested light-DOM children; the gap renders its chips into its own shadow
3271
3258
  root now, so it is always "empty" to a selector. `:state(filled)` is the occupancy hook. */
3272
- &:state(filled) {
3259
+ }
3260
+ [disabled]:is(qti-gap-match-interaction qti-gap):is(:state(filled), [data-state~='filled']) {
3273
3261
  cursor: default !important;
3274
3262
  }
3275
- }
3276
- &[enabled] {
3263
+ [enabled]:is(qti-gap-match-interaction qti-gap) {
3277
3264
  background-color: var(--qti-bg-active);
3278
3265
  }
3279
- &[active] {
3266
+ [active]:is(qti-gap-match-interaction qti-gap) {
3280
3267
  background-color: var(--qti-bg-active);
3281
3268
  }
3282
3269
  /* The gap's own box — inline-flex, centred, baseline-aligned — is in qti-gap.styles.ts. */
@@ -3305,17 +3292,15 @@
3305
3292
  */
3306
3293
  /* `flex: 0 0 auto` is dropRegion's, in qti-base — a drop hosts a chip, it never sizes one.
3307
3294
  The rotate reset and the outline are paint. */
3308
- &::part(drag) {
3295
+ :is(qti-gap-match-interaction qti-gap)::part(drag) {
3309
3296
  transform: rotate(0);
3310
3297
  /* Through the drag slot so a correction (which sets --drag-box-shadow) composes over it
3311
3298
  instead of this leaking onto a corrected chip. */
3312
3299
  --drag-box-shadow: 0 0 0 1px #e5e7eb;
3313
3300
  }
3314
- }
3315
- }
3316
3301
 
3317
3302
  /* The source and placed chip share one visual component. */
3318
- qti-gap-match-interaction qti-gap-text:state(drag),
3303
+ qti-gap-match-interaction qti-gap-text:is(:state(drag), [data-state~='drag']),
3319
3304
  qti-gap-match-interaction qti-gap::part(drag) {
3320
3305
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
3321
3306
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
@@ -3366,6 +3351,7 @@
3366
3351
  filter: var(--drag-filter, var(--qti-component-filter));
3367
3352
  }
3368
3353
  }
3354
+
3369
3355
  @layer qti-components.interactions.states {
3370
3356
  /* One chip vocabulary across the bank and placed drags for gap-match only. */
3371
3357
  qti-gap::part(drag) {
@@ -3418,7 +3404,7 @@
3418
3404
  filter: var(--drag-filter, var(--qti-component-filter));
3419
3405
  }
3420
3406
 
3421
- qti-gap-match-interaction qti-gap-text:state(placeholder) {
3407
+ qti-gap-match-interaction qti-gap-text:is(:state(placeholder), [data-state~='placeholder']) {
3422
3408
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
3423
3409
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
3424
3410
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -3429,7 +3415,7 @@
3429
3415
  pointer-events: none;
3430
3416
  }
3431
3417
 
3432
- qti-gap-match-interaction qti-gap-text:state(dragging) {
3418
+ qti-gap-match-interaction qti-gap-text:is(:state(dragging), [data-state~='dragging']) {
3433
3419
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
3434
3420
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
3435
3421
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -3443,21 +3429,22 @@
3443
3429
  pointer-events: none;
3444
3430
  }
3445
3431
 
3446
- qti-gap-match-interaction qti-gap-text:state(return-anchor) {
3432
+ qti-gap-match-interaction qti-gap-text:is(:state(return-anchor), [data-state~='return-anchor']) {
3447
3433
  border-color: var(--qti-drop-highlight-border-color);
3448
3434
  box-shadow: inset 0 0 0 var(--qti-drop-highlight-border-width, 2px)
3449
3435
  color-mix(in srgb, var(--qti-drop-highlight-border-color) 55%, transparent);
3450
3436
  }
3451
3437
 
3452
- qti-gap-match-interaction qti-gap:state(hover),
3453
- qti-gap-match-interaction qti-gap:state(active) {
3438
+ qti-gap-match-interaction qti-gap:is(:state(hover), [data-state~='hover']),
3439
+ qti-gap-match-interaction qti-gap:is(:state(active), [data-state~='active']) {
3454
3440
  background-color: var(--qti-drop-highlight-bg);
3455
3441
  }
3456
3442
 
3457
- qti-gap-match-interaction qti-gap:state(enabled) {
3443
+ qti-gap-match-interaction qti-gap:is(:state(enabled), [data-state~='enabled']) {
3458
3444
  background-color: var(--qti-drop-highlight-enabled-bg);
3459
3445
  }
3460
3446
  }
3447
+
3461
3448
  @layer qti-components.interactions.corrections {
3462
3449
  /* A corrected chip — in the bank, and where it landed. Moved here from the shared
3463
3450
  qti-corrections.css, which claimed `:state(drag)` for EVERY interaction in one unscoped rule:
@@ -3470,16 +3457,16 @@
3470
3457
  DIRECTLY, which outranks the paint binding that reads --drag-background-color, so the solid fill
3471
3458
  they asked for never rendered — a corrected gap-text has always been the pale tint. Left in place
3472
3459
  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) {
3460
+ qti-gap-match-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-correct), [data-state~='candidate-correct']),
3461
+ qti-gap::part(drag):is(:state(candidate-correct), [data-state~='candidate-correct']) {
3475
3462
  --drag-background-color: var(--qti-correct-light);
3476
3463
  --drag-color: var(--qti-correct);
3477
3464
  --drag-border-color: var(--qti-correct);
3478
3465
  --drag-box-shadow: none;
3479
3466
  }
3480
3467
 
3481
- qti-gap-match-interaction :state(drag):state(candidate-incorrect),
3482
- qti-gap::part(drag):state(candidate-incorrect) {
3468
+ qti-gap-match-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
3469
+ qti-gap::part(drag):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
3483
3470
  --drag-background-color: var(--qti-incorrect-light);
3484
3471
  --drag-color: var(--qti-incorrect);
3485
3472
  --drag-border-color: var(--qti-incorrect);
@@ -3489,6 +3476,7 @@
3489
3476
  /* The bank is hidden by the interaction itself, on :host([answer-key]) — see
3490
3477
  qti-gap-match-interaction.styles.ts. */
3491
3478
  }
3479
+
3492
3480
  /*
3493
3481
  * A hotspot is indicated by a RING, not a fill — matching QTI 3's own figures (Impl-Guide Fig
3494
3482
  * 54/55) and its shared-vocab wording ("selection lines and fills", the line being the ring).
@@ -3560,17 +3548,18 @@
3560
3548
  outline-offset: 2px;
3561
3549
  }
3562
3550
  }
3551
+
3563
3552
  @layer qti-components.interactions.states {
3564
3553
  /*
3565
3554
  * Selected: a bold, saturated ring — the Fig-54 weight jump. Same var, so poly and box shapes
3566
3555
  * move together; poly additionally swaps to the bold outline mask.
3567
3556
  */
3568
- qti-hotspot-interaction qti-hotspot-choice:state(checked) {
3557
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(checked), [data-state~='checked']) {
3569
3558
  --hs-ring: var(--qti-hotspot-ring-color, var(--qti-selected-bg));
3570
3559
  --hs-width: calc(var(--qti-hotspot-ring-width, 2px) * 2);
3571
3560
  }
3572
3561
 
3573
- qti-hotspot-interaction qti-hotspot-choice[shape='poly']:state(checked) {
3562
+ qti-hotspot-interaction qti-hotspot-choice[shape='poly']:is(:state(checked), [data-state~='checked']) {
3574
3563
  mask-image: var(--qti-shape-outline-bold);
3575
3564
  -webkit-mask-image: var(--qti-shape-outline-bold);
3576
3565
  }
@@ -3604,35 +3593,36 @@
3604
3593
  * In `states` so it outranks the base ring and the `:hover` strengthen: hover must NOT reveal, or
3605
3594
  * sweeping the pointer hands back the answer regions one at a time.
3606
3595
  */
3607
- qti-hotspot-interaction.qti-unselected-hidden qti-hotspot-choice:not(:state(checked)):not(:focus-visible) {
3596
+ qti-hotspot-interaction.qti-unselected-hidden qti-hotspot-choice:not(:is(:state(checked), [data-state~='checked'])):not(:focus-visible) {
3608
3597
  --hs-ring: transparent;
3609
3598
  --hs-halo: transparent;
3610
3599
  }
3611
3600
 
3612
- qti-hotspot-interaction qti-hotspot-choice:state(disabled),
3613
- qti-hotspot-interaction qti-hotspot-choice:state(readonly) {
3601
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(disabled), [data-state~='disabled']),
3602
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(readonly), [data-state~='readonly']) {
3614
3603
  cursor: not-allowed;
3615
3604
  pointer-events: none;
3616
3605
  }
3617
3606
  }
3607
+
3618
3608
  @layer qti-components.interactions.corrections {
3619
3609
  /*
3620
3610
  * Correction and answer-key views recolour the same ring — one var, so circle and poly stay in
3621
3611
  * step. Answer-key first, so a candidate's own judgement wins when both are present.
3622
3612
  */
3623
- qti-hotspot-interaction qti-hotspot-choice:state(correct-response) {
3613
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(correct-response), [data-state~='correct-response']) {
3624
3614
  --qti-hotspot-ring-color: var(--qti-answer-border);
3625
3615
  }
3626
3616
 
3627
- qti-hotspot-interaction qti-hotspot-choice:state(candidate-correct) {
3617
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(candidate-correct), [data-state~='candidate-correct']) {
3628
3618
  --qti-hotspot-ring-color: var(--qti-correct);
3629
3619
  }
3630
3620
 
3631
- qti-hotspot-interaction qti-hotspot-choice:state(candidate-incorrect) {
3621
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
3632
3622
  --qti-hotspot-ring-color: var(--qti-incorrect);
3633
3623
  }
3634
3624
 
3635
- .full-correct-response qti-hotspot-interaction qti-hotspot-choice:state(checked) {
3625
+ .full-correct-response qti-hotspot-interaction qti-hotspot-choice:is(:state(checked), [data-state~='checked']) {
3636
3626
  --qti-hotspot-ring-color: var(--qti-answer-border);
3637
3627
  }
3638
3628
 
@@ -3642,22 +3632,23 @@
3642
3632
  * candidate-* hotspot is usually also :state(checked) and would be bold anyway; correct-response
3643
3633
  * on an unselected region is the case this covers.)
3644
3634
  */
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) {
3635
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(correct-response), [data-state~='correct-response']),
3636
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(candidate-correct), [data-state~='candidate-correct']),
3637
+ qti-hotspot-interaction qti-hotspot-choice:is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
3638
+ .full-correct-response qti-hotspot-interaction qti-hotspot-choice:is(:state(checked), [data-state~='checked']) {
3649
3639
  --hs-ring: var(--qti-hotspot-ring-color);
3650
3640
  --hs-width: calc(var(--qti-hotspot-ring-width, 2px) * 2);
3651
3641
  }
3652
3642
 
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) {
3643
+ qti-hotspot-interaction qti-hotspot-choice[shape='poly']:is(:state(correct-response), [data-state~='correct-response']),
3644
+ qti-hotspot-interaction qti-hotspot-choice[shape='poly']:is(:state(candidate-correct), [data-state~='candidate-correct']),
3645
+ qti-hotspot-interaction qti-hotspot-choice[shape='poly']:is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
3646
+ .full-correct-response qti-hotspot-interaction qti-hotspot-choice[shape='poly']:is(:state(checked), [data-state~='checked']) {
3657
3647
  mask-image: var(--qti-shape-outline-bold);
3658
3648
  -webkit-mask-image: var(--qti-shape-outline-bold);
3659
3649
  }
3660
3650
  }
3651
+
3661
3652
  @layer qti-components.interactions.base {
3662
3653
  qti-hottext-interaction {
3663
3654
  --qti-hottext-selected-padding: var(--qti-padding-inline);
@@ -3678,118 +3669,91 @@
3678
3669
  * target's active border: pay the space once, up front, so the state has only to repaint. Every
3679
3670
  * hottext is now the size a selected one used to be, which is also a larger target to click.
3680
3671
  */
3681
- qti-hottext {
3672
+ }
3673
+ qti-hottext-interaction qti-hottext {
3682
3674
  padding: var(--qti-hottext-selected-padding);
3683
3675
  }
3684
-
3685
- &.qti-input-control-hidden {
3686
- qti-hottext {
3676
+ qti-hottext-interaction.qti-input-control-hidden qti-hottext {
3687
3677
  border: var(--qti-border);
3688
3678
  border-radius: var(--qti-border-radius);
3689
3679
 
3690
3680
  background: var(--qti-bg);
3681
+ }
3691
3682
 
3692
3683
 
3693
- &:focus {
3684
+ :is(qti-hottext-interaction.qti-input-control-hidden qti-hottext):focus {
3694
3685
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3695
3686
  }
3696
- }
3697
- }
3698
-
3699
- &.qti-unselected-hidden {
3700
- qti-hottext {
3687
+ qti-hottext-interaction.qti-unselected-hidden qti-hottext {
3701
3688
  border: 0;
3702
3689
  border-radius: var(--qti-border-radius);
3703
3690
  background-color: transparent;
3704
3691
  outline: none;
3705
3692
  cursor: pointer;
3693
+ }
3706
3694
 
3707
3695
 
3708
- &:focus {
3696
+ :is(qti-hottext-interaction.qti-unselected-hidden qti-hottext):focus {
3709
3697
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3710
3698
  }
3711
- }
3712
- }
3713
-
3714
- &:not(.qti-input-control-hidden, .qti-unselected-hidden) {
3715
- qti-hottext {
3699
+ qti-hottext-interaction:not(.qti-input-control-hidden,.qti-unselected-hidden) qti-hottext {
3716
3700
  border: var(--qti-border);
3717
3701
  border-radius: var(--qti-border-radius);
3702
+ }
3718
3703
 
3719
3704
 
3720
- &:focus {
3705
+ :is(qti-hottext-interaction:not(.qti-input-control-hidden,.qti-unselected-hidden) qti-hottext):focus {
3721
3706
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3722
3707
  }
3723
- }
3724
- }
3725
- }
3726
3708
  }
3709
+
3727
3710
  @layer qti-components.interactions.states {
3728
- qti-hottext-interaction {
3729
- &.qti-input-control-hidden qti-hottext {
3730
- &:state(checked) {
3711
+ :is(qti-hottext-interaction.qti-input-control-hidden qti-hottext):is(:state(checked), [data-state~='checked']) {
3731
3712
  border-color: var(--qti-border-active);
3732
3713
  background-color: var(--qti-selected-bg);
3733
3714
  color: var(--qti-selected-color);
3734
3715
  }
3735
3716
 
3736
- &:state(readonly) {
3717
+ :is(qti-hottext-interaction.qti-input-control-hidden qti-hottext):is(:state(readonly), [data-state~='readonly']) {
3737
3718
  cursor: pointer;
3738
3719
  }
3739
3720
 
3740
- &:state(disabled) {
3721
+ :is(qti-hottext-interaction.qti-input-control-hidden qti-hottext):is(:state(disabled), [data-state~='disabled']) {
3741
3722
  cursor: not-allowed;
3742
3723
  }
3743
- }
3744
-
3745
- &.qti-unselected-hidden qti-hottext,
3746
- &:not(.qti-input-control-hidden, .qti-unselected-hidden) qti-hottext {
3747
- &:state(checked) {
3724
+ :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
3725
  background-color: var(--qti-selected-bg);
3749
3726
  color: var(--qti-selected-color);
3750
3727
  }
3751
3728
 
3752
- &:state(readonly) {
3729
+ :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
3730
  cursor: pointer;
3754
3731
  }
3755
3732
 
3756
- &:state(disabled) {
3733
+ :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
3734
  cursor: not-allowed;
3758
3735
  }
3759
- }
3760
- }
3761
3736
  }
3737
+
3762
3738
  @layer qti-components.interactions.corrections {
3763
- qti-hottext-interaction {
3764
- qti-hottext {
3765
- &:state(correct-response),
3766
- &[data-correct-response='true'] {
3767
- &::after {
3739
+ :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
3740
  content: '\02714';
3769
3741
  color: var(--qti-correct);
3770
3742
  }
3771
- }
3772
- }
3773
-
3774
- &.qti-input-control-hidden qti-hottext {
3775
- &:state(candidate-correct) {
3743
+ :is(qti-hottext-interaction.qti-input-control-hidden qti-hottext):is(:state(candidate-correct), [data-state~='candidate-correct']) {
3776
3744
  background-color: var(--qti-correct-light);
3777
3745
  color: var(--qti-correct);
3778
3746
  border-color: var(--qti-correct);
3779
3747
  box-shadow: none;
3780
3748
  }
3781
3749
 
3782
- &:state(candidate-incorrect) {
3750
+ :is(qti-hottext-interaction.qti-input-control-hidden qti-hottext):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
3783
3751
  background-color: var(--qti-incorrect-light);
3784
3752
  color: var(--qti-incorrect);
3785
3753
  border-color: var(--qti-incorrect);
3786
3754
  box-shadow: none;
3787
3755
  }
3788
- }
3789
-
3790
- &.qti-unselected-hidden qti-hottext,
3791
- &:not(.qti-input-control-hidden, .qti-unselected-hidden) qti-hottext {
3792
- &:state(candidate-correct) {
3756
+ :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
3757
  background-color: var(--qti-correct-light);
3794
3758
  color: var(--qti-correct);
3795
3759
  border-color: var(--qti-correct);
@@ -3797,21 +3761,19 @@
3797
3761
  box-shadow: inset 0 0 0 var(--qti-border-thickness) var(--qti-correct);
3798
3762
  }
3799
3763
 
3800
- &:state(candidate-incorrect) {
3764
+ :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
3765
  background-color: var(--qti-incorrect-light);
3802
3766
  color: var(--qti-incorrect);
3803
3767
  border-color: var(--qti-incorrect);
3804
3768
  box-shadow: none;
3805
3769
  box-shadow: inset 0 0 0 var(--qti-border-thickness) var(--qti-incorrect);
3806
3770
  }
3807
- }
3808
- }
3809
3771
 
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),
3772
+ .full-correct-response qti-hottext-interaction.qti-input-control-hidden qti-hottext:is(:state(checked), [data-state~='checked']),
3773
+ .full-correct-response qti-hottext-interaction.qti-unselected-hidden qti-hottext:is(:state(checked), [data-state~='checked']),
3812
3774
  .full-correct-response
3813
3775
  qti-hottext-interaction:not(.qti-input-control-hidden, .qti-unselected-hidden)
3814
- qti-hottext:state(checked) {
3776
+ qti-hottext:is(:state(checked), [data-state~='checked']) {
3815
3777
  background-color: var(--qti-answer-bg);
3816
3778
  color: var(--qti-answer-fg);
3817
3779
  border-color: var(--qti-answer-border);
@@ -3822,7 +3784,8 @@
3822
3784
  padding-inline-end: var(--qti-gap);
3823
3785
 
3824
3786
  /* The icon must not become the inline-flex container's first baseline item. */
3825
- &::before {
3787
+ }
3788
+ :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
3789
  content: '';
3827
3790
  background-color: var(--qti-answer-fg);
3828
3791
  mask: var(--qti-check-mask) no-repeat center / contain;
@@ -3836,8 +3799,8 @@
3836
3799
  width: 1em;
3837
3800
  height: 1em;
3838
3801
  }
3839
- }
3840
3802
  }
3803
+
3841
3804
  @layer qti-components.interactions.base {
3842
3805
  qti-inline-choice-interaction {
3843
3806
  /* ── Host: the closed combobox, painted through the paint contract so a brand can restyle the
@@ -3858,7 +3821,8 @@
3858
3821
  --field-color: var(--qti-fg);
3859
3822
 
3860
3823
  /* ── Trigger: transparent inside the field border ───────────────────────── */
3861
- &::part(trigger) {
3824
+ }
3825
+ qti-inline-choice-interaction::part(trigger) {
3862
3826
  padding: var(--qti-padding-box);
3863
3827
  background: transparent;
3864
3828
  border: none;
@@ -3868,27 +3832,25 @@
3868
3832
  font-size: inherit;
3869
3833
  }
3870
3834
 
3871
- &::part(trigger):focus-visible {
3835
+ qti-inline-choice-interaction::part(trigger):focus-visible {
3872
3836
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3873
3837
  outline: none;
3874
3838
  }
3875
3839
 
3876
- &:state(disabled)::part(trigger),
3877
- &[disabled]::part(trigger) {
3840
+ qti-inline-choice-interaction:is(:state(disabled), [data-state~='disabled'])::part(trigger),qti-inline-choice-interaction[disabled]::part(trigger) {
3878
3841
  cursor: not-allowed;
3879
3842
  }
3880
3843
 
3881
- &:state(readonly)::part(trigger),
3882
- &[readonly]::part(trigger) {
3844
+ qti-inline-choice-interaction:is(:state(readonly), [data-state~='readonly'])::part(trigger),qti-inline-choice-interaction[readonly]::part(trigger) {
3883
3845
  cursor: pointer;
3884
3846
  }
3885
3847
 
3886
3848
  /* ── Chevron icon: color only; transform/mask/size live in shadow DOM ────── */
3887
- &::part(dropdown-icon) {
3849
+ qti-inline-choice-interaction::part(dropdown-icon) {
3888
3850
  color: inherit;
3889
3851
  }
3890
3852
 
3891
- &::part(dropdown-icon-open) {
3853
+ qti-inline-choice-interaction::part(dropdown-icon-open) {
3892
3854
  color: inherit;
3893
3855
  }
3894
3856
 
@@ -3906,7 +3868,7 @@
3906
3868
  * above it. Two spacing tokens for one box is the whole bug — the vocabulary says pick one and
3907
3869
  * spend it whole (see the CONTRACT note on --qti-padding-box / -inline / --qti-gap).
3908
3870
  */
3909
- &::part(menu) {
3871
+ qti-inline-choice-interaction::part(menu) {
3910
3872
  background-color: var(--menu-background-color, var(--qti-component-background-color));
3911
3873
  background-image: var(--menu-background-image, var(--qti-component-background-image));
3912
3874
  border-width: var(--menu-border-width, var(--qti-component-border-width));
@@ -3940,18 +3902,18 @@
3940
3902
  * position-try fallback applied", so fixing that needs a signal from script, and it is not worth
3941
3903
  * one for the rarer case.
3942
3904
  */
3943
- &:state(open) {
3905
+ qti-inline-choice-interaction:is(:state(open), [data-state~='open']) {
3944
3906
  border-end-start-radius: 0;
3945
3907
  border-end-end-radius: 0;
3946
3908
  }
3947
3909
 
3948
- &::part(menu):popover-open {
3910
+ qti-inline-choice-interaction::part(menu):popover-open {
3949
3911
  border-start-start-radius: 0;
3950
3912
  border-start-end-radius: 0;
3951
3913
  }
3952
3914
 
3953
3915
  /* ── Option rows ─────────────────────────────────────────────────────────── */
3954
- &::part(option) {
3916
+ qti-inline-choice-interaction::part(option) {
3955
3917
  padding: var(--qti-padding-box);
3956
3918
  border: 0;
3957
3919
  /* Square, because a row now spans the full width of the menu. The menu's own radius still
@@ -3962,19 +3924,19 @@
3962
3924
  cursor: pointer;
3963
3925
  }
3964
3926
 
3965
- &::part(option):focus-visible {
3927
+ qti-inline-choice-interaction::part(option):focus-visible {
3966
3928
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3967
3929
  outline: none;
3968
3930
  }
3969
3931
 
3970
- &::part(option-selected) {
3932
+ qti-inline-choice-interaction::part(option-selected) {
3971
3933
  background-color: var(--qti-bg-active);
3972
3934
  }
3973
3935
 
3974
3936
  /* ── Slotted qti-inline-choice elements (light DOM) ──────────────────────── */
3975
3937
  /* The option row's own box — flex, centred, single-line, clipped — is in
3976
3938
  qti-inline-choice.styles.ts. */
3977
- & qti-inline-choice {
3939
+ qti-inline-choice-interaction qti-inline-choice {
3978
3940
  width: 100%;
3979
3941
  box-sizing: border-box;
3980
3942
  padding: var(--qti-padding-box);
@@ -3990,19 +3952,19 @@
3990
3952
  text-overflow: ellipsis;
3991
3953
  }
3992
3954
 
3993
- & qti-inline-choice:focus-visible {
3955
+ qti-inline-choice-interaction qti-inline-choice:focus-visible {
3994
3956
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
3995
3957
  outline: none;
3996
3958
  }
3997
3959
 
3998
- & qti-inline-choice:state(checked) {
3960
+ qti-inline-choice-interaction qti-inline-choice:is(:state(checked), [data-state~='checked']) {
3999
3961
  background-color: var(--qti-bg-active);
4000
3962
  }
4001
3963
 
4002
3964
  /* ── Validation message: a warning box, painted through the contract. Its non-neutral bits
4003
3965
  (fg text, amber-tinted fill, amber border) are --message-* overrides; width/style/radius
4004
3966
  come from the defaults. ── */
4005
- &::part(message) {
3967
+ qti-inline-choice-interaction::part(message) {
4006
3968
  background-color: var(--message-background-color, var(--qti-component-background-color));
4007
3969
  background-image: var(--message-background-image, var(--qti-component-background-image));
4008
3970
  border-width: var(--message-border-width, var(--qti-component-border-width));
@@ -4018,8 +3980,8 @@
4018
3980
  --message-border-color: var(--qti-warning);
4019
3981
  padding: var(--qti-padding-box);
4020
3982
  }
4021
- }
4022
3983
  }
3984
+
4023
3985
  @layer qti-components.interactions.corrections {
4024
3986
  /* The answer key, moved here from the shared qti-corrections.css: it names this interaction, so
4025
3987
  this file owns it. */
@@ -4027,36 +3989,34 @@
4027
3989
  background-color: var(--qti-answer-bg);
4028
3990
  color: var(--qti-answer-fg);
4029
3991
  border-color: var(--qti-answer-border);
3992
+ }
4030
3993
 
4031
- &::part(value) {
3994
+ :is(.full-correct-response qti-inline-choice-interaction)::part(value) {
4032
3995
  color: var(--qti-answer-fg);
4033
3996
  }
4034
- }
4035
-
4036
- qti-inline-choice-interaction {
4037
- &:state(candidate-correct) {
3997
+ qti-inline-choice-interaction:is(:state(candidate-correct), [data-state~='candidate-correct']) {
4038
3998
  --field-background-color: var(--qti-correct-light);
4039
3999
  --field-color: var(--qti-correct);
4040
4000
  --field-border-color: var(--qti-correct);
4041
4001
  --field-box-shadow: none;
4002
+ }
4042
4003
 
4043
- &::part(trigger) {
4004
+ qti-inline-choice-interaction:is(:state(candidate-correct), [data-state~='candidate-correct'])::part(trigger) {
4044
4005
  background: transparent;
4045
4006
  }
4046
- }
4047
4007
 
4048
- &:state(candidate-incorrect) {
4008
+ qti-inline-choice-interaction:is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
4049
4009
  --field-background-color: var(--qti-incorrect-light);
4050
4010
  --field-color: var(--qti-incorrect);
4051
4011
  --field-border-color: var(--qti-incorrect);
4052
4012
  --field-box-shadow: none;
4013
+ }
4053
4014
 
4054
- &::part(trigger) {
4015
+ qti-inline-choice-interaction:is(:state(candidate-incorrect), [data-state~='candidate-incorrect'])::part(trigger) {
4055
4016
  background: transparent;
4056
4017
  }
4057
- }
4058
- }
4059
4018
  }
4019
+
4060
4020
  /* This interaction's chip vocabulary — the bank chip, the placed chip, and the floating clone. One
4061
4021
  such block per drag-drop interaction, opt-in; graphic-gap-match deliberately declares none. See
4062
4022
  qti-states.css. Kept in the `global` layer, where these rules used to live, so cascade position is
@@ -4073,7 +4033,7 @@
4073
4033
  sat on document.body it was in a different tree from the adopted stylesheet, and nothing — no
4074
4034
  part, no attribute — could style it. */
4075
4035
  @layer qti-components.global {
4076
- qti-match-interaction :state(drag),
4036
+ qti-match-interaction :is(:state(drag), [data-state~='drag']),
4077
4037
  [data-drag-clone][data-drag-interaction='qti-match-interaction'] {
4078
4038
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
4079
4039
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
@@ -4124,7 +4084,7 @@
4124
4084
  filter: var(--drag-filter, var(--qti-component-filter));
4125
4085
  }
4126
4086
 
4127
- qti-match-interaction :state(drag)::part(control)::before,
4087
+ qti-match-interaction :is(:state(drag), [data-state~='drag'])::part(control)::before,
4128
4088
  qti-match-interaction::part(drag-control)::before,
4129
4089
  qti-match-interaction ::part(drag-control)::before,
4130
4090
  [data-drag-clone][data-drag-interaction='qti-match-interaction']::part(control)::before {
@@ -4139,6 +4099,7 @@
4139
4099
  -webkit-mask: var(--qti-grip-mask) no-repeat center / contain;
4140
4100
  }
4141
4101
  }
4102
+
4142
4103
  @layer qti-components.interactions.base {
4143
4104
  /* Editor fake-drag projected into the target choice's `drop` shadow.
4144
4105
  Reuses the same `drag` styling so the editor preview matches what students
@@ -4191,19 +4152,19 @@
4191
4152
  color: var(--drag-color, var(--qti-component-color));
4192
4153
  opacity: var(--drag-opacity, var(--qti-component-opacity));
4193
4154
  filter: var(--drag-filter, var(--qti-component-filter));
4194
- &:hover {
4155
+ }
4195
4156
  @media (hover: hover) {
4157
+ :is(qti-simple-associable-choice::part(drag)):hover {
4196
4158
  background-color: color-mix(
4197
4159
  in srgb,
4198
4160
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
4199
4161
  black
4200
4162
  );
4201
- }
4202
4163
  }
4203
- &:focus {
4164
+ }
4165
+ :is(qti-simple-associable-choice::part(drag)):focus {
4204
4166
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
4205
4167
  }
4206
- }
4207
4168
 
4208
4169
  qti-match-interaction:not(.qti-match-tabular) {
4209
4170
  /*
@@ -4225,9 +4186,11 @@
4225
4186
 
4226
4187
  /* The bank and the target grid are laid out in qti-match-interaction.styles.ts. Only the gaps
4227
4188
  are still set here — gap is spacing. */
4228
- & qti-simple-match-set:first-of-type {
4189
+ }
4190
+ qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:first-of-type {
4229
4191
  gap: var(--qti-gap);
4230
- & qti-simple-associable-choice {
4192
+ }
4193
+ :is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:first-of-type) qti-simple-associable-choice {
4231
4194
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
4232
4195
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
4233
4196
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -4275,44 +4238,41 @@
4275
4238
  color: var(--drag-color, var(--qti-component-color));
4276
4239
  opacity: var(--drag-opacity, var(--qti-component-opacity));
4277
4240
  filter: var(--drag-filter, var(--qti-component-filter));
4278
- &[dragging] {
4241
+ }
4242
+ [dragging]:is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:first-of-type) qti-simple-associable-choice) {
4279
4243
  pointer-events: none;
4280
4244
  rotate: -2deg;
4281
4245
  --drag-box-shadow: 0 8px 12px rgb(0 0 0 / 20%), 0 4px 8px rgb(0 0 0 / 10%);
4282
4246
  }
4283
- &:hover {
4284
4247
  @media (hover: hover) {
4248
+ :is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:first-of-type) qti-simple-associable-choice):hover {
4285
4249
  background-color: color-mix(
4286
4250
  in srgb,
4287
4251
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
4288
4252
  black
4289
4253
  );
4290
- }
4291
4254
  }
4292
- &:focus {
4255
+ }
4256
+ :is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:first-of-type) qti-simple-associable-choice):focus {
4293
4257
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
4294
4258
  }
4295
- }
4296
- }
4297
- & qti-simple-match-set:last-of-type {
4259
+ qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type {
4298
4260
  gap: var(--qti-gap);
4299
- & > qti-simple-associable-choice {
4300
- & img {
4261
+ }
4262
+ :is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type) > qti-simple-associable-choice) img {
4301
4263
  max-width: 100%;
4302
4264
  height: auto;
4303
4265
  }
4304
- &[disabled] {
4305
- &::part(drop) {
4266
+ [disabled]:is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type) > qti-simple-associable-choice)::part(drop) {
4306
4267
  cursor: not-allowed;
4307
4268
  }
4308
- }
4309
4269
  /*
4310
4270
  * No `[active]` highlight. Lifting a chip used to draw a solid `--qti-border-active` border
4311
4271
  * around every drop container at once, which says "any of these" — the least useful thing to
4312
4272
  * say. What the candidate wants to see is the hole the chip left, and that is painted on the
4313
4273
  * chip's own `:state(dragging)` / `:state(placeholder)`.
4314
4274
  */
4315
- &::part(drop) {
4275
+ :is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type) > qti-simple-associable-choice)::part(drop) {
4316
4276
  background-color: var(--drop-background-color, var(--qti-component-background-color));
4317
4277
  background-image: var(--drop-background-image, var(--qti-component-background-image));
4318
4278
  border-width: var(--drop-border-width, var(--qti-component-border-width));
@@ -4330,20 +4290,22 @@
4330
4290
  used to LEAK: `dragging` sets --component-box-shadow, one shared namespace, so it
4331
4291
  inherited onto every chip nested in the drop. That half is now structural — `dragging`
4332
4292
  sets --drag-box-shadow, which a drop does not read.) */
4333
- &:focus {
4293
+ }
4294
+ :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
4295
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
4335
4296
  }
4297
+ :is(:is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type) > qti-simple-associable-choice)::part(drop) {
4336
4298
  margin-top: var(--qti-gap);
4337
4299
  gap: var(--qti-gap);
4338
4300
  }
4339
- & > *:not(qti-simple-associable-choice) {
4301
+ :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
4302
  pointer-events: none;
4341
4303
  }
4342
4304
  /* A chip nested in a drop target renders no drop region of its own. That is the component's
4343
4305
  own rule — qti-simple-associable-choice.styles.ts hides [part~='drop'] for both
4344
4306
  :host(:state(drag)) and :host([part~='drag']), which covers the placed clone this
4345
4307
  selector could not reach. */
4346
- & > qti-simple-associable-choice {
4308
+ :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
4309
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
4348
4310
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
4349
4311
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -4391,26 +4353,23 @@
4391
4353
  color: var(--drag-color, var(--qti-component-color));
4392
4354
  opacity: var(--drag-opacity, var(--qti-component-opacity));
4393
4355
  filter: var(--drag-filter, var(--qti-component-filter));
4394
- &:hover {
4356
+ }
4395
4357
  @media (hover: hover) {
4358
+ :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
4359
  background-color: color-mix(
4397
4360
  in srgb,
4398
4361
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
4399
4362
  black
4400
4363
  );
4401
- }
4402
4364
  }
4403
- &:focus {
4365
+ }
4366
+ :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
4367
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
4405
4368
  }
4406
- }
4407
- }
4408
- }
4409
- }
4410
4369
 
4411
4370
  qti-match-interaction:not(.qti-match-tabular)
4412
4371
  qti-simple-match-set:first-of-type
4413
- qti-simple-associable-choice:state(drag),
4372
+ qti-simple-associable-choice:is(:state(drag), [data-state~='drag']),
4414
4373
  qti-match-interaction:not(.qti-match-tabular) qti-simple-associable-choice::part(drag) {
4415
4374
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
4416
4375
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
@@ -4460,35 +4419,32 @@
4460
4419
  opacity: var(--drag-opacity, var(--qti-component-opacity));
4461
4420
  filter: var(--drag-filter, var(--qti-component-filter));
4462
4421
  }
4463
-
4464
- qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type > qti-simple-associable-choice {
4465
4422
  /* The label's `align-content` and the drop's `flex-grow` are the target card's own box, and live
4466
4423
  in qti-simple-associable-choice.styles.ts under :host(:state(droppable)). `box-sizing` went with
4467
4424
  them: border-box is already the reset, and content-box is what dropRegion sets. */
4468
- &::part(label) {
4425
+ :is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type > qti-simple-associable-choice)::part(label) {
4469
4426
  border: var(--qti-border);
4470
4427
  border-top-left-radius: var(--qti-border-radius);
4471
4428
  border-top-right-radius: var(--qti-border-radius);
4472
4429
  padding: 0.5rem;
4473
4430
  }
4474
4431
 
4475
- &::part(drop) {
4432
+ :is(qti-match-interaction:not(.qti-match-tabular) qti-simple-match-set:last-of-type > qti-simple-associable-choice)::part(drop) {
4476
4433
  margin-top: 0;
4477
4434
  border-top: none;
4478
4435
  border-top-left-radius: 0;
4479
4436
  border-top-right-radius: 0;
4480
4437
  background: none;
4481
4438
  }
4482
-
4483
- }
4484
4439
  }
4440
+
4485
4441
  @layer qti-components.interactions.states {
4486
4442
  qti-match-interaction:not(.qti-match-tabular)
4487
4443
  qti-simple-match-set:first-of-type
4488
- qti-simple-associable-choice:state(dragging),
4444
+ qti-simple-associable-choice:is(:state(dragging), [data-state~='dragging']),
4489
4445
  qti-match-interaction:not(.qti-match-tabular)
4490
4446
  qti-simple-match-set:first-of-type
4491
- qti-simple-associable-choice:state(placeholder) {
4447
+ qti-simple-associable-choice:is(:state(placeholder), [data-state~='placeholder']) {
4492
4448
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
4493
4449
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
4494
4450
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -4501,42 +4457,42 @@
4501
4457
 
4502
4458
  qti-match-interaction:not(.qti-match-tabular)
4503
4459
  qti-simple-match-set:first-of-type
4504
- qti-simple-associable-choice:state(dragging)
4460
+ qti-simple-associable-choice:is(:state(dragging), [data-state~='dragging'])
4505
4461
  > *,
4506
4462
  qti-match-interaction:not(.qti-match-tabular)
4507
4463
  qti-simple-match-set:first-of-type
4508
- qti-simple-associable-choice:state(placeholder)
4464
+ qti-simple-associable-choice:is(:state(placeholder), [data-state~='placeholder'])
4509
4465
  > * {
4510
4466
  visibility: hidden;
4511
4467
  }
4512
4468
 
4513
4469
  qti-match-interaction:not(.qti-match-tabular)
4514
4470
  qti-simple-match-set:first-of-type
4515
- qti-simple-associable-choice:state(dragging)::part(label),
4471
+ qti-simple-associable-choice:is(:state(dragging), [data-state~='dragging'])::part(label),
4516
4472
  qti-match-interaction:not(.qti-match-tabular)
4517
4473
  qti-simple-match-set:first-of-type
4518
- qti-simple-associable-choice:state(placeholder)::part(label) {
4474
+ qti-simple-associable-choice:is(:state(placeholder), [data-state~='placeholder'])::part(label) {
4519
4475
  visibility: hidden;
4520
4476
  }
4521
4477
 
4522
4478
  qti-match-interaction:not(.qti-match-tabular)
4523
4479
  qti-simple-match-set:last-of-type
4524
- > qti-simple-associable-choice:state(hover)::part(drop),
4480
+ > qti-simple-associable-choice:is(:state(hover), [data-state~='hover'])::part(drop),
4525
4481
  qti-match-interaction:not(.qti-match-tabular)
4526
4482
  qti-simple-match-set:last-of-type
4527
- > qti-simple-associable-choice:state(active)::part(drop) {
4483
+ > qti-simple-associable-choice:is(:state(active), [data-state~='active'])::part(drop) {
4528
4484
  background-color: var(--qti-drop-highlight-bg);
4529
4485
  }
4530
4486
 
4531
4487
  qti-match-interaction:not(.qti-match-tabular)
4532
4488
  qti-simple-match-set:last-of-type
4533
- > qti-simple-associable-choice:state(enabled)::part(drop) {
4489
+ > qti-simple-associable-choice:is(:state(enabled), [data-state~='enabled'])::part(drop) {
4534
4490
  background-color: var(--qti-drop-highlight-enabled-bg);
4535
4491
  }
4536
4492
 
4537
4493
  qti-match-interaction:not(.qti-match-tabular)
4538
4494
  qti-simple-match-set:first-of-type
4539
- qti-simple-associable-choice:state(dragging) {
4495
+ qti-simple-associable-choice:is(:state(dragging), [data-state~='dragging']) {
4540
4496
  border-color: var(--qti-drop-highlight-border-color);
4541
4497
  box-shadow: inset 0 0 0 var(--qti-drop-highlight-border-width, 2px)
4542
4498
  color-mix(in srgb, var(--qti-drop-highlight-border-color) 55%, transparent);
@@ -4544,12 +4500,13 @@
4544
4500
 
4545
4501
  qti-match-interaction:not(.qti-match-tabular)
4546
4502
  qti-simple-match-set:first-of-type
4547
- qti-simple-associable-choice:state(return-anchor) {
4503
+ qti-simple-associable-choice:is(:state(return-anchor), [data-state~='return-anchor']) {
4548
4504
  border-color: var(--qti-drop-highlight-border-color);
4549
4505
  box-shadow: inset 0 0 0 var(--qti-drop-highlight-border-width, 2px)
4550
4506
  color-mix(in srgb, var(--qti-drop-highlight-border-color) 55%, transparent);
4551
4507
  }
4552
4508
  }
4509
+
4553
4510
  @layer qti-components.interactions.corrections {
4554
4511
  /* A corrected chip — in the bank, and where it landed. Moved here from the shared
4555
4512
  qti-corrections.css, which claimed `:state(drag)` for EVERY interaction in one unscoped rule:
@@ -4561,16 +4518,16 @@
4561
4518
  same reason as gap-match's: the shared rule's DIRECT `background-color` outranked the binding
4562
4519
  that reads them, so their solid fill never rendered. They out-specify this block, so leaving them
4563
4520
  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) {
4521
+ qti-match-interaction:not(.qti-match-tabular) :is(:state(drag), [data-state~='drag']):is(:state(candidate-correct), [data-state~='candidate-correct']),
4522
+ qti-match-interaction:not(.qti-match-tabular) qti-simple-associable-choice::part(drag):is(:state(candidate-correct), [data-state~='candidate-correct']) {
4566
4523
  --drag-background-color: var(--qti-correct-light);
4567
4524
  --drag-color: var(--qti-correct);
4568
4525
  --drag-border-color: var(--qti-correct);
4569
4526
  --drag-box-shadow: none;
4570
4527
  }
4571
4528
 
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) {
4529
+ qti-match-interaction:not(.qti-match-tabular) :is(:state(drag), [data-state~='drag']):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
4530
+ qti-match-interaction:not(.qti-match-tabular) qti-simple-associable-choice::part(drag):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
4574
4531
  --drag-background-color: var(--qti-incorrect-light);
4575
4532
  --drag-color: var(--qti-incorrect);
4576
4533
  --drag-border-color: var(--qti-incorrect);
@@ -4580,19 +4537,19 @@
4580
4537
  /* The bank is hidden by the interaction itself, on :host([answer-key]) — see
4581
4538
  qti-match-interaction.styles.ts. */
4582
4539
  }
4540
+
4583
4541
  @layer qti-components.interactions.base {
4584
- qti-match-interaction.qti-match-tabular {
4585
- &::part(corner) {
4542
+ qti-match-interaction.qti-match-tabular::part(corner) {
4586
4543
  border: var(--qti-border-thickness) var(--qti-border-style) var(--qti-border-color);
4587
4544
  background-color: var(--qti-bg-active);
4588
4545
  }
4589
4546
 
4590
- &::part(input-cell) {
4547
+ qti-match-interaction.qti-match-tabular::part(input-cell) {
4591
4548
  border: var(--qti-border-thickness) var(--qti-border-style) var(--qti-border-color);
4592
4549
  padding: var(--qti-padding-box);
4593
4550
  transition: background-color 0.1s ease;
4594
4551
  }
4595
- &::part(input-cell):hover {
4552
+ qti-match-interaction.qti-match-tabular::part(input-cell):hover {
4596
4553
  background-color: var(--qti-bg-active);
4597
4554
  }
4598
4555
 
@@ -4603,7 +4560,7 @@
4603
4560
  ElementInternals and cannot carry `:state()` — and `::part(x):state(y)` needs the state on
4604
4561
  the part's own element. They therefore carry variant *part tokens*, which deliberately
4605
4562
  reuse the state vocabulary: radio | checkbox, checked, correct | incorrect. */
4606
- &::part(control radio) {
4563
+ qti-match-interaction.qti-match-tabular::part(control radio) {
4607
4564
  background-color: var(--radio-background-color, var(--qti-component-background-color));
4608
4565
  background-image: var(--radio-background-image, var(--qti-component-background-image));
4609
4566
  border-width: var(--radio-border-width, var(--qti-component-border-width));
@@ -4620,7 +4577,7 @@
4620
4577
  edge, so a brand can square a radio off without having to out-specify this rule. */
4621
4578
  border-radius: var(--radio-border-radius, 100%);
4622
4579
  }
4623
- &::part(control checkbox) {
4580
+ qti-match-interaction.qti-match-tabular::part(control checkbox) {
4624
4581
  background-color: var(--checkbox-background-color, var(--qti-component-background-color));
4625
4582
  background-image: var(--checkbox-background-image, var(--qti-component-background-image));
4626
4583
  border-width: var(--checkbox-border-width, var(--qti-component-border-width));
@@ -4634,7 +4591,7 @@
4634
4591
  width: var(--qti-control-size);
4635
4592
  height: var(--qti-control-size);
4636
4593
  }
4637
- &::part(control-mark) {
4594
+ qti-match-interaction.qti-match-tabular::part(control-mark) {
4638
4595
  width: calc(var(--qti-control-size) * 0.625);
4639
4596
  height: calc(var(--qti-control-size) * 0.625);
4640
4597
  }
@@ -4642,19 +4599,18 @@
4642
4599
  outer box and the inner indicator independently. Scope the base inner-fill rules to
4643
4600
  `control-mark` so the classic dot-in-ring look stays intact; themes wanting the inverted
4644
4601
  look (outer filled, e.g. Kennisnet) add their own `::part(control checked)` rules. */
4645
- &::part(control-mark radio checked) {
4602
+ qti-match-interaction.qti-match-tabular::part(control-mark radio checked) {
4646
4603
  /* 0.5 reproduces today's 8px at the default 1rem — see mark-size for why this is a fraction. */
4647
4604
  width: calc(var(--qti-control-size) * 0.5);
4648
4605
  height: calc(var(--qti-control-size) * 0.5);
4649
4606
  background-color: var(--radio-mark-color, white);
4650
4607
  border-radius: 100%;
4651
4608
  }
4652
- &::part(control-mark checkbox checked) {
4609
+ qti-match-interaction.qti-match-tabular::part(control-mark checkbox checked) {
4653
4610
  background-color: var(--checkbox-mark-color, white);
4654
4611
  -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
4612
  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
4613
  }
4657
- }
4658
4614
 
4659
4615
  /* Header cells — the two slotted <qti-simple-match-set>s have display:contents
4660
4616
  so their child choices act as row/column header cells. Written as a fully-
@@ -4684,79 +4640,71 @@
4684
4640
  qti-match-interaction.qti-match-tabular qti-simple-match-set:last-of-type qti-simple-associable-choice {
4685
4641
  text-align: center;
4686
4642
  }
4687
-
4688
- qti-match-interaction.qti-match-tabular {
4689
4643
  /* The 1px gap is the grid's own background showing through, which is what draws the cell rules.
4690
4644
  Clipping it to the rounded corners is layout and lives in qti-match-interaction-tabular.styles.ts. */
4691
- &::part(grid) {
4645
+ qti-match-interaction.qti-match-tabular::part(grid) {
4692
4646
  gap: 1px;
4693
4647
  background-color: var(--qti-border-color);
4694
4648
  border: var(--qti-border-thickness) solid var(--qti-border-color);
4695
4649
  border-radius: var(--qti-border-radius);
4696
4650
  }
4697
4651
 
4698
- &::part(corner),
4699
- &::part(input-cell) {
4652
+ qti-match-interaction.qti-match-tabular::part(corner),qti-match-interaction.qti-match-tabular::part(input-cell) {
4700
4653
  border: none;
4701
4654
  background-color: white;
4702
4655
  }
4703
4656
 
4704
- &::part(control checked) {
4657
+ qti-match-interaction.qti-match-tabular::part(control checked) {
4705
4658
  border-color: var(--qti-primary);
4706
4659
  background-color: var(--qti-primary);
4707
4660
  }
4708
4661
 
4709
- &::part(control-mark radio checked) {
4662
+ qti-match-interaction.qti-match-tabular::part(control-mark radio checked) {
4710
4663
  background-color: white;
4711
4664
  width: calc(var(--qti-control-size) * 0.5);
4712
4665
  height: calc(var(--qti-control-size) * 0.5);
4713
4666
  }
4714
4667
 
4715
- &::part(control-mark checkbox checked) {
4668
+ qti-match-interaction.qti-match-tabular::part(control-mark checkbox checked) {
4716
4669
  background-color: white;
4717
4670
  }
4718
- }
4719
4671
 
4720
4672
  qti-match-interaction.qti-match-tabular qti-simple-match-set qti-simple-associable-choice {
4721
4673
  border: none;
4722
4674
  background-color: white;
4723
4675
  }
4724
4676
  }
4677
+
4725
4678
  @layer qti-components.interactions.corrections {
4726
- qti-match-interaction.qti-match-tabular {
4727
4679
  /* Correct / incorrect overlays — border on the outer box, and a matching fill on the inner
4728
4680
  indicator when checked. */
4729
- &::part(control correct) {
4681
+ qti-match-interaction.qti-match-tabular::part(control correct) {
4730
4682
  border-color: var(--qti-correct);
4731
4683
  }
4732
- &::part(control incorrect) {
4684
+ qti-match-interaction.qti-match-tabular::part(control incorrect) {
4733
4685
  border-color: var(--qti-incorrect);
4734
4686
  }
4735
4687
 
4736
- &::part(control checked correct) {
4688
+ qti-match-interaction.qti-match-tabular::part(control checked correct) {
4737
4689
  border-color: var(--qti-correct);
4738
4690
  background-color: var(--qti-correct);
4739
4691
  }
4740
4692
 
4741
- &::part(control checked incorrect) {
4693
+ qti-match-interaction.qti-match-tabular::part(control checked incorrect) {
4742
4694
  border-color: var(--qti-incorrect);
4743
4695
  background-color: var(--qti-incorrect);
4744
4696
  }
4745
- }
4746
-
4747
- .full-correct-response qti-match-interaction.qti-match-tabular {
4748
- &::part(control checked) {
4697
+ :is(.full-correct-response qti-match-interaction.qti-match-tabular)::part(control checked) {
4749
4698
  background-color: var(--qti-answer-bg);
4750
4699
  color: var(--qti-answer-fg);
4751
4700
  border-color: var(--qti-answer-border);
4752
4701
  }
4753
4702
 
4754
- &::part(control-mark radio checked),
4755
- &::part(control-mark checkbox checked) {
4703
+ :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
4704
  background-color: var(--qti-answer-fg);
4757
4705
  }
4758
- }
4759
4706
  }
4707
+
4760
4708
  /* This interaction's chip vocabulary — the bank chip, the placed chip, and the floating clone. One
4761
4709
  such block per drag-drop interaction, opt-in; graphic-gap-match deliberately declares none. See
4762
4710
  qti-states.css. Kept in the `global` layer, where these rules used to live, so cascade position is
@@ -4773,7 +4721,7 @@
4773
4721
  sat on document.body it was in a different tree from the adopted stylesheet, and nothing — no
4774
4722
  part, no attribute — could style it. */
4775
4723
  @layer qti-components.global {
4776
- qti-order-interaction :state(drag),
4724
+ qti-order-interaction :is(:state(drag), [data-state~='drag']),
4777
4725
  [data-drag-clone][data-drag-interaction='qti-order-interaction'] {
4778
4726
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
4779
4727
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
@@ -4824,7 +4772,7 @@
4824
4772
  filter: var(--drag-filter, var(--qti-component-filter));
4825
4773
  }
4826
4774
 
4827
- qti-order-interaction :state(drag)::part(control)::before,
4775
+ qti-order-interaction :is(:state(drag), [data-state~='drag'])::part(control)::before,
4828
4776
  qti-order-interaction::part(drag-control)::before,
4829
4777
  qti-order-interaction ::part(drag-control)::before,
4830
4778
  [data-drag-clone][data-drag-interaction='qti-order-interaction']::part(control)::before {
@@ -4839,19 +4787,15 @@
4839
4787
  -webkit-mask: var(--qti-grip-mask) no-repeat center / contain;
4840
4788
  }
4841
4789
  }
4790
+
4842
4791
  @layer qti-components.interactions.base {
4843
- qti-order-interaction {
4844
- &:state(dragzone-active)::part(drags) {
4792
+ qti-order-interaction:is(:state(dragzone-active), [data-state~='dragzone-active'])::part(drags) {
4845
4793
  background-color: var(--qti-bg-active);
4846
4794
  }
4847
- &:state(dragzone-enabled)::part(drags) {
4795
+ qti-order-interaction:is(:state(dragzone-enabled), [data-state~='dragzone-enabled'])::part(drags) {
4848
4796
  background-color: var(--qti-bg-active);
4849
4797
  }
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) {
4798
+ qti-order-interaction::part(drag),qti-order-interaction qti-simple-choice,qti-order-interaction::part(drag) {
4855
4799
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
4856
4800
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
4857
4801
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -4899,30 +4843,30 @@
4899
4843
  color: var(--drag-color, var(--qti-component-color));
4900
4844
  opacity: var(--drag-opacity, var(--qti-component-opacity));
4901
4845
  filter: var(--drag-filter, var(--qti-component-filter));
4902
- &[dragging] {
4846
+ }
4847
+ [dragging]:is(qti-order-interaction::part(drag),qti-order-interaction qti-simple-choice,qti-order-interaction::part(drag)) {
4903
4848
  pointer-events: none;
4904
4849
  rotate: -2deg;
4905
4850
  --drag-box-shadow: 0 8px 12px rgb(0 0 0 / 20%), 0 4px 8px rgb(0 0 0 / 10%);
4906
4851
  }
4907
- &:hover {
4908
4852
  @media (hover: hover) {
4853
+ :is(qti-order-interaction::part(drag),qti-order-interaction qti-simple-choice,qti-order-interaction::part(drag)):hover {
4909
4854
  background-color: color-mix(
4910
4855
  in srgb,
4911
4856
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
4912
4857
  black
4913
4858
  );
4914
- }
4915
4859
  }
4916
- &:focus {
4860
+ }
4861
+ :is(qti-order-interaction::part(drag),qti-order-interaction qti-simple-choice,qti-order-interaction::part(drag)):focus {
4917
4862
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
4918
4863
  }
4919
- }
4920
4864
  /* Editor fake-drag projected into the interaction's drop-target shadow.
4921
4865
  Same `drag` styling so editor previews match runtime visuals.
4922
4866
  Kept as its own block (separate from the runtime-drag selector list
4923
4867
  above) because a CSS comment between commas inside a nested selector
4924
4868
  list is silently dropped by Chrome's nesting parser. */
4925
- &::part(drag) {
4869
+ qti-order-interaction::part(drag) {
4926
4870
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
4927
4871
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
4928
4872
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -4970,21 +4914,21 @@
4970
4914
  color: var(--drag-color, var(--qti-component-color));
4971
4915
  opacity: var(--drag-opacity, var(--qti-component-opacity));
4972
4916
  filter: var(--drag-filter, var(--qti-component-filter));
4973
- &:hover {
4917
+ }
4974
4918
  @media (hover: hover) {
4919
+ :is(qti-order-interaction::part(drag)):hover {
4975
4920
  background-color: color-mix(
4976
4921
  in srgb,
4977
4922
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
4978
4923
  black
4979
4924
  );
4980
- }
4981
4925
  }
4982
- &:focus {
4926
+ }
4927
+ :is(qti-order-interaction::part(drag)):focus {
4983
4928
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
4984
4929
  }
4985
- }
4986
4930
  /* The drag's box — flex, centred, clipped — is in qti-order-interaction.styles.ts. */
4987
- &::part(drag) {
4931
+ qti-order-interaction::part(drag) {
4988
4932
  /*
4989
4933
  * No `width: 100%`. It stretched a placed chip to its slot, which is why order was the one
4990
4934
  * entry in CHIP_BOX_KNOWN_BAD ("the placed chip stretches to fill its drop slot") — a 55px
@@ -5003,28 +4947,28 @@
5003
4947
  * The drop's flags are part tokens rather than attributes: an attribute selector cannot follow
5004
4948
  * `::part()`, and a <div> cannot carry a custom state (attachInternals throws). See setDropFlag.
5005
4949
  */
5006
- &::part(active) {
4950
+ qti-order-interaction::part(active) {
5007
4951
  background-color: var(--qti-bg-active);
5008
4952
  }
5009
4953
 
5010
- &::part(enabled) {
4954
+ qti-order-interaction::part(enabled) {
5011
4955
  background-color: var(--qti-bg-active);
5012
4956
  }
5013
4957
 
5014
4958
  /* A filled drop hides its dashed outline and drop-here glyph. Hides — the border keeps its
5015
4959
  width, or the column shifts 2px the instant a chip lands. */
5016
- &::part(filled) {
4960
+ qti-order-interaction::part(filled) {
5017
4961
  --qti-drop-border: var(--qti-border-thickness, 1px) solid transparent;
5018
4962
  --qti-drop-bg-img: none;
5019
4963
  }
5020
4964
 
5021
- &::part(drops) {
4965
+ qti-order-interaction::part(drops) {
5022
4966
  gap: var(--qti-gap);
5023
4967
  }
5024
- &::part(drags) {
4968
+ qti-order-interaction::part(drags) {
5025
4969
  gap: var(--qti-gap);
5026
4970
  }
5027
- &::part(drop) {
4971
+ qti-order-interaction::part(drop) {
5028
4972
  background-color: var(--drop-background-color, var(--qti-component-background-color));
5029
4973
  background-image: var(--drop-background-image, var(--qti-component-background-image));
5030
4974
  border-width: var(--drop-border-width, var(--qti-component-border-width));
@@ -5036,20 +4980,17 @@
5036
4980
  opacity: var(--drop-opacity, var(--qti-component-opacity));
5037
4981
  filter: var(--drop-filter, var(--qti-component-filter));
5038
4982
  border-style: var(--drop-border-style, dashed);
5039
- &[enabled] {
4983
+ }
4984
+ [enabled]:is(qti-order-interaction::part(drop)) {
5040
4985
  background-color: var(--qti-bg-active);
5041
4986
  }
5042
- &:focus {
4987
+ :is(qti-order-interaction::part(drop)):focus {
5043
4988
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
5044
4989
  }
5045
- }
5046
- &::part(active) {
4990
+ qti-order-interaction::part(active) {
5047
4991
  background-color: var(--qti-bg-active);
5048
4992
  }
5049
- }
5050
-
5051
- qti-order-interaction {
5052
- &::part(drops) {
4993
+ qti-order-interaction::part(drops) {
5053
4994
  margin: 0;
5054
4995
  border-radius: var(--qti-border-radius);
5055
4996
  }
@@ -5059,18 +5000,18 @@
5059
5000
  overhang the row by the border (42px row, 44px drop), which the drop's padding then doubled.
5060
5001
  It also defeated `:has(qti-simple-choice) [part~='drop'] { align-self: flex-start }` in
5061
5002
  qti-order-interaction.styles.ts. */
5062
- &::part(drop) {
5003
+ qti-order-interaction::part(drop) {
5063
5004
  border: var(--qti-drop-border, var(--qti-border-thickness) dashed var(--qti-border-color));
5064
5005
  border-radius: var(--qti-border-radius);
5065
5006
  background-color: transparent;
5066
5007
  background-image: var(--qti-drop-bg-img, revert-layer);
5067
5008
  }
5068
5009
 
5069
- &::part(active) {
5010
+ qti-order-interaction::part(active) {
5070
5011
  background-color: var(--qti-bg-active);
5071
5012
  }
5072
5013
 
5073
- &::part(enabled) {
5014
+ qti-order-interaction::part(enabled) {
5074
5015
  background-color: var(--qti-bg-active);
5075
5016
  }
5076
5017
 
@@ -5081,7 +5022,7 @@
5081
5022
  overlay (apps/e2e/src/stories/kennisnet/kennisnet.css), not here. In the base theme a placed
5082
5023
  chip is just the neutral chip (@mixin drag), and its corrections route through --drag-*
5083
5024
  like every other chip. */
5084
- & qti-simple-choice {
5025
+ qti-order-interaction qti-simple-choice {
5085
5026
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
5086
5027
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
5087
5028
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -5131,7 +5072,7 @@
5131
5072
  filter: var(--drag-filter, var(--qti-component-filter));
5132
5073
  }
5133
5074
 
5134
- &::part(container) {
5075
+ qti-order-interaction::part(container) {
5135
5076
  gap: 2rem;
5136
5077
  }
5137
5078
 
@@ -5148,16 +5089,13 @@
5148
5089
  * the bank above the drops" — that is what the qti-choices-top/bottom/left/right classes
5149
5090
  * already do, and wiring orientation to them is a behaviour change, not a move.
5150
5091
  */
5151
- &[orientation='vertical'] {
5152
- & qti-simple-choice,
5153
- &::part(drag) {
5092
+ qti-order-interaction[orientation='vertical'] qti-simple-choice,qti-order-interaction[orientation='vertical']::part(drag) {
5154
5093
  width: auto;
5155
5094
  }
5156
- }
5157
- }
5158
5095
  }
5096
+
5159
5097
  @layer qti-components.interactions.states {
5160
- qti-order-interaction qti-simple-choice:state(placeholder) {
5098
+ qti-order-interaction qti-simple-choice:is(:state(placeholder), [data-state~='placeholder']) {
5161
5099
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
5162
5100
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
5163
5101
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -5168,7 +5106,7 @@
5168
5106
  pointer-events: none;
5169
5107
  }
5170
5108
 
5171
- qti-order-interaction qti-simple-choice:state(dragging) {
5109
+ qti-order-interaction qti-simple-choice:is(:state(dragging), [data-state~='dragging']) {
5172
5110
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
5173
5111
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
5174
5112
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -5182,7 +5120,7 @@
5182
5120
  pointer-events: none;
5183
5121
  }
5184
5122
 
5185
- qti-order-interaction qti-simple-choice:state(return-anchor) {
5123
+ qti-order-interaction qti-simple-choice:is(:state(return-anchor), [data-state~='return-anchor']) {
5186
5124
  border-color: var(--qti-drop-highlight-border-color);
5187
5125
  box-shadow: inset 0 0 0 var(--qti-drop-highlight-border-width, 2px)
5188
5126
  color-mix(in srgb, var(--qti-drop-highlight-border-color) 55%, transparent);
@@ -5197,6 +5135,7 @@
5197
5135
  background-color: var(--qti-drop-highlight-enabled-bg);
5198
5136
  }
5199
5137
  }
5138
+
5200
5139
  @layer qti-components.interactions.corrections {
5201
5140
  /*
5202
5141
  * Judgement paint, via the shared mixin — every other drag-drop interaction already uses it.
@@ -5220,16 +5159,16 @@
5220
5159
  `[data-drag-clone]:state(candidate-*)` went with it and is deliberately NOT reproduced — a clone
5221
5160
  carries no custom states at all, because cloneNode does not copy ElementInternals (measured), so
5222
5161
  that selector never matched anything. */
5223
- qti-order-interaction :state(drag):state(candidate-correct),
5224
- qti-order-interaction::part(drag):state(candidate-correct) {
5162
+ qti-order-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-correct), [data-state~='candidate-correct']),
5163
+ qti-order-interaction::part(drag):is(:state(candidate-correct), [data-state~='candidate-correct']) {
5225
5164
  --drag-background-color: var(--qti-correct-light);
5226
5165
  --drag-color: var(--qti-correct);
5227
5166
  --drag-border-color: var(--qti-correct);
5228
5167
  --drag-box-shadow: none;
5229
5168
  }
5230
5169
 
5231
- qti-order-interaction :state(drag):state(candidate-incorrect),
5232
- qti-order-interaction::part(drag):state(candidate-incorrect) {
5170
+ qti-order-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
5171
+ qti-order-interaction::part(drag):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
5233
5172
  --drag-background-color: var(--qti-incorrect-light);
5234
5173
  --drag-color: var(--qti-incorrect);
5235
5174
  --drag-border-color: var(--qti-incorrect);
@@ -5239,6 +5178,7 @@
5239
5178
  /* The bank is hidden by the interaction itself, on :host([answer-key]) — see
5240
5179
  qti-order-interaction.styles.ts. */
5241
5180
  }
5181
+
5242
5182
  /* This interaction's chip vocabulary — the bank chip, the placed chip, and the floating clone. One
5243
5183
  such block per drag-drop interaction, opt-in; graphic-gap-match deliberately declares none. See
5244
5184
  qti-states.css. Kept in the `global` layer, where these rules used to live, so cascade position is
@@ -5260,7 +5200,7 @@
5260
5200
  and text colour from the removed rule and nothing else. Deleting that rule without this block
5261
5201
  turned the associate bank chips transparent, square and un-grabbable — measured, not guessed. */
5262
5202
  @layer qti-components.global {
5263
- qti-associate-interaction :state(drag),
5203
+ qti-associate-interaction :is(:state(drag), [data-state~='drag']),
5264
5204
  [data-drag-clone][data-drag-interaction='qti-associate-interaction'] {
5265
5205
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
5266
5206
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
@@ -5311,7 +5251,7 @@
5311
5251
  filter: var(--drag-filter, var(--qti-component-filter));
5312
5252
  }
5313
5253
 
5314
- qti-associate-interaction :state(drag)::part(control)::before,
5254
+ qti-associate-interaction :is(:state(drag), [data-state~='drag'])::part(control)::before,
5315
5255
  qti-associate-interaction::part(drag-control)::before,
5316
5256
  qti-associate-interaction ::part(drag-control)::before,
5317
5257
  [data-drag-clone][data-drag-interaction='qti-associate-interaction']::part(control)::before {
@@ -5326,8 +5266,8 @@
5326
5266
  -webkit-mask: var(--qti-grip-mask) no-repeat center / contain;
5327
5267
  }
5328
5268
  }
5269
+
5329
5270
  @layer qti-components.interactions.base {
5330
- qti-associate-interaction {
5331
5271
  /*
5332
5272
  * NO `--drag-border-color: transparent` here, and that absence is the rule.
5333
5273
  *
@@ -5343,20 +5283,17 @@
5343
5283
  * Associate was the only interaction doing this; match, order and gap-match all let the bank
5344
5284
  * chip take --qti-component-border-color, and now so does this one.
5345
5285
  */
5346
- & qti-simple-associable-choice {
5286
+ qti-associate-interaction qti-simple-associable-choice {
5347
5287
  padding: var(--qti-drag-padding);
5348
5288
  }
5349
5289
 
5350
- &:state(dragzone-active) slot[name='qti-simple-associable-choice'] {
5290
+ qti-associate-interaction:is(:state(dragzone-active), [data-state~='dragzone-active']) slot[name='qti-simple-associable-choice'] {
5351
5291
  background-color: var(--qti-bg-active);
5352
5292
  }
5353
- &:state(dragzone-enabled) slot[name='qti-simple-associable-choice'] {
5293
+ qti-associate-interaction:is(:state(dragzone-enabled), [data-state~='dragzone-enabled']) slot[name='qti-simple-associable-choice'] {
5354
5294
  background-color: var(--qti-bg-active);
5355
5295
  }
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) {
5296
+ qti-associate-interaction qti-simple-associable-choice::part(drag),qti-associate-interaction::part(drag) {
5360
5297
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
5361
5298
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
5362
5299
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -5404,29 +5341,29 @@
5404
5341
  color: var(--drag-color, var(--qti-component-color));
5405
5342
  opacity: var(--drag-opacity, var(--qti-component-opacity));
5406
5343
  filter: var(--drag-filter, var(--qti-component-filter));
5407
- &:hover {
5344
+ }
5408
5345
  @media (hover: hover) {
5346
+ :is(qti-associate-interaction qti-simple-associable-choice::part(drag),qti-associate-interaction::part(drag)):hover {
5409
5347
  background-color: color-mix(
5410
5348
  in srgb,
5411
5349
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
5412
5350
  black
5413
5351
  );
5414
- }
5415
5352
  }
5416
- &:focus {
5353
+ }
5354
+ :is(qti-associate-interaction qti-simple-associable-choice::part(drag),qti-associate-interaction::part(drag)):focus {
5417
5355
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
5418
5356
  }
5419
- &[dragging] {
5357
+ [dragging]:is(qti-associate-interaction qti-simple-associable-choice::part(drag),qti-associate-interaction::part(drag)) {
5420
5358
  pointer-events: none;
5421
5359
  rotate: -2deg;
5422
5360
  --drag-box-shadow: 0 8px 12px rgb(0 0 0 / 20%), 0 4px 8px rgb(0 0 0 / 10%);
5423
5361
  }
5424
- }
5425
5362
  /* Editor fake-drag projected into the interaction's drop-target shadow.
5426
5363
  Kept as a separate block (not merged into the comma list above)
5427
5364
  because a CSS comment between commas in a nested selector list is
5428
5365
  silently dropped by Chrome's nesting parser. */
5429
- &::part(drag) {
5366
+ qti-associate-interaction::part(drag) {
5430
5367
  /* Structural — the load-bearing box, never state-switched (layout-invariance contract).
5431
5368
  box-sizing is deliberate: the clone is appended to document.body outside reset.css's
5432
5369
  @scope, so it inherits no border-box from anywhere else and this sets it. */
@@ -5474,29 +5411,29 @@
5474
5411
  color: var(--drag-color, var(--qti-component-color));
5475
5412
  opacity: var(--drag-opacity, var(--qti-component-opacity));
5476
5413
  filter: var(--drag-filter, var(--qti-component-filter));
5477
- &:hover {
5414
+ }
5478
5415
  @media (hover: hover) {
5416
+ :is(qti-associate-interaction::part(drag)):hover {
5479
5417
  background-color: color-mix(
5480
5418
  in srgb,
5481
5419
  var(--drag-background-color, var(--qti-component-background-color)) 92%,
5482
5420
  black
5483
5421
  );
5484
- }
5485
5422
  }
5486
- &:focus {
5423
+ }
5424
+ :is(qti-associate-interaction::part(drag)):focus {
5487
5425
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
5488
5426
  }
5489
- }
5490
5427
  /* `display: grid` lives in qti-associate-interaction.styles.ts, which already declares it on
5491
5428
  [part~='drop'] alongside the drop's min-height/min-width. */
5492
5429
  /* A filled drop hides its dashed edge — see the note in qti-gap-match-interaction.css. Every
5493
5430
  associate drop holds exactly one chip (the div never carries match-max), so no capacity guard
5494
5431
  is needed here. Colour only: the width stays, or the row shifts when a chip lands. */
5495
- &::part(filled) {
5432
+ qti-associate-interaction::part(filled) {
5496
5433
  --drop-border-color: transparent;
5497
5434
  }
5498
5435
 
5499
- &::part(drop) {
5436
+ qti-associate-interaction::part(drop) {
5500
5437
  background-color: var(--drop-background-color, var(--qti-component-background-color));
5501
5438
  background-image: var(--drop-background-image, var(--qti-component-background-image));
5502
5439
  border-width: var(--drop-border-width, var(--qti-component-border-width));
@@ -5509,7 +5446,7 @@
5509
5446
  filter: var(--drop-filter, var(--qti-component-filter));
5510
5447
  border-style: var(--drop-border-style, dashed);
5511
5448
  }
5512
- &::part(drop):focus {
5449
+ qti-associate-interaction::part(drop):focus {
5513
5450
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
5514
5451
  }
5515
5452
 
@@ -5524,15 +5461,15 @@
5524
5461
  * Deleted here: `&::part(drop)[dragging] { @mixin act }`. An attribute selector after ::part()
5525
5462
  * never matches; that rule has never applied.
5526
5463
  */
5527
- &::part(active) {
5464
+ qti-associate-interaction::part(active) {
5528
5465
  background-color: var(--qti-bg-active);
5529
5466
  }
5530
5467
 
5531
- &::part(enabled) {
5468
+ qti-associate-interaction::part(enabled) {
5532
5469
  background-color: var(--qti-bg-active);
5533
5470
  }
5534
- }
5535
5471
  }
5472
+
5536
5473
  @layer qti-components.interactions.states {
5537
5474
  qti-simple-associable-choice::part(drag),
5538
5475
  qti-associate-interaction::part(drag) {
@@ -5594,7 +5531,7 @@
5594
5531
  background-color: var(--qti-drop-highlight-enabled-bg);
5595
5532
  }
5596
5533
 
5597
- qti-associate-interaction qti-simple-associable-choice:state(placeholder) {
5534
+ qti-associate-interaction qti-simple-associable-choice:is(:state(placeholder), [data-state~='placeholder']) {
5598
5535
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
5599
5536
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
5600
5537
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -5606,7 +5543,7 @@
5606
5543
  pointer-events: none;
5607
5544
  }
5608
5545
 
5609
- qti-associate-interaction qti-simple-associable-choice:state(dragging) {
5546
+ qti-associate-interaction qti-simple-associable-choice:is(:state(dragging), [data-state~='dragging']) {
5610
5547
  /* The hole a spent/lifted chip leaves. Like the chip (see `drag`), it sets NO fill / border /
5611
5548
  shadow of its own — those inherit from the resting chip (neutral by default, the brand's fill
5612
5549
  when an overlay is on), so a brand can restyle the hole via a `:state(placeholder)` overlay
@@ -5620,22 +5557,23 @@
5620
5557
  pointer-events: none;
5621
5558
  }
5622
5559
 
5623
- qti-associate-interaction qti-simple-associable-choice:state(placeholder) > *,
5624
- qti-associate-interaction qti-simple-associable-choice:state(dragging) > * {
5560
+ qti-associate-interaction qti-simple-associable-choice:is(:state(placeholder), [data-state~='placeholder']) > *,
5561
+ qti-associate-interaction qti-simple-associable-choice:is(:state(dragging), [data-state~='dragging']) > * {
5625
5562
  visibility: hidden;
5626
5563
  }
5627
5564
 
5628
- qti-associate-interaction qti-simple-associable-choice:state(placeholder)::part(label),
5629
- qti-associate-interaction qti-simple-associable-choice:state(dragging)::part(label) {
5565
+ qti-associate-interaction qti-simple-associable-choice:is(:state(placeholder), [data-state~='placeholder'])::part(label),
5566
+ qti-associate-interaction qti-simple-associable-choice:is(:state(dragging), [data-state~='dragging'])::part(label) {
5630
5567
  visibility: hidden;
5631
5568
  }
5632
5569
 
5633
- qti-associate-interaction qti-simple-associable-choice:state(return-anchor) {
5570
+ qti-associate-interaction qti-simple-associable-choice:is(:state(return-anchor), [data-state~='return-anchor']) {
5634
5571
  border-color: var(--qti-drop-highlight-border-color);
5635
5572
  box-shadow: inset 0 0 0 var(--qti-drop-highlight-border-width, 2px)
5636
5573
  color-mix(in srgb, var(--qti-drop-highlight-border-color) 55%, transparent);
5637
5574
  }
5638
5575
  }
5576
+
5639
5577
  @layer qti-components.interactions.corrections {
5640
5578
  /* A corrected chip — in the bank, and where it landed. Moved here from the shared
5641
5579
  qti-corrections.css, which claimed `:state(drag)` for EVERY interaction in one unscoped rule:
@@ -5643,18 +5581,18 @@
5643
5581
  `[data-drag-clone]:state(candidate-*)` went with it and is deliberately NOT reproduced — a clone
5644
5582
  carries no custom states at all, because cloneNode does not copy ElementInternals (measured), so
5645
5583
  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) {
5584
+ qti-associate-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-correct), [data-state~='candidate-correct']),
5585
+ qti-simple-associable-choice::part(drag):is(:state(candidate-correct), [data-state~='candidate-correct']),
5586
+ qti-associate-interaction::part(drag):is(:state(candidate-correct), [data-state~='candidate-correct']) {
5649
5587
  --drag-background-color: var(--qti-correct-light);
5650
5588
  --drag-color: var(--qti-correct);
5651
5589
  --drag-border-color: var(--qti-correct);
5652
5590
  --drag-box-shadow: none;
5653
5591
  }
5654
5592
 
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) {
5593
+ qti-associate-interaction :is(:state(drag), [data-state~='drag']):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
5594
+ qti-simple-associable-choice::part(drag):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']),
5595
+ qti-associate-interaction::part(drag):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
5658
5596
  --drag-background-color: var(--qti-incorrect-light);
5659
5597
  --drag-color: var(--qti-incorrect);
5660
5598
  --drag-border-color: var(--qti-incorrect);
@@ -5664,6 +5602,7 @@
5664
5602
  /* The bank is hidden by the interaction itself, on :host([answer-key]) — see
5665
5603
  qti-associate-interaction.styles.ts. */
5666
5604
  }
5605
+
5667
5606
  @layer qti-components.interactions.base {
5668
5607
  qti-graphic-order-interaction {
5669
5608
  --qti-graphic-order-pin-size: calc(var(--qti-select-point-marker-size, 1.5rem) * 1.2);
@@ -5671,8 +5610,9 @@
5671
5610
  --qti-graphic-order-pin-fill: var(--qti-background, #fff);
5672
5611
  --qti-graphic-order-pin-number-size: calc(var(--qti-graphic-order-pin-size) * 0.5);
5673
5612
  --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");
5613
+ }
5674
5614
 
5675
- & qti-hotspot-choice {
5615
+ qti-graphic-order-interaction qti-hotspot-choice {
5676
5616
  width: 100%;
5677
5617
  height: 100%;
5678
5618
  background-color: color-mix(in srgb, var(--qti-border-color) 18%, transparent);
@@ -5681,63 +5621,58 @@
5681
5621
  transition:
5682
5622
  background-color var(--qti-motion-duration-fast, 0.15s) ease,
5683
5623
  border-color var(--qti-motion-duration-fast, 0.15s) ease;
5624
+ }
5684
5625
 
5685
- &[shape='circle'] {
5626
+ [shape='circle']:is(qti-graphic-order-interaction qti-hotspot-choice) {
5686
5627
  min-width: calc(var(--qti-graphic-order-pin-size) * 0.6);
5687
5628
  min-height: calc(var(--qti-graphic-order-pin-size) * 0.6);
5688
5629
  }
5689
5630
 
5690
- &[shape='poly'] {
5631
+ [shape='poly']:is(qti-graphic-order-interaction qti-hotspot-choice) {
5691
5632
  background-color: color-mix(in srgb, var(--qti-border-color) 18%, transparent);
5692
5633
  box-shadow: none;
5693
5634
  }
5694
5635
 
5695
- &[shape='circle']:hover,
5696
- &[shape='rect']:hover,
5697
- &[shape='ellipse']:hover {
5636
+ [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
5637
  background-color: color-mix(in srgb, var(--qti-border-active) 12%, transparent);
5699
5638
  }
5700
5639
 
5701
- &[shape='poly']:hover {
5640
+ [shape='poly']:is(qti-graphic-order-interaction qti-hotspot-choice):hover {
5702
5641
  background-color: color-mix(in srgb, var(--qti-border-active) 20%, transparent);
5703
5642
  box-shadow: none;
5704
5643
  }
5705
5644
 
5706
- &[shape='circle']:focus-visible,
5707
- &[shape='rect']:focus-visible,
5708
- &[shape='ellipse']:focus-visible {
5645
+ [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
5646
  outline: var(--qti-focus-border-width) solid var(--qti-focus-color);
5710
5647
  outline-offset: 2px;
5711
5648
  }
5712
5649
 
5713
- &[shape='poly']:focus-visible {
5650
+ [shape='poly']:is(qti-graphic-order-interaction qti-hotspot-choice):focus-visible {
5714
5651
  outline: var(--qti-focus-border-width) solid var(--qti-focus-color);
5715
5652
  outline-offset: 2px;
5716
5653
  }
5717
5654
 
5718
- &[aria-ordervalue] {
5655
+ [aria-ordervalue]:is(qti-graphic-order-interaction qti-hotspot-choice) {
5719
5656
  border: 0 !important;
5720
5657
  box-shadow: none;
5721
5658
  background-color: color-mix(in srgb, var(--qti-selected-bg) 35%, transparent);
5722
5659
  }
5723
5660
 
5724
- &[aria-ordervalue]:hover,
5725
- &[aria-ordervalue]:focus-visible {
5661
+ [aria-ordervalue]:is(qti-graphic-order-interaction qti-hotspot-choice):hover,[aria-ordervalue]:is(qti-graphic-order-interaction qti-hotspot-choice):focus-visible {
5726
5662
  border-color: transparent !important;
5727
5663
  }
5728
- &[aria-ordercorrectvalue][aria-ordervalue]:not([shape='circle'])::after {
5664
+ [aria-ordercorrectvalue][aria-ordervalue]:is(qti-graphic-order-interaction qti-hotspot-choice):not([shape='circle'])::after {
5729
5665
  content: 'C=' attr(aria-ordercorrectvalue) ' R=' attr(aria-ordervalue) !important;
5730
5666
  color: var(--qti-correct);
5731
5667
  }
5732
- &[aria-ordercorrectvalue]:not([aria-ordervalue]):not([shape='circle'])::after {
5668
+ [aria-ordercorrectvalue]:is(qti-graphic-order-interaction qti-hotspot-choice):not([aria-ordervalue]):not([shape='circle'])::after {
5733
5669
  content: 'C=' attr(aria-ordercorrectvalue) !important;
5734
5670
  color: var(--qti-correct);
5735
5671
  }
5736
- }
5737
5672
 
5738
5673
  /* The pin's face. Where it sits — absolute, anchored to its hotspot, above the image — is
5739
5674
  placement, and lives in qti-graphic-order-interaction.styles.ts as ::slotted(). */
5740
- & .cito-graphic-order-marker {
5675
+ qti-graphic-order-interaction .cito-graphic-order-marker {
5741
5676
  width: var(--qti-graphic-order-pin-size);
5742
5677
  height: var(--qti-graphic-order-pin-size);
5743
5678
  padding-top: calc(var(--qti-graphic-order-pin-size) * 0.2);
@@ -5750,47 +5685,41 @@
5750
5685
  pointer-events: none;
5751
5686
  }
5752
5687
 
5753
- & .cito-graphic-order-marker--poly {
5688
+ qti-graphic-order-interaction .cito-graphic-order-marker--poly {
5754
5689
  width: calc(var(--qti-select-point-marker-size, 1.5rem) * 0.72);
5755
5690
  height: calc(var(--qti-select-point-marker-size, 1.5rem) * 0.72);
5756
5691
  font-size: calc(var(--qti-select-point-marker-size, 1.5rem) * 0.45);
5757
5692
  padding-top: calc(var(--qti-select-point-marker-size, 1.5rem) * 0.14);
5758
5693
  }
5759
- }
5760
5694
  }
5695
+
5761
5696
  @layer qti-components.interactions.states {
5762
- qti-graphic-order-interaction {
5763
- & qti-hotspot-choice {
5764
- &:state(readonly) {
5697
+ :is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(readonly), [data-state~='readonly']) {
5765
5698
  cursor: pointer;
5766
5699
  }
5767
5700
 
5768
- &:state(disabled) {
5701
+ :is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(disabled), [data-state~='disabled']) {
5769
5702
  cursor: not-allowed;
5770
5703
  }
5771
5704
 
5772
- &[shape='circle']:state(checked),
5773
- &[shape='rect']:state(checked),
5774
- &[shape='ellipse']:state(checked) {
5705
+ [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
5706
  background-color: color-mix(in srgb, var(--qti-selected-bg) 35%, transparent);
5776
5707
  }
5777
5708
 
5778
- &[shape='poly']:state(checked) {
5709
+ [shape='poly']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(checked), [data-state~='checked']) {
5779
5710
  background-color: color-mix(in srgb, var(--qti-selected-bg) 40%, transparent);
5780
5711
  box-shadow: none;
5781
5712
  }
5782
5713
 
5783
- &[aria-ordervalue]:state(checked),
5784
- &[aria-ordervalue] {
5714
+ [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
5715
  border-color: transparent !important;
5786
5716
  }
5787
- }
5788
5717
 
5789
5718
  /* No qti-selections-light / -dark here. Appendix B.1 of the QTI 3 implementation guide defines
5790
5719
  that class pair for qti-hotspot-interaction and qti-graphic-gap-match-interaction only, and no
5791
5720
  item in this repo ever set it on a graphic-order interaction. What was here overrode the neutral
5792
5721
  drag-bank highlight below with --qti-light-* and --qti-dark-* colours that nothing could reach. */
5793
- &:state(dragzone-active)::part(drags) {
5722
+ qti-graphic-order-interaction:is(:state(dragzone-active), [data-state~='dragzone-active'])::part(drags) {
5794
5723
  /* Direct properties, not slots: ::part(drags) is a bare slot container with no paint role of
5795
5724
  its own (the other interactions paint it with `@mixin dropping`, also direct), so there is
5796
5725
  no --<role>-* to switch. Inlined from `@mixin act`. */
@@ -5798,85 +5727,70 @@
5798
5727
  background-color: var(--qti-bg-active);
5799
5728
  }
5800
5729
 
5801
- &:state(dragzone-enabled)::part(drags) {
5730
+ qti-graphic-order-interaction:is(:state(dragzone-enabled), [data-state~='dragzone-enabled'])::part(drags) {
5802
5731
  background-color: var(--qti-bg-active);
5803
5732
  }
5804
- }
5805
5733
  }
5734
+
5806
5735
  @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) {
5736
+ [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
5737
  background-color: color-mix(in srgb, var(--qti-answer-bg) 75%, transparent);
5813
5738
  }
5814
5739
 
5815
- &[shape='poly']:state(correct-response) {
5740
+ [shape='poly']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(correct-response), [data-state~='correct-response']) {
5816
5741
  background-color: color-mix(in srgb, var(--qti-answer-bg) 80%, transparent);
5817
5742
  box-shadow: none;
5818
5743
  }
5819
5744
 
5820
- &[shape='circle']:state(candidate-correct),
5821
- &[shape='rect']:state(candidate-correct),
5822
- &[shape='ellipse']:state(candidate-correct) {
5745
+ [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
5746
  background-color: color-mix(in srgb, var(--qti-correct) 35%, transparent);
5824
5747
  }
5825
5748
 
5826
- &[shape='poly']:state(candidate-correct) {
5749
+ [shape='poly']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(candidate-correct), [data-state~='candidate-correct']) {
5827
5750
  background-color: color-mix(in srgb, var(--qti-correct) 40%, transparent);
5828
5751
  box-shadow: none;
5829
5752
  }
5830
5753
 
5831
- &[shape='circle']:state(candidate-incorrect),
5832
- &[shape='rect']:state(candidate-incorrect),
5833
- &[shape='ellipse']:state(candidate-incorrect) {
5754
+ [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
5755
  background-color: color-mix(in srgb, var(--qti-incorrect) 35%, transparent);
5835
5756
  }
5836
5757
 
5837
- &[shape='poly']:state(candidate-incorrect) {
5758
+ [shape='poly']:is(qti-graphic-order-interaction qti-hotspot-choice):is(:state(candidate-incorrect), [data-state~='candidate-incorrect']) {
5838
5759
  background-color: color-mix(in srgb, var(--qti-incorrect) 40%, transparent);
5839
5760
  box-shadow: none;
5840
5761
  }
5841
5762
 
5842
- &[aria-ordervalue]:state(correct-response),
5843
- &[aria-ordervalue]:state(candidate-correct),
5844
- &[aria-ordervalue]:state(candidate-incorrect) {
5763
+ [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
5764
  border-color: transparent !important;
5846
5765
  }
5847
- }
5848
- }
5849
5766
  }
5767
+
5850
5768
  @layer qti-components.interactions.base {
5851
- qti-graphic-associate-interaction {
5852
5769
  /*
5853
5770
  * qti-associable-hotspot does not use ActiveElementMixin, so it carries no
5854
5771
  * checked/disabled/readonly custom states. Selection and correction are drawn as
5855
5772
  * ::part(line) / ::part(point) instead. Give it a `part="drag"` and the
5856
5773
  * ActiveElementMixin states before adding state-dependent rules here.
5857
5774
  */
5858
- & qti-associable-hotspot {
5859
5775
  /* `[shape='square']` used to be in this selector. It is not a QTI shape — the spec has
5860
5776
  circle, default, ellipse, poly and rect — positionShapes has no branch for it, and no item
5861
5777
  in the repo sets it. */
5862
- &[shape='circle'] {
5778
+ [shape='circle']:is(qti-graphic-associate-interaction qti-associable-hotspot) {
5863
5779
  background-color: transparent;
5864
- &:focus {
5780
+ }
5781
+ [shape='circle']:is(qti-graphic-associate-interaction qti-associable-hotspot):focus {
5865
5782
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
5866
5783
  }
5867
- }
5868
- }
5869
- }
5870
5784
  }
5785
+
5871
5786
  @layer qti-components.interactions.states {
5872
- qti-graphic-associate-interaction {
5873
5787
 
5874
5788
 
5875
5789
  /* No qti-selections-light / -dark here. Appendix B.1 of the QTI 3 implementation guide defines
5876
5790
  that class pair for qti-hotspot-interaction and qti-graphic-gap-match-interaction only, and no
5877
5791
  item in this repo ever set it on a graphic-associate interaction. What was here overrode the neutral
5878
5792
  drag-bank highlight below with --qti-light-* and --qti-dark-* colours that nothing could reach. */
5879
- &:state(dragzone-active)::part(drags) {
5793
+ qti-graphic-associate-interaction:is(:state(dragzone-active), [data-state~='dragzone-active'])::part(drags) {
5880
5794
  /* Direct properties, not slots: ::part(drags) is a bare slot container with no paint role of
5881
5795
  its own (the other interactions paint it with `@mixin dropping`, also direct), so there is
5882
5796
  no --<role>-* to switch. Inlined from `@mixin act`. */
@@ -5884,17 +5798,18 @@
5884
5798
  background-color: var(--qti-bg-active);
5885
5799
  }
5886
5800
 
5887
- &:state(dragzone-enabled)::part(drags) {
5801
+ qti-graphic-associate-interaction:is(:state(dragzone-enabled), [data-state~='dragzone-enabled'])::part(drags) {
5888
5802
  background-color: var(--qti-bg-active);
5889
5803
  }
5890
- }
5891
5804
  }
5805
+
5892
5806
  @layer qti-components.interactions.base{
5893
5807
  qti-slider-interaction {
5894
5808
  --qti-tick-color: rgb(229 231 235 / 100%);
5895
5809
  --qti-tick-width: 1px;
5896
5810
  }
5897
5811
  }
5812
+
5898
5813
  @layer qti-components.interactions.base {
5899
5814
  qti-select-point-interaction {
5900
5815
  /*
@@ -5911,37 +5826,34 @@ qti-slider-interaction {
5911
5826
  * itself (`[part~='correct']`), which is deeper than this host declaration.
5912
5827
  */
5913
5828
  --qti-select-point-marker-color: var(--qti-primary);
5914
-
5915
- &::part(point) {
5916
- &:focus {
5829
+ }
5830
+ :is(qti-select-point-interaction::part(point)):focus {
5917
5831
  box-shadow: 0 0 0 var(--qti-focus-border-width) var(--qti-focus-color);
5918
5832
  }
5919
- }
5920
- }
5921
5833
  }
5834
+
5922
5835
  @layer qti-components.interactions.corrections {
5923
- qti-select-point-interaction {
5924
5836
  /* Same knob as the resting colour above — these set `color` before, which stopped reaching the
5925
5837
  marker once it was no longer painted with `currentColor`. */
5926
- &::part(correct) {
5838
+ qti-select-point-interaction::part(correct) {
5927
5839
  --qti-select-point-marker-color: var(--qti-correct);
5928
5840
  }
5929
5841
 
5930
- &::part(incorrect) {
5842
+ qti-select-point-interaction::part(incorrect) {
5931
5843
  --qti-select-point-marker-color: var(--qti-incorrect);
5932
5844
  }
5933
- }
5934
5845
  }
5846
+
5935
5847
  @layer qti-components.interactions.base{
5936
- qti-position-object-stage {
5937
- & qti-position-object-interaction {
5848
+ qti-position-object-stage qti-position-object-interaction {
5938
5849
  /* no styles necessary, only layout styles, defined in the component */
5939
5850
  }
5940
5851
  }
5941
- }
5852
+
5942
5853
  @layer qti-components.interactions.base{
5943
5854
  /* `display: block` lives in qti-prompt.styles.ts, which already declares it on :host. */
5944
5855
  }
5856
+
5945
5857
  @layer qti-components.interactions.corrections{
5946
5858
  /*
5947
5859
  * What is LEFT here is what the base class owns.
@@ -5998,14 +5910,14 @@ qti-position-object-stage {
5998
5910
  /* A cloned interaction presents the answer key rather than a candidate judgement. What is left
5999
5911
  here is the part no interaction owns: any chip, anywhere. The text-entry and inline-choice
6000
5912
  halves moved into their own files — those name one interaction each, so they belong to it. */
6001
- .full-correct-response :state(drag),
5913
+ .full-correct-response :is(:state(drag), [data-state~='drag']),
6002
5914
  .full-correct-response ::part(drag) {
6003
5915
  background-color: var(--qti-answer-bg);
6004
5916
  color: var(--qti-answer-fg);
6005
5917
  border-color: var(--qti-answer-border);
6006
5918
  }
6007
5919
 
6008
- .full-correct-response :state(drag)::part(control)::before,
5920
+ .full-correct-response :is(:state(drag), [data-state~='drag'])::part(control)::before,
6009
5921
  .full-correct-response ::part(drag-control)::before {
6010
5922
  content: '';
6011
5923
  background-color: var(--qti-answer-fg);
@@ -6013,6 +5925,7 @@ qti-position-object-stage {
6013
5925
  -webkit-mask: var(--qti-check-mask) no-repeat center / contain;
6014
5926
  }
6015
5927
  }
5928
+
6016
5929
  @layer qti-components.elements{
6017
5930
  /* `display: block` is declared by the element itself, in qti-rubric-block.ts. */
6018
5931
  qti-rubric-block {
@@ -6023,14 +5936,17 @@ qti-rubric-block {
6023
5936
  margin-top: var(--qti-gap);
6024
5937
  }
6025
5938
  }
5939
+
6026
5940
  @layer qti-components.elements{
6027
5941
  qti-feedback-inline::part(feedback) {
6028
5942
  }
6029
5943
  }
5944
+
6030
5945
  @layer qti-components.elements{
6031
5946
  qti-feedback-block::part(feedback) {
6032
5947
  }
6033
5948
  }
5949
+
6034
5950
  @layer qti-components.elements{
6035
5951
  /*
6036
5952
  * Neither selector below matches anything this repo renders — `qti-modal-feedback-custom` is not a
@@ -6049,6 +5965,7 @@ qti-feedback-block::part(feedback) {
6049
5965
  margin-top: var(--qti-gap);
6050
5966
  }
6051
5967
  }
5968
+
6052
5969
  @layer qti-components.global{
6053
5970
  /*
6054
5971
  * Cross-interaction custom states.
@@ -6098,13 +6015,15 @@ qti-feedback-block::part(feedback) {
6098
6015
  * paint. If the double indication ever reads as too much, the fix is here or in the mixin, not
6099
6016
  * a display value.
6100
6017
  */
6101
- :state(show-correct-response) {
6018
+ :is(:state(show-correct-response), [data-state~='show-correct-response']) {
6102
6019
  outline: var(--qti-border-thickness) dashed var(--qti-correct);
6103
6020
  outline-offset: 0.25rem;
6104
6021
  }
6105
6022
  }
6023
+
6106
6024
  /* Item-shell structure (what is shown, what is hidden). Kept out of styles/qti-theme/ because it is
6107
6025
  structure rather than paint — that directory is where the no-layout-in-theme lint rule applies. */
6026
+
6108
6027
  @layer qti-components.global{
6109
6028
 
6110
6029
  /**