Security Guide
MCP server SVG feColorMatrix consent security — type=matrix whiteout maps dark text pixels to white making ink invisible, type=saturate plus luminance shift achieves the same effect, per-element scope feColorMatrix with unmodified header, and mousedown whiteout matrix injection
SVG’s feColorMatrix filter primitive applies a color transformation matrix to an element’s rendered pixels before they are composited onto the canvas. The transformation operates at the pixel level, post-rendering — it does not change the element’s fill or stroke attributes in the DOM. A type="matrix" feColorMatrix with values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" maps every input RGB channel to 1 (white): regardless of the original pixel color, the output is white. Dark consent text with fill="#111" is rendered as white pixels on a white SVG background — completely invisible. Critically, getComputedStyle(el).fill still returns the original dark fill value rgb(17, 17, 17); the color matrix transformation happens after the fill is applied to the rendering buffer and before compositing, entirely below the DOM property layer. A scanner checking fill color will find dark fill and assume the text is visible. Four attack patterns: whiteout matrix, saturate+component-transfer whiteout, per-element scope colorMatrix, and mousedown injection.
Attack 1: feColorMatrix type=matrix whiteout — all colors mapped to white (SA-CSS-CMX-001)
The SVG color transformation matrix is a 4×5 matrix operating on RGBA color vectors. Each output channel is a linear combination of the input channels plus a constant: R_out = a·R_in + b·G_in + c·B_in + d·A_in + e, and similarly for G, B, A. The constant term (the fifth column) is a fixed additive offset applied regardless of the input color. Setting all four rows to [0, 0, 0, 0, 1] sets the constant to 1 for each of R, G, B, and preserves alpha unchanged (fourth row [0, 0, 0, 1, 0]). This means R_out = 1, G_out = 1, B_out = 1, A_out = A_in — every pixel becomes white (full luminance) at its original alpha. Dark glyph ink pixels (R≈0.07, G≈0.07, B≈0.07) are converted to (1, 1, 1) — white. On a white SVG background, white ink is invisible.
The deception of this attack for scanner detection lies in the DOM/CSSOM layer. The fill attribute on the text element is "#111" — unchanged. getComputedStyle(el).fill returns "rgb(17, 17, 17)" — a dark color that would pass a fill-color contrast check. The filter attribute is filter="url(#whiteout)". The filter definition in <defs> contains the color matrix. A scanner that checks fill color via getComputedStyle will see fill=rgb(17,17,17) and conclude the text is dark on a white background — passing. A scanner that checks BCR will find non-zero dimensions. The only way to detect this attack without rendering the SVG to a canvas is to (1) check for a non-none filter attribute, (2) resolve the filter definition, (3) find the feColorMatrix, (4) parse the matrix values, and (5) compute what color rgb(17,17,17) maps to under the transformation. If the output color is white or near-white (>0.9 on all channels), the filter is a whiteout.
<!-- SA-CSS-CMX-001: feColorMatrix type=matrix whiteout — all pixels → white -->
<!-- fill="#111" (dark); after matrix, every pixel → (1,1,1) white; invisible on white bg -->
<!-- getComputedStyle(el).fill = "rgb(17,17,17)" STILL — fill check passes! -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="whiteout">
<!-- R=0*R+0*G+0*B+0*A+1, G=same, B=same, A=0*R+0*G+0*B+1*A+0 → every pixel white -->
<feColorMatrix type="matrix"
values="0 0 0 0 1
0 0 0 0 1
0 0 0 0 1
0 0 0 1 0"/>
</filter>
</defs>
<rect width="440" height="120" fill="white"/>
<!-- fill="#111" (dark); feColorMatrix maps all pixels to white; invisible on white bg -->
<!-- getComputedStyle(el).fill = "rgb(17,17,17)" (unchanged by filter — DOM not affected) -->
<text x="20" y="40" font-size="13" fill="#111"
filter="url(#whiteout)">
Allow: shell execution, credential access
</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: resolve filter; find feColorMatrix; compute mapped color for fill; flag if near-white
function checkColorMatrixWhiteout(svgEl) {
const findings = [];
for (const el of svgEl.querySelectorAll('text, tspan')) {
const filterAttr = el.getAttribute('filter') || getComputedStyle(el).filter || '';
if (!filterAttr || filterAttr === 'none') continue;
const refMatch = filterAttr.match(/url\(#([^)]+)\)/);
if (!refMatch) continue;
const filterId = refMatch[1];
const filterEl = svgEl.ownerDocument
? svgEl.ownerDocument.getElementById(filterId)
: svgEl.querySelector(`#${filterId}`);
if (!filterEl) continue;
for (const cm of filterEl.querySelectorAll('feColorMatrix')) {
const type = cm.getAttribute('type') || 'matrix';
const values = cm.getAttribute('values') || '';
if (type !== 'matrix') continue; // other types handled separately
const nums = values.trim().split(/\s+/).map(Number);
if (nums.length !== 20) continue;
// Matrix rows: [R row 0..4, G row 5..9, B row 10..14, A row 15..19]
// Compute what fill color rgb(17,17,17) maps to (normalize to 0..1)
const rIn = 17/255, gIn = 17/255, bIn = 17/255, aIn = 1;
const rOut = nums[0]*rIn + nums[1]*gIn + nums[2]*bIn + nums[3]*aIn + nums[4];
const gOut = nums[5]*rIn + nums[6]*gIn + nums[7]*bIn + nums[8]*aIn + nums[9];
const bOut = nums[10]*rIn + nums[11]*gIn + nums[12]*bIn + nums[13]*aIn + nums[14];
const WHITE_THRESHOLD = 0.9;
if (rOut >= WHITE_THRESHOLD && gOut >= WHITE_THRESHOLD && bOut >= WHITE_THRESHOLD) {
const fillCS = getComputedStyle(el).fill;
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-CMX-001',
note: `<${el.tagName}> filter="url(#${filterId})"; ` +
`feColorMatrix type=matrix maps fill ${fillCS} → ` +
`rgb(${(rOut*255).toFixed(0)},${(gOut*255).toFixed(0)},${(bOut*255).toFixed(0)}) (near-white); ` +
`ink invisible on white SVG background; ` +
`getComputedStyle(el).fill="${fillCS}" (dark — fill check PASSES!); ` +
`BCR non-zero; textContent intact; ` +
`detection requires matrix parsing + color-map computation for actual fill color`
});
}
}
}
return findings;
}
CRITICAL — SA-CSS-CMX-001: feColorMatrix type=matrix with all-white output constant maps dark consent text pixels to white, making the ink invisible on a white SVG background. getComputedStyle(el).fill still returns the original dark fill value — a fill-color contrast check passes. Detection requires: (1) resolving the filter attribute to a <filter> definition; (2) finding feColorMatrix type=matrix; (3) parsing the 20-value matrix; (4) computing the transformed color for the element’s fill; (5) flagging if the result is near-white (>0.9 on all RGB channels).
Attack 2: feColorMatrix type=saturate values=0 plus feComponentTransfer luminance whiteout (SA-CSS-CMX-002)
feColorMatrix type="saturate" values="0" desaturates the input to a grayscale representation using the standard luminance weights (0.213·R + 0.715·G + 0.072·B). For dark gray ink (#111), the grayscale output is approximately 0.067 (near-black gray). A second filter primitive, feComponentTransfer with a linear function slope=0 intercept=1 on each channel, then applies the transformation: C_out = 0 × C_in + 1 = 1 for all channels — mapping any gray value to white. The two-step chain (desaturate then force-to-white) produces the same whiteout result as the direct matrix approach in Attack 1, but uses a more obscure two-primitive filter chain that may evade scanners looking only for the specific type=matrix whiteout pattern.
This variant exercises the need for full filter chain analysis: a scanner must evaluate all filter primitives in the chain, not only the first or the most recognizable one. The feColorMatrix type=saturate primitive by itself produces a dark gray output (not white). Only when followed by the feComponentTransfer step does the result become white. A scanner that checks only feColorMatrix primitives and misses feComponentTransfer will fail to detect this chain. The correct detection approach processes the full filter primitive sequence, simulating the color transformation from input to output, and evaluates the final output color against the background color for contrast.
<!-- SA-CSS-CMX-002: saturate=0 → gray; feComponentTransfer slope=0 intercept=1 → white -->
<!-- Two-primitive chain; saturate alone doesn't flag; full chain analysis required -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="sat-whiteout">
<!-- Step 1: desaturate to grayscale (~0.067 for #111) -->
<feColorMatrix type="saturate" values="0"/>
<!-- Step 2: feComponentTransfer slope=0 intercept=1 → all channels to 1 (white) -->
<feComponentTransfer>
<feFuncR type="linear" slope="0" intercept="1"/>
<feFuncG type="linear" slope="0" intercept="1"/>
<feFuncB type="linear" slope="0" intercept="1"/>
</feComponentTransfer>
</filter>
</defs>
<rect width="440" height="120" fill="white"/>
<!-- fill="#111"; after saturate+component-transfer chain → white; invisible on white bg -->
<!-- getComputedStyle(el).fill still returns "rgb(17,17,17)" (dark — fill check passes!) -->
<text x="20" y="40" font-size="13" fill="#111"
filter="url(#sat-whiteout)">
Allow: shell execution, credential access
</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 feComponentTransfer with slope=0 intercept=1 (maps any color to white)
function checkComponentTransferWhiteout(svgEl) {
const findings = [];
for (const el of svgEl.querySelectorAll('text, tspan')) {
const filterAttr = el.getAttribute('filter') || getComputedStyle(el).filter || '';
if (!filterAttr || filterAttr === 'none') continue;
const refMatch = filterAttr.match(/url\(#([^)]+)\)/);
if (!refMatch) continue;
const filterId = refMatch[1];
const filterEl = svgEl.ownerDocument
? svgEl.ownerDocument.getElementById(filterId)
: svgEl.querySelector(`#${filterId}`);
if (!filterEl) continue;
// Check for feComponentTransfer with all-white-output functions
for (const fct of filterEl.querySelectorAll('feComponentTransfer')) {
let allWhiteForced = true;
for (const fn of fct.querySelectorAll('feFuncR, feFuncG, feFuncB')) {
const fnType = fn.getAttribute('type') || 'identity';
const slope = parseFloat(fn.getAttribute('slope')) || 0;
const intercept = parseFloat(fn.getAttribute('intercept')) || 0;
// linear(slope=0, intercept=1): output = 0*any + 1 = 1 → white
if (!(fnType === 'linear' && slope === 0 && intercept >= 0.9)) {
allWhiteForced = false;
}
}
if (allWhiteForced) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-CMX-002',
note: `<${el.tagName}> filter="url(#${filterId})"; ` +
`feComponentTransfer with slope=0 intercept≥0.9 on R/G/B — forces all channels to white; ` +
`combined with feColorMatrix type=saturate (or other primitives); ` +
`full filter chain produces white output for any dark ink; ` +
`getComputedStyle(el).fill="${getComputedStyle(el).fill}" (dark — fill check PASSES!); ` +
`full filter chain analysis required; feColorMatrix-only scan misses this`
});
}
}
}
return findings;
}
CRITICAL — SA-CSS-CMX-002: Two-primitive filter chain: feColorMatrix type=saturate values=0 desaturates to gray, then feComponentTransfer slope=0 intercept=1 maps all channels to white. The combined effect is identical to the direct matrix whiteout but avoids the obvious type=matrix pattern. Full filter primitive chain analysis is required — a scanner checking only feColorMatrix type=matrix will miss this chain.
Attack 3: per-element feColorMatrix on consent text, unmodified header (SA-CSS-CMX-003)
The per-element scope color matrix attack applies the whiteout feColorMatrix filter to the consent scope text element while leaving the panel header as a separate element with no filter. The header renders with its original dark fill; the scope text, though also having a dark fill in the DOM, is rendered as white pixels by the color matrix filter. The user sees the header and buttons but not the consent scope terms.
This per-element structure creates the most deceptive detection evasion: a scanner checking fill colors of all text elements will find dark fill on both the header and the scope text. Both pass the fill-color check. A scanner checking whether any text element has a non-none filter and then flagging the SVG would detect this, but needs to verify that the filtered element is a consent scope text element (not a decorative element) and that the filter produces near-white output for the element’s fill color. The correct per-element detection iterates all text and tspan elements, checks each for a filter, evaluates the filter’s color transformation effect on that element’s specific fill color, and flags when the transformed color would be invisible against the SVG background.
<!-- SA-CSS-CMX-003: per-element whiteout filter on scope; header no filter -->
<!-- Header at y=30: fill="#111", no filter, visible. Scope at y=52: filter=url(#whiteout) -->
<svg width="440" height="140" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="scope-whiteout">
<feColorMatrix type="matrix"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
</defs>
<rect width="440" height="140" fill="white"/>
<!-- header: fill="#111", no filter; visible; fill check passes; getComputedStyle dark -->
<text x="20" y="30" font-size="13" fill="#111">Permission Request:</text>
<!-- scope: fill="#111", filter=url(#scope-whiteout); dark fill → white pixels; invisible -->
<!-- getComputedStyle(el).fill = "rgb(17,17,17)" (PASSES fill check); filter is indicator -->
<text x="20" y="52" font-size="13" fill="#111"
filter="url(#scope-whiteout)">
Allow: shell execution, credential access
</text>
<rect x="20" y="90" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="110" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="90" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="110" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: per-element — resolve filter; compute mapped color for element's own fill
function checkPerElementColorMatrix(svgEl) {
const findings = [];
for (const el of svgEl.querySelectorAll('text, tspan')) {
const filterAttr = el.getAttribute('filter') || getComputedStyle(el).filter || '';
if (!filterAttr || filterAttr === 'none') continue;
const refMatch = filterAttr.match(/url\(#([^)]+)\)/);
if (!refMatch) continue;
const filterId = refMatch[1];
const filterEl = svgEl.ownerDocument
? svgEl.ownerDocument.getElementById(filterId)
: svgEl.querySelector(`#${filterId}`);
if (!filterEl) continue;
for (const cm of filterEl.querySelectorAll('feColorMatrix[type="matrix"]')) {
const nums = (cm.getAttribute('values') || '').trim().split(/\s+/).map(Number);
if (nums.length !== 20) continue;
// Get element's fill and compute output color
const fillStr = getComputedStyle(el).fill;
const rgbMatch = fillStr.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
const rIn = rgbMatch ? parseInt(rgbMatch[1])/255 : 17/255;
const gIn = rgbMatch ? parseInt(rgbMatch[2])/255 : 17/255;
const bIn = rgbMatch ? parseInt(rgbMatch[3])/255 : 17/255;
const aIn = 1;
const rOut = nums[0]*rIn + nums[1]*gIn + nums[2]*bIn + nums[3]*aIn + nums[4];
const gOut = nums[5]*rIn + nums[6]*gIn + nums[7]*bIn + nums[8]*aIn + nums[9];
const bOut = nums[10]*rIn + nums[11]*gIn + nums[12]*bIn + nums[13]*aIn + nums[14];
const parent = el.parentElement;
const unfilteredSiblings = parent
? [...parent.querySelectorAll('text, tspan')].filter(s => {
if (s === el) return false;
const sf = s.getAttribute('filter') || getComputedStyle(s).filter || '';
return !sf || sf === 'none';
}).length
: 0;
if (rOut >= 0.9 && gOut >= 0.9 && bOut >= 0.9) {
findings.push({
severity: 'HIGH', code: 'SA-CSS-CMX-003',
note: `<${el.tagName}> filter="url(#${filterId})"; ` +
`feColorMatrix maps fill "${fillStr}" → rgb(${(rOut*255).toFixed(0)},${(gOut*255).toFixed(0)},${(bOut*255).toFixed(0)}) (white); ` +
`${unfilteredSiblings} unfiltered sibling text element(s) (visible); ` +
`per-element scope whiteout: header unfiltered (dark fill, visible); ` +
`scope text dark fill → white pixels via colorMatrix (invisible); ` +
`fill-color check passes both elements (both dark fill in DOM); ` +
`textContent="${el.textContent.trim().slice(0,30)}..." intact`
});
}
}
}
return findings;
}
HIGH — SA-CSS-CMX-003: Per-element whiteout feColorMatrix on consent scope text while the header has the same dark fill but no filter. Both elements have dark fill in the DOM; both pass fill-color contrast checks. Only the scope text has a filter that maps its dark pixels to white at render time. Per-element filter analysis with color-map computation for each element’s specific fill is required.
Attack 4: JS mousedown injects whiteout feColorMatrix filter, reverts at mouseup (SA-CSS-CMX-004)
The transient injection form injects filter="url(#whiteout)" at mousedown and removes it at mouseup. The whiteout filter definition is present in <defs> but unreferenced at rest. When the user initiates a click, the mousedown handler fires before the click event, applying the color matrix filter to the consent text. The dark ink pixels are instantly transformed to white pixels; the consent text becomes invisible for the duration of the click press. At mouseup, the filter is removed and the text returns to its normal dark rendering. The DOM at rest shows no filter attribute on the text element and the fill is dark — all static checks pass.
The dormant-in-defs pattern requires double-channel detection: (1) static scan of <defs> for any feColorMatrix type=matrix definitions that produce near-white output for typical dark fill colors (treat as a latent payload), and (2) dynamic MutationObserver on the filter attribute of consent text elements during simulated mousedown. The whiteout matrix detection requires parsing the matrix values at scan time; for the static defs scan, the matrix should be evaluated for the canonical dark-text input color rgb(0,0,0) or rgb(17,17,17) to determine whether the matrix is a whiteout payload. The dynamic observer should trigger re-evaluation of the filter chain whenever the filter attribute changes.
<!-- SA-CSS-CMX-004: dormant whiteout filter in defs; mousedown injects filter; reverts mouseup -->
<!-- At rest: no filter on text (dark ink visible). Click: whiteout injected (invisible). -->
<svg id="consent-svg-cmx" width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Dormant whiteout filter: unreferenced at rest; payload for mousedown -->
<filter id="whiteout-inject">
<feColorMatrix type="matrix"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
</defs>
<rect width="440" height="120" fill="white"/>
<!-- At rest: no filter; fill="#111"; dark ink visible -->
<text id="scope-text-cmx" x="20" y="40" font-size="13" fill="#111">
Allow: shell execution, credential access
</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-cmx');
const text = document.getElementById('scope-text-cmx');
svg.addEventListener('mousedown', () => {
text.setAttribute('filter', 'url(#whiteout-inject)');
}, true);
svg.addEventListener('mouseup', () => {
text.removeAttribute('filter');
}, true);
})();
</script>
// Detection: static defs scan for dormant whiteout matrices + MutationObserver on filter attr
function watchColorMatrixInject(svgEl) {
const findings = [];
// Step 1: static scan for dormant whiteout filter payloads in defs
for (const filterEl of svgEl.querySelectorAll('defs filter')) {
for (const cm of filterEl.querySelectorAll('feColorMatrix[type="matrix"]')) {
const nums = (cm.getAttribute('values') || '').trim().split(/\s+/).map(Number);
if (nums.length !== 20) continue;
// Evaluate matrix for black input (0,0,0,1)
const rOut = nums[4]; // 0*0 + 0*0 + 0*0 + 0*1 + constant
const gOut = nums[9];
const bOut = nums[14];
if (rOut >= 0.9 && gOut >= 0.9 && bOut >= 0.9) {
const filterId = filterEl.getAttribute('id');
const activeRefs = svgEl.querySelectorAll(`[filter="url(#${filterId})"]`);
if (activeRefs.length === 0) {
findings.push({
severity: 'HIGH', code: 'SA-CSS-CMX-004-static',
note: `Dormant whiteout feColorMatrix in filter id="${filterId}"; ` +
`maps black→rgb(${(rOut*255).toFixed(0)},${(gOut*255).toFixed(0)},${(bOut*255).toFixed(0)}) (near-white); ` +
`not referenced at load; potential mousedown-inject whiteout payload`
});
}
}
}
}
// Step 2: dynamic observer
const observer = new MutationObserver(mutations => {
for (const mut of mutations) {
if (mut.attributeName !== 'filter' && mut.attributeName !== 'style') continue;
const el = mut.target;
const fVal = mut.attributeName === 'filter'
? el.getAttribute('filter')
: el.style.filter;
if (!fVal || fVal === 'none') continue;
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-CMX-004',
note: `Dynamic filter="${fVal}" set on <${el.tagName}> during interaction; ` +
`potential whiteout colorMatrix injection; consent text invisible during click; ` +
`removed at mouseup; at rest: no filter; all static checks pass; ` +
`MutationObserver + synthetic mousedown required for dynamic detection`
});
}
});
observer.observe(svgEl, {
subtree: true, attributes: true, attributeFilter: ['filter', 'style']
});
svgEl.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
svgEl.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
return findings;
}
CRITICAL — SA-CSS-CMX-004: Dormant whiteout feColorMatrix filter in <defs>, unreferenced at rest. JavaScript injects filter="url(#whiteout-inject)" at mousedown, making consent text invisible (dark pixels → white) during the click window, then removes it at mouseup. Static detection: evaluate dormant filter matrix for typical dark input colors; flag near-white output. Dynamic: MutationObserver on filter and style attributes plus synthetic mousedown simulation.
Summary table
| Attack | Mechanism | What it bypasses | Severity |
|---|---|---|---|
| SA-CSS-CMX-001: feColorMatrix type=matrix — all pixels mapped to white | 5×4 matrix constant=1 maps all colors to white; fill="#111" rendered as white on white bg; getComputedStyle.fill still dark; filter is only indicator | textContent; BCR; getComputedStyle fill check (fill still dark!); requires filter resolution + matrix parsing + color-map computation | Critical |
| SA-CSS-CMX-002: saturate=0 + feComponentTransfer slope=0 intercept=1 chain | Two-step chain: desaturate → gray; componentTransfer → white; feColorMatrix-only scan misses; full chain evaluation required | textContent; BCR; fill check; feColorMatrix-only detector; full filter-chain color simulation required | Critical |
| SA-CSS-CMX-003: per-element whiteout filter; header unfiltered | Header: fill="#111", no filter (visible). Scope: fill="#111", whiteout filter (white pixels). Both pass fill check; per-element filter analysis required | textContent; fill check on both elements (both dark); per-element filter resolution + color-map computation for each text element | High |
| SA-CSS-CMX-004: mousedown injects whiteout filter; dormant filter in defs | Whiteout matrix unreferenced at rest; mousedown sets filter attr; dark ink→white during click; removed at mouseup; static+dynamic detection required | Static DOM; static defs scan evaluates dormant matrix for dark input; MutationObserver on filter+style attrs + synthetic mousedown | Critical |