/** * theming-sentinel-exceptions — the written adjudications R-5.4 requires. * * A public token the sentinel reports as "no effect" is EITHER a lie (retire * it) OR its silence has a measured reason that lives here, one line per * token: the part is conditional and the demo does not mount it, the state * needs forcing, the consumer is a pseudo-element or a composed component the * instrument cannot read, the value sits behind a media query. "Forced → * reaches" means the adjudication PROVED the token alive by mounting what it * needs; an instrument-limit entry names the limit (they are also registered * in next-features §13). * * An entry whose token starts moving again is reported as STALE by the guard — * prune it (a stale exception is the same debt as a stale allowlist row). * * All entries below were measured during the F2-A adversarial review * (2026-08-21) unless dated otherwise. */ const PLANE_SUPERSEDED = 'SUPERSEDED by the depth plane: the part carries data-depth=overlay, and [data-depth=overlay] declares this property at the same specificity (0,1,0) and LATER in the cascade, so the recipe key is bypassed (see next-features 12.9, measured on tooltip, popover and float-panel)'; export const SENTINEL_EXCEPTIONS: Record> = { fab: { // 2026-08-24. ONE key, and it is the guard's blind spot for `translate` — // the same limit already adjudicated for `transform` on background, // rating-group and card, on the sibling property. Three independent // reasons, all measured: the property is not in the read set; the static // pass parks the pointer and this rule is `:hover`-only; and a sentinel- // sized lift moves the node OUT from under the cursor, so the state // cancels itself (`:hover` true at t+50ms, false by t+300ms). lift: 'the guard does not snapshot `translate` (the `transform` limit of background / rating-group / card on the sibling property) and its static pass never hovers, while this rule is `[data-fab]:hover:not([data-disabled]):not(:active)`. Measured by hand with a real pointer: set --fab-lift=1234px BEFORE hovering and the FAB computes `translate: 0px -1234px` at t+300ms -> reaches. Note the self-cancel that makes it unmeasurable even with a hover pass: a 1234px lift carries the node out from under the cursor, :hover drops and it settles back to none by t+900ms' }, 'tags-input': { // 2026-08-24. Four reasons, all measured on the real nodes with a real // pointer; the twenty-four tone keys are the palette cascade and ride the // PATTERN entry below. 'item-fg': "the ARCHETYPE ink always wins: the morfo stamps aria-selected='true' as a LITERAL on EVERY tag, so archetypes.css's `[data-archetype='item'][aria-selected='true']:not([data-state='checked']):not([data-state='on'])` (0,4,0) out-ranks the recipe's `[data-tags-input-item]` (0,1,0) on every item, at rest and always. Measured 2026-08-24: with the attribute as shipped the token moves nothing (the ink stays oklch(0.2435 0 0) = --color-content-primary, the archetype's); set aria-selected=false on the real tag and the same write reaches (rgb(1, 2, 3)). Same class as table.selected-row-fg and the listbox ink (next-features §12); the aria literal is a morfo decision and is registered, not touched from this axis", 'placeholder-fg': "the input carries a `placeholder` attribute only while it is EMPTY (soma: `isEmpty ? opts.placeholder : undefined`) and the stage boots with two tags, so ::placeholder returns the ELEMENT's style — the trap already measured on field. Emptying the stage is not an option for this run: it deletes every item node, which is where a third of the contract paints. Measured both ways 2026-08-24: clearing the tags through the demo's own Clear trigger gives the input placeholder=\"Add tag\", and forcing the attribute on the real input makes the token reach (rgb(1, 2, 3); its default is oklch(0.61 0 0) = --color-content-muted)", 'transition-duration': 'the guard freezes transitions and this token IS the transition; measured unfrozen -> reaches (0.12s -> 4.321s on the three transitioned properties of the control)', 'transition-ease': 'same freeze; measured unfrozen -> reaches (cubic-bezier(0.4, 0, 0.2, 1) -> steps(3))' }, avatar: { // 2026-08-23. Five steps of scales the measured stage does not wear, all // forced on the REAL nodes with transitions frozen. Two of them sit on the // `xxl` step, which the size sweep of the guard does not reach (the same // limit `metrics` recorded below). 'size-xxl': 'the size sweep of the guard stops at xl; forced data-size=xxl -> reaches (block-size 96px -> 1234px)', 'font-size-xxl': 'same xxl step, read on the fallback initials (the image carries no font); forced -> reaches (28px -> 37px)', 'radius-none': 'only the step in force paints; forced data-radius=none -> reaches (0px -> 37px)', 'ring-width-sm': 'only the step in force paints (the demo boots ringWidth=md); forced data-ring-width=sm -> reaches (halo 1.5px -> 17px)', 'group-overlap-xxl': 'same xxl step, on AvatarGroup; forced data-size=xxl on the real group -> reaches (margin-inline-start -33.6px -> 37px)' }, editable: { // 2026-08-23. Only two reasons, both measured: the transition pair IS the // transition the guard freezes, and the twelve tone keys are the palette // cascade (adjudicated by PATTERN below). 'submit-fg': 'OSCILLATES: read ALIVE in 3 of 4 runs on the same HEAD (42/56) and dead in the fourth (41/56). Leaving the edit mode is what a blur does, and the guard opens by focus + prepareWith, so the surface it measures is not deterministic. Measured alive with the editor open and transitions frozen -> reaches. Without this entry the fourth run fails R-5.4 with a token that does not lie (2026-08-23)', 'transition-duration': 'the guard freezes transitions and this token IS the transition; measured unfrozen -> reaches (0.12s -> 4.321s on the three transitioned properties of the preview)', 'transition-ease': 'same freeze; measured unfrozen -> reaches (cubic-bezier(0.4, 0, 0.2, 1) -> steps(3))', // NOT the palette pattern: this one key is ALSO read straight by the invalid // rules, so its silence has a second, different cause — the demo is valid. 'risk-border': 'the palette path is superseded like its eleven siblings, but this key is ALSO the invalid tint, read directly by three rules; forced data-invalid on the real root -> reaches on the input AND on the control (oklch(0.8059 0.1123 59.96) -> rgb(1, 2, 3))' }, background: { // 2026-08-23. Its parts are independent OPT-IN layers and only ONE pattern // renders at a time, so the default stage moved 3 of 37 tokens. The guard // now switches the scrim on and sweeps `data-pattern` (18/37); what is // left is measured here, forcing the attribute on the REAL layer — never // on a node built by hand. // // The mask: the guard does not read `mask-image`, so both read dead while // being perfectly alive (instrument gap, next-features §13). 'fade-size': 'the guard does not snapshot mask-image; measured by hand → reaches (the radial mask changes)', 'fade-at': 'the guard does not snapshot mask-image; measured by hand → reaches (at 50% 35% → at 11% 50%)', // The ink context: only under `on='dark'` / `on='light'`. 'scrim-fg-over-dark': "only under on='dark'; forced → reaches (the veil colour changes)", 'scrim-fg-over-light': "only under on='light'; forced → reaches (the veil colour changes)", // Two scales where only the step in force paints (the `section` / `qr-code` // class): every step measured one by one on the real scrim layer. 'scrim-blur-sm': 'only the step in force paints; forced data-blur=sm → reaches (4px → 33px)', 'scrim-blur-md': 'only the step in force paints; forced data-blur=md → reaches (8px → 33px)', 'scrim-blur-lg': 'only the step in force paints; forced data-blur=lg → reaches (12px → 33px)', 'scrim-blur-xxl': 'only the step in force paints; forced data-blur=xxl → reaches (24px → 33px)', 'scrim-strength-xs': 'only the step in force paints; forced data-strength=xs → reaches (alpha 0.08 → 0.9)', 'scrim-strength-sm': 'only the step in force paints; forced data-strength=sm → reaches (alpha 0.13 → 0.9)', 'scrim-strength-lg': 'only the step in force paints; forced data-strength=lg → reaches (alpha 0.4 → 0.9)', 'scrim-strength-xl': 'only the step in force paints; forced data-strength=xl → reaches (alpha 0.7 → 0.9)', // A gradient layer EXCLUDES a pattern layer, so the sweep cannot have both. 'gradient-drift-duration': 'the gradient layer is exclusive with the pattern one the sweep needs; forced data-kind=gradient → reaches (24s → 42s)', // The spotlight is off on the default stage and, once on, it repaints the // pattern layer it shares a node with — measured on its own pass. 'spotlight-size': 'spotlight is off in the demo; forced data-spotlight → reaches (40% 40% → 42px)', 'spotlight-strength': 'spotlight is off in the demo; forced data-spotlight → reaches (the mix stop moves)', 'spotlight-fg': 'spotlight is off AND the demo layer carries data-color, where --palette-solid wins by design; forced data-spotlight with no data-color → reaches', // Structural, not conditional: the travel only enters through the // scroll-linked `background-parallax` keyframes, which need a scroll // timeline the measured stage does not have. 'parallax-travel': 'the demo boots at speed 0, so no layer carries data-parallax; forced data-parallax + speed 1 on the real layer, the scroll-linked translate follows it (14.6 -> 60.5 px at the same scrollY). The guard would not see it anyway: translate / transform are not in its property snapshot (next-features 13)', // No node at all: the demo never mounts the pause control. 'pause-offset': 'the pause control is not mounted in the demo (0 nodes on the page)', 'pause-z': 'the pause control is not mounted in the demo (0 nodes on the page)' }, 'chat-composer': { // 2026-08-23. Four, and three of them are the guard's own two habits. 'focus-border': 'only paints under :focus-within, and the guard blurs the active element after opening; focused the REAL textarea with transitions frozen -> reaches (color(srgb 0.745 0.577 0.894 / 0.48) -> rgb(1, 2, 3) on the shell border)', 'transition-duration': 'the guard freezes transitions and this token IS the transition; measured unfrozen -> reaches (0.12s -> 4.321s on the shell AND on the send)', 'transition-ease': 'same freeze; measured unfrozen -> reaches (cubic-bezier(0.4, 0, 0.2, 1) -> steps(3))', // NOT the guard: a cross-recipe collision, and the composer is on the losing // side of it BY DESIGN — its documented composition puts it inside FileUpload. 'disabled-opacity': 'file-upload sweeps DESCENDANTS: `[data-file-upload] [data-disabled]` is (0,2,0), the same weight as `[data-chat-composer-send][data-disabled]`, and its recipe is emitted LATER, so it wins over every disabled node inside a FileUpload — which is where the demo (and the documented composition) puts the composer; measured on the REAL nodes with the ancestor attr removed -> reaches (0.4 -> 0.123 on the send AND on the shell). Both defaults are --opacity-disabled, so the collision is invisible until a theme moves one' }, 'chat-message': { // 2026-08-24. Four, and every one of them is a limit of the INSTRUMENT — // measured on the real nodes, one by one. 'bubble-tail-radius': 'the guard snapshots exactly ONE corner (`borderTopLeftRadius`) and the tail is the BOTTOM one — `border-end-start-radius` incoming, `border-end-end-radius` outgoing. Measured on the real bubble across the 8 direction x run combinations: 4px -> 1234px on solo and last in BOTH directions (10px/10px/10px/4px -> 10px/10px/10px/1234px in, 10px/10px/4px/10px -> 10px/10px/1234px/10px out), while the corner the guard reads never moves. Its sibling `bubble-radius-run` reads alive because `middle` flattens the START corner, which IS in the read set', 'emphasize-duration': 'the guard freezes transitions and this token IS the transition (the jump-to-message flash is a two-state toggle + a transition, R-4.5 — there are no keyframes); measured unfrozen on the real row -> reaches (0.24s -> 4.321s)', 'focus-radius': 'the row is tabbable ONLY when composed into a Feed — soma merges `Feed.Article`s identity (tabindex, keyboard) into the provider props when a Feed ancestor exists — and its own demo mounts it standalone, so `:focus-visible` has no node there. Measured on /uix/components/chat-log, where the rows ARE feed articles (tabindex=0), with transitions frozen: rest 6px (the emphasize corner), focused 4px -> 1234px, both by programmatic focus and by a real Tab walk', 'quick-react-hover-scale': 'two limits at once: the guard does not snapshot `transform` (the same blind spot adjudicated for background / rating-group / card) and the cell is a PORTALED tapback button whose rule is `:hover, :focus-visible`. Measured with a real pointer on the open bar, transitions frozen -> reaches (matrix(1.18, 0, 0, 1.18, 0, 0) -> matrix(7.77, 0, 0, 7.77, 0, 0))' }, 'chat-log': { // 2026-08-23. ONE key, and only because the guard blurs after opening: the // event row is a tabbable feed article whose corner exists only under the // focus ring (it is square at rest). 'event-radius': 'only paints under :focus-visible, and the guard blurs the active element after opening; keyboard-focused the REAL row with transitions frozen -> reaches (rest 0px, focused 4px -> 1234px)' }, badge: { // 2026-08-23. The demo mounts ONE chip, and its `rounded` axis paints only // the step in force — measured forcing each on the real chip. 'radius-sm': 'only the step in force paints; forced data-rounded=sm → reaches (4px → 37px)', 'radius-lg': 'only the step in force paints; forced data-rounded=lg → reaches (10px → 37px)' }, 'color-swatch': { // 2026-08-23. The inset ring is OPT-IN (`bordered`, off by default) and NO // consumer in the repo passes it, so the `::after` that draws it exists on // no page. Forced `data-bordered` on a real chip of the gradient-builder // stage, transitions frozen: the ring appears and both tokens follow. 'border-width': 'the `bordered` ring is opt-in and no consumer mounts it; forced data-bordered on the real chip → reaches (inset 0 0 0 1px → 1234px)', 'border-fg': 'same opt-in ring; forced data-bordered on the real chip → reaches (oklch(0.931 0 0) → rgb(1, 2, 3))' }, form: { // 2026-08-23. 'grid-min': "only under [data-layout='grid'], and the computed template only CHANGES when the value alters the column count: forced the layout and 90px -> reaches (502px -> four 110.5px columns)", 'error-summary-list-indent': 'the summary renders EMPTY in the demo (no errors), so it has no