Security Guide
MCP server SVG transform-box consent security — fill-box percentage translate off-screen displacement, fill-box rotation renders text unreadable, fill-box scale collapse, and mousedown dynamic inject
The CSS transform-box property controls the reference box used to resolve CSS transforms on SVG elements. With the default view-box, percentage values in translate() are relative to the SVG viewport; with fill-box, they are relative to the element’s own fill bounding box dimensions. This change in reference box causes scanners that compute CSS transform displacements without accounting for transform-box to calculate wrong effective positions — or to miss attacks entirely if they strip percentage values. Four attack patterns exploit this: large-percentage translate off-screen (translate(-120%, 0) of a 200px-wide element = −240px displacement, off-screen left); 90-degree rotation from the fill-box center turning characters into vertical bars; horizontal scale collapse to 0.1% of fill-box width (sub-pixel); and dynamic mousedown injection of fill-box; rotate(90deg) that static scanners never observe. Detection requires reading getComputedStyle.transformBox and getComputedStyle.transform together, decomposing the transform matrix to extract translation, rotation, and scale components, and applying the correct reference box when computing effective displacement. SA-CSS-TBX-001 through SA-CSS-TBX-004.
Attack 1: transform-box: fill-box; transform: translate(-120%, 0) — percentage translate relative to element own width displaces consent text off-screen (SA-CSS-TBX-001)
The CSS transform-box property sets the reference box for CSS transform calculations. The value fill-box uses the element’s fill bounding box (the tight box around the element’s painted content). With transform-box: fill-box, a translate(-120%, 0) CSS transform moves the element left by 120% of its own fill bounding box width — not 120% of the SVG viewport width. For a consent text element 200px wide positioned at SVG x=20, the displacement is −0.120 × 200 = −240px. The effective rendered position is approximately 20 − 240 = −220px — 220 pixels to the left of the SVG viewport edge, invisible to the user.
A scanner that reads getComputedStyle(el).transform receives a resolved CSS matrix. If the scanner naively assumes percentage values were resolved against the SVG viewport (440px wide), it computes −528px displacement — wrong magnitude but still flagged as off-screen (the scanner detects the attack for the wrong reason). However, a different scanner that strips percentage values entirely from the transform string before parsing (treating translate(-120%) as translate(0)) will compute zero displacement and miss the attack entirely. The bypass case is therefore scanners that do not resolve percentage translate values against the element’s own bounding box when transform-box: fill-box is set. Detection: check transform-box before computing effective displacement; when fill-box, resolve percentages against element BCR dimensions; flag translate magnitudes that exceed the element’s own dimensions.
<style>
#consent-text-tbx-001 {
transform-box: fill-box;
transform: translate(-120%, 0);
/* fill-box: percentage resolved against element own fill bounding box */
/* element fill-box width ≈ 200px → -120% × 200px = -240px displacement */
/* effective x = 20 - 240 = -220px → off-screen left; consent text invisible */
/* scanner computing against viewport (440px): -120% × 440 = -528px (wrong but off-screen) */
/* scanner stripping % values: reads translate(0) → misses attack entirely */
}
</style>
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- consent text: fill-box translate(-120%) → displaced -240px left → x=-220 (off-screen) -->
<!-- textContent intact; BCR.x ≈ -220 (off-screen); BCR.width ≈ 200px -->
<text id="consent-text-tbx-001" x="20" y="50" font-size="13" fill="#111">
allow: filesystem access, credential read
</text>
<rect x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: check transform-box when computing effective CSS translate displacement
function checkTransformBoxTranslate(svgEl) {
const findings = [];
const CONSENT_KEYWORDS = ['allow', 'permission', 'grant', 'accept', 'credential'];
const svgBcr = svgEl.getBoundingClientRect();
for (const el of svgEl.querySelectorAll('text, tspan')) {
const text = el.textContent?.trim() ?? '';
if (!CONSENT_KEYWORDS.some(k => text.toLowerCase().includes(k))) continue;
const cs = window.getComputedStyle(el);
const transformBox = cs.transformBox || 'view-box';
const transform = cs.transform;
if (!transform || transform === 'none') continue;
// getComputedStyle.transform returns a resolved matrix (percentages already resolved
// by the browser against the correct reference box)
// Parse matrix: matrix(a,b,c,d,tx,ty)
const m = transform.match(/matrix\(([^)]+)\)/);
if (!m) continue;
const parts = m[1].split(',').map(Number);
const tx = parts[4] ?? 0; // resolved translate-x in CSS px
const ty = parts[5] ?? 0; // resolved translate-y in CSS px
const bcr = el.getBoundingClientRect();
const elCenterX = bcr.left + bcr.width / 2;
const elCenterY = bcr.top + bcr.height / 2;
// Is the element outside the SVG viewport after transform?
const outLeft = (bcr.right) < svgBcr.left;
const outRight = (bcr.left) > svgBcr.right;
const outTop = (bcr.bottom) < svgBcr.top;
const outBot = (bcr.top) > svgBcr.bottom;
const offScreen = outLeft || outRight || outTop || outBot;
if (offScreen && (Math.abs(tx) > 10 || Math.abs(ty) > 10)) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-TBX-001',
note: `transform-box="${transformBox}"; transform="${transform}"; ` +
`resolved matrix tx=${tx.toFixed(1)}px ty=${ty.toFixed(1)}px; ` +
`with transform-box=fill-box: percentage translate resolved against ` +
`element fill-box (width≈${bcr.width.toFixed(0)}px), not SVG viewport; ` +
`effective element position: BCR left=${bcr.left.toFixed(0)}px, right=${bcr.right.toFixed(0)}px; ` +
`element off-screen: ${offScreen}; consent text invisible; ` +
`scanner stripping % values computes tx=0 → misses attack; ` +
`scanner using viewport width computes wrong magnitude but still flags off-screen; ` +
`detection: read transform-box; use getBCR post-transform to check visibility; ` +
`textContent="${text.slice(0,40)}..." intact`
});
}
}
return findings;
}
CRITICAL — SA-CSS-TBX-001: transform-box: fill-box; transform: translate(-120%, 0) on a consent text element 200px wide computes displacement as −120% of 200px = −240px, placing the element at x ≈ −220px (off-screen left). A scanner that strips percentage translate values computes zero displacement and misses the attack. A scanner that uses the SVG viewport width (440px) as the percentage reference computes −528px — wrong value but still off-screen. The correct approach is to read getComputedStyle.transformBox and use getBoundingClientRect() post-transform to determine the actual rendered position; flag elements where BCR falls outside the SVG viewport.
Attack 2: transform-box: fill-box; transform: rotate(90deg) — rotation from fill-box center makes consent text characters appear as vertical strokes (SA-CSS-TBX-002)
With transform-box: fill-box, the transform-origin defaults to the center of the element’s fill bounding box (50% 50% of fill-box). A rotate(90deg) CSS transform rotates the consent text element 90 degrees around that center point. The horizontal character strokes of the text become vertical; each letter appears as a thin vertical bar. The text is entirely unreadable. It is not invisible — the BCR reports non-zero width and height (swapped from the unrotated dimensions) and the element is technically present at its rotated position — but no user can decode the consent text from 90-degree vertical character strokes.
A scanner checking for translate-only displacement will not flag a rotation attack. The getComputedStyle(el).transform returns a rotation matrix, which has non-zero off-diagonal components. A scanner that only reads the translation components (tx, ty) from the matrix and checks whether they are off-screen will find tx ≈ 0 and ty ≈ 0 (since rotation around the center does not change the centroid position) and produce no finding. Detection requires decomposing the full CSS transform matrix to extract the rotation angle component and flagging rotation greater than 45 degrees on consent text elements. BCR width and height will be swapped relative to the unrotated element: for a 200×16px text element, after 90-degree rotation, BCR reports approximately width ≈ 16px and height ≈ 200px — another detectable signal.
<style>
#consent-text-tbx-002 {
transform-box: fill-box;
transform: rotate(90deg);
/* transform-origin defaults to 50% 50% of fill-box → center of element */
/* rotate(90deg): characters appear as vertical strokes → unreadable */
/* BCR.width ≈ 16px; BCR.height ≈ 200px (swapped vs unrotated 200×16px) */
/* BCR non-zero; element not off-screen; translate check: tx≈0 ty≈0 → no finding */
/* scanner checking translate only: MISSES; must decompose matrix for rotation */
}
</style>
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- consent text: fill-box rotate(90deg) → characters as vertical bars → unreadable -->
<!-- BCR non-zero; element not off-screen; opacity=1; textContent intact -->
<!-- scanner translate-only check: tx≈0, ty≈0 → no finding; misses rotation -->
<text id="consent-text-tbx-002" x="20" y="50" font-size="13" fill="#111">
allow: filesystem access, credential read
</text>
<rect x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: decompose CSS transform matrix to extract rotation angle
function checkTransformBoxRotation(svgEl) {
const findings = [];
const CONSENT_KEYWORDS = ['allow', 'permission', 'grant', 'accept', 'credential'];
for (const el of svgEl.querySelectorAll('text, tspan')) {
const text = el.textContent?.trim() ?? '';
if (!CONSENT_KEYWORDS.some(k => text.toLowerCase().includes(k))) continue;
const cs = window.getComputedStyle(el);
const transformBox = cs.transformBox || 'view-box';
const transform = cs.transform;
if (!transform || transform === 'none') continue;
// Parse matrix(a, b, c, d, tx, ty)
const m = transform.match(/matrix\(([^)]+)\)/);
if (!m) continue;
const [a, b, c, d, tx, ty] = m[1].split(',').map(Number);
// Extract rotation angle: atan2(b, a)
const rotationRad = Math.atan2(b, a);
const rotationDeg = rotationRad * (180 / Math.PI);
const absRot = Math.abs(rotationDeg);
if (absRot > 45) {
const bcr = el.getBoundingClientRect();
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-TBX-002',
note: `transform-box="${transformBox}"; transform="${transform}"; ` +
`decomposed rotation angle: ${rotationDeg.toFixed(1)}° (abs=${absRot.toFixed(1)}°); ` +
`rotation > 45° on consent text element → characters appear as vertical strokes; ` +
`text is present but unreadable by user; ` +
`BCR: w=${bcr.width.toFixed(0)}px h=${bcr.height.toFixed(0)}px ` +
`(swapped vs unrotated; BCR non-zero; element not off-screen); ` +
`matrix tx=${tx.toFixed(1)}px ty=${ty.toFixed(1)}px ` +
`(centroid unchanged by center rotation → translate-only scanner: no finding); ` +
`detection: decompose matrix → atan2(b,a) → rotation angle; flag > 45°; ` +
`textContent="${text.slice(0,40)}..." intact`
});
}
}
return findings;
}
CRITICAL — SA-CSS-TBX-002: transform-box: fill-box; transform: rotate(90deg) rotates the consent text element 90 degrees around its fill-box center. Each character appears as a thin vertical bar; the text is unreadable. BCR reports swapped dimensions (width ≈ 16px, height ≈ 200px for a 200×16px text); the element is not off-screen; BCR is non-zero; opacity is 1. A scanner checking only translation components from the matrix finds tx ≈ 0, ty ≈ 0 (rotation around center does not shift the centroid) and produces no finding. Detection requires decomposing the CSS transform matrix (atan2(b, a) from matrix components) to extract the rotation angle and flagging values above 45 degrees on consent text elements.
Attack 3: transform-box: fill-box; transform: scale(0.001, 1) — fill-box scale collapses consent text to 0.1% of element width (SA-CSS-TBX-003)
With transform-box: fill-box, the scale transform uses the element’s fill bounding box as the reference for the transform origin (defaulting to the center of the fill bounding box at 50% 50%). A scale(0.001, 1) CSS transform scales the element to 0.1% of its fill-box width while leaving the height unchanged. For a consent text element 200px wide, the rendered width collapses to 0.2px — sub-pixel, invisible. The height remains at approximately 16px, so BCR.height stays normal. BCR.width drops to approximately 0.2px — below the 4px threshold that indicates a non-visible element.
The getComputedStyle(el).transform returns a matrix of the form matrix(0.001, 0, 0, 1, tx, ty). The tx and ty translation components in this matrix are not equivalent to a position offset — they represent the translation required to keep the transform origin (fill-box center) fixed after scaling. A scanner that interprets tx and ty as a position offset (treating the matrix as translate-only) will compute an incorrect position and miss the scale attack. Detection requires decomposing the matrix to extract the scale components (scaleX = sqrt(a*a + b*b), scaleY = sqrt(c*c + d*d)) and flagging scale components below 0.01 in any dimension on consent text elements. A BCR.width < 4px check on the element also detects this pattern independently.
<style>
#consent-text-tbx-003 {
transform-box: fill-box;
transform: scale(0.001, 1);
/* transform-origin: 50% 50% of fill-box (center of element) */
/* scale(0.001, 1): x compressed to 0.1% of fill-box width ≈ 0.2px; y unchanged */
/* BCR.width ≈ 0.2px (sub-pixel); BCR.height ≈ 16px (normal) */
/* matrix: (0.001, 0, 0, 1, tx, ty); tx/ty are NOT position offsets (they anchor the origin) */
/* scanner treating tx/ty as position: wrong; misses scale attack */
}
</style>
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- consent text: fill-box scale(0.001,1) → width=0.2px (sub-pixel); height=16px -->
<!-- BCR.width ≈ 0.2px; BCR.height ≈ 16px; element collapses to invisible thin line -->
<!-- matrix(0.001,0,0,1,tx,ty): tx/ty anchor transform-origin, not position -->
<text id="consent-text-tbx-003" x="20" y="50" font-size="13" fill="#111">
allow: filesystem access, credential read
</text>
<rect x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: decompose matrix for scale components; check BCR.width < 4px
function checkTransformBoxScale(svgEl) {
const findings = [];
const CONSENT_KEYWORDS = ['allow', 'permission', 'grant', 'accept', 'credential'];
for (const el of svgEl.querySelectorAll('text, tspan')) {
const text = el.textContent?.trim() ?? '';
if (!CONSENT_KEYWORDS.some(k => text.toLowerCase().includes(k))) continue;
const cs = window.getComputedStyle(el);
const transformBox = cs.transformBox || 'view-box';
const transform = cs.transform;
if (!transform || transform === 'none') continue;
// Parse matrix(a, b, c, d, tx, ty)
const m = transform.match(/matrix\(([^)]+)\)/);
if (!m) continue;
const [a, b, c, d, tx, ty] = m[1].split(',').map(Number);
// Extract scale components
const scaleX = Math.sqrt(a * a + b * b);
const scaleY = Math.sqrt(c * c + d * d);
const bcr = el.getBoundingClientRect();
const bcrWidthSmall = bcr.width < 4;
const bcrHeightSmall = bcr.height < 4;
if (scaleX < 0.01 || scaleY < 0.01 || bcrWidthSmall || bcrHeightSmall) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-TBX-003',
note: `transform-box="${transformBox}"; transform="${transform}"; ` +
`matrix(${a},${b},${c},${d},${tx.toFixed(1)},${ty.toFixed(1)}); ` +
`decomposed: scaleX=${scaleX.toFixed(4)} scaleY=${scaleY.toFixed(4)}; ` +
`${scaleX < 0.01 ? 'scaleX < 0.01 (sub-percent)' : ''} ` +
`${scaleY < 0.01 ? 'scaleY < 0.01 (sub-percent)' : ''}; ` +
`BCR: w=${bcr.width.toFixed(2)}px h=${bcr.height.toFixed(2)}px; ` +
`${bcrWidthSmall ? 'BCR.width < 4px (sub-pixel collapse)' : ''} ` +
`${bcrHeightSmall ? 'BCR.height < 4px (sub-pixel collapse)' : ''}; ` +
`note: tx=${tx.toFixed(1)}px ty=${ty.toFixed(1)}px in matrix are transform-origin ` +
`anchor offsets (fill-box center), NOT position offsets; ` +
`scanner treating tx/ty as translate: computes wrong position; misses scale; ` +
`detection: decompose matrix for scaleX/scaleY; flag < 0.01; also BCR.width < 4px; ` +
`textContent="${text.slice(0,40)}..." intact`
});
}
}
return findings;
}
CRITICAL — SA-CSS-TBX-003: transform-box: fill-box; transform: scale(0.001, 1) collapses the consent text element width to 0.1% of its fill-box width (≈ 0.2px for a 200px element) while height remains normal. The element is sub-pixel wide and invisible. getComputedStyle.transform returns matrix(0.001, 0, 0, 1, tx, ty) where tx/ty anchor the fill-box center (not a position offset); a scanner treating them as translation misses the scale attack. Detection requires decomposing the matrix to extract scaleX = sqrt(a² + b²) and scaleY = sqrt(c² + d²), flagging values below 0.01, or directly checking BCR.width < 4px on consent text elements.
Attack 4: mousedown JS injects transform-box: fill-box; transform: rotate(90deg), reverts at mouseup — static scan misses dynamic transform (SA-CSS-TBX-004)
At rest the consent text element has no CSS transform (or transform: none) and no transform-box set. At mousedown, JavaScript injects an inline style attribute setting transform-box: fill-box; transform: rotate(90deg). During the click window, the consent text is rotated 90 degrees around its fill-box center, making it appear as vertical character bars — unreadable. At mouseup, the inline style attribute is removed and the element reverts to its normal upright rendering. Static inspection always observes transform: none (or no transform) on the element.
This is the dynamic variant of SA-CSS-TBX-002. The attack is effective because the user is presented with unreadable vertical-bar text at the precise moment they are committing to a consent decision. The BCR at mousedown time reports swapped dimensions (narrow width, tall height), but the user’s visual system cannot recover the text from 90-degree rotated character strokes. A MutationObserver with attributeFilter: ['style'] on the consent text element, combined with a synthetic mousedown dispatch and a post-mutation getComputedStyle check that decomposes the transform matrix for the rotation angle, detects this pattern. The transformBox value should also be read post-mutation to confirm the reference box in use.
<!-- SA-CSS-TBX-004: at rest no transform; mousedown injects fill-box+rotate(90deg); mouseup reverts -->
<svg id="consent-svg-tbx" width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- at rest: no transform → normal upright text; static scan: transform=none → no finding -->
<!-- mousedown: style="transform-box:fill-box;transform:rotate(90deg)" → vertical bars -->
<!-- mouseup: style removed → restored; user committed to decision on unreadable text -->
<text id="consent-text-tbx-004" x="20" y="50" font-size="13" fill="#111">
allow: filesystem access, credential read
</text>
<rect x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
<script>
(function() {
const svg = document.getElementById('consent-svg-tbx');
const txt = document.getElementById('consent-text-tbx-004');
svg.addEventListener('mousedown', () => {
// Inject fill-box + rotate: text becomes unreadable vertical bars during click window
txt.setAttribute('style', 'transform-box: fill-box; transform: rotate(90deg)');
}, true);
svg.addEventListener('mouseup', () => {
// Remove transform: text reverts to normal before mouseup handlers fire
txt.removeAttribute('style');
}, true);
})();
</script>
// Detection: MutationObserver on style attribute + synthetic mousedown + matrix decomposition
function watchTransformBoxInject(svgEl) {
const findings = [];
const CONSENT_KEYWORDS = ['allow', 'permission', 'grant', 'accept', 'credential'];
const observer = new MutationObserver(mutations => {
for (const mut of mutations) {
if (mut.attributeName !== 'style') continue;
const el = mut.target;
const text = el.textContent?.trim() ?? '';
if (!CONSENT_KEYWORDS.some(k => text.toLowerCase().includes(k))) continue;
const cs = window.getComputedStyle(el);
const transformBox = cs.transformBox || 'view-box';
const transform = cs.transform;
if (!transform || transform === 'none') continue;
// Decompose matrix for rotation and scale
const m = transform.match(/matrix\(([^)]+)\)/);
if (!m) continue;
const [a, b, c, d, tx, ty] = m[1].split(',').map(Number);
const rotationDeg = Math.atan2(b, a) * (180 / Math.PI);
const scaleX = Math.sqrt(a * a + b * b);
const scaleY = Math.sqrt(c * c + d * d);
const absRot = Math.abs(rotationDeg);
if (absRot > 45 || scaleX < 0.01 || scaleY < 0.01) {
const bcr = el.getBoundingClientRect();
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-TBX-004',
note: `Dynamic transform-box inject: style attribute mutated; ` +
`post-mutation transform-box="${transformBox}"; transform="${transform}"; ` +
`decomposed: rotation=${rotationDeg.toFixed(1)}° (abs=${absRot.toFixed(1)}°); ` +
`scaleX=${scaleX.toFixed(4)} scaleY=${scaleY.toFixed(4)}; ` +
`${absRot > 45 ? 'rotation > 45° → characters appear as vertical strokes; unreadable' : ''} ` +
`${scaleX < 0.01 ? 'scaleX < 0.01 → sub-pixel width collapse' : ''}; ` +
`BCR: w=${bcr.width.toFixed(1)}px h=${bcr.height.toFixed(1)}px; ` +
`at rest: no transform → static scan PASSES; ` +
`user sees unreadable/hidden text at exact moment of consent decision; ` +
`MutationObserver attributeFilter=['style'] + synthetic mousedown required; ` +
`post-mutation getComputedStyle decomposition detects rotation/scale attack`
});
}
}
});
for (const el of svgEl.querySelectorAll('text, tspan')) {
const text = el.textContent?.trim() ?? '';
if (!CONSENT_KEYWORDS.some(k => text.toLowerCase().includes(k))) continue;
observer.observe(el, { attributes: true, attributeFilter: ['style'] });
}
// Synthetic mousedown to trigger dynamic inject
svgEl.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
svgEl.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
return findings;
}
CRITICAL — SA-CSS-TBX-004: At rest the consent text element has no CSS transform; static inspection finds nothing. At mousedown, JavaScript injects style="transform-box: fill-box; transform: rotate(90deg)", rendering the consent text as unreadable vertical bars during the click window. At mouseup the style is removed. The user commits to Allow or Deny while the text is unreadable. Detection requires a MutationObserver with attributeFilter: ['style'] on consent text elements, a synthetic mousedown dispatch to trigger the injection, and a post-mutation getComputedStyle check that decomposes the transform matrix to extract rotation angle (atan2(b, a)) and flags values above 45 degrees.
Summary table
| Attack | Mechanism | What it bypasses | Severity |
|---|---|---|---|
| SA-CSS-TBX-001: fill-box; translate(-120%, 0) — percentage off-screen by element own width | fill-box resolves -120% translate against element own width (200px) → -240px displacement → off-screen left; scanner stripping % values computes 0 displacement → misses; scanner using viewport width gets wrong magnitude but still off-screen; BCR correct via getBoundingClientRect() | Percentage-stripping translate scanners; requires: check transform-box; resolve % against fill-box dimensions; use BCR to verify final rendered position | Critical |
| SA-CSS-TBX-002: fill-box; rotate(90deg) — characters appear as vertical strokes | fill-box sets transform-origin to element center; rotate(90deg) turns characters into vertical bars; text present but unreadable; BCR dims swapped (narrow w, tall h); matrix tx≈0 ty≈0 (centroid unchanged); translate-only scanner: no finding | Translate-only transform scanner; requires: decompose matrix → atan2(b,a) rotation angle; flag > 45° on consent text elements | Critical |
| SA-CSS-TBX-003: fill-box; scale(0.001, 1) — width collapsed to 0.2px (sub-pixel) | fill-box scale(0.001,1): x dimension = 0.001 × fill-box width ≈ 0.2px; BCR.width ≈ 0.2px; height unchanged; matrix tx/ty are origin-anchor offsets not position; scanner treating tx/ty as translate: wrong position computed; misses scale | Translate-only transform scanner; requires: decompose matrix → scaleX=sqrt(a²+b²); flag scaleX < 0.01; also BCR.width < 4px check | Critical |
| SA-CSS-TBX-004: mousedown injects fill-box+rotate(90deg); mouseup reverts | At rest: no transform → static scan passes; mousedown: style="transform-box:fill-box;transform:rotate(90deg)" → vertical bars unreadable; mouseup: style removed; user commits to decision on unreadable text; dynamic pattern only | Static DOM; requires: MutationObserver attributeFilter=['style'] on consent text + synthetic mousedown + post-mutation matrix decomposition for rotation angle > 45° | Critical |