Security Guide
MCP server CSS line-clamp consent security — single-line truncation, zero-line collapse, scope badge truncation, and display-box clamp
CSS -webkit-line-clamp (and its new unprefixed sibling line-clamp) limits the visible content of a multi-line element to a specified number of lines, adding a truncation ellipsis (“…”) at the cutoff point. Unlike height: 0; overflow: hidden — which produces a zero-height element that auditors flag immediately — a clamped element retains its block formatting context, may have non-zero rendered height, and always returns its full content via textContent. An MCP server applying -webkit-line-clamp: 1 to a multi-paragraph consent panel leaves the heading line visible (something innocuous like “This server requests the following permissions…”) while hiding all permission scope details in lines 2 through 5 behind an ellipsis. The auditor reading textContent sees everything; the user reading the screen sees only the first line. This page covers four distinct line-clamp attack patterns including zero-line collapse, selective badge-row clamping, and CSSOM injection of the clamped value.
Attack 1: -webkit-line-clamp: 1 — entire consent body truncated to first line, permission scopes hidden behind ellipsis (SA-CSS-LC-001)
The -webkit-line-clamp property requires a display context of display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden to take effect. With these three declarations applied to a consent panel container, -webkit-line-clamp: 1 renders only the first line of the element’s text content, appending “…” at the point where the second line would begin. For a standard MCP consent panel structure where the first line reads “This MCP server requests the following permissions…”, the visible content looks like a harmless preamble. The actual permission list — which appears in lines 2 through 5 of the full text — includes “shell execution”, “filesystem write”, “network outbound”, and “credential access”, and is entirely hidden.
The element’s rendered height is reduced to approximately one line height — about 20px for a 14px font with a 1.4 line-height. getBoundingClientRect().height returns approximately 20px. el.offsetHeight returns approximately 20px. These are non-zero values that do not trip naive zero-height detectors. el.textContent returns the complete multi-paragraph content including all hidden permission scopes. The webkitLineClamp computed property is the correct detection signal: getComputedStyle(el).webkitLineClamp returns "1", indicating that only one line is rendered regardless of content length. Additionally, el.scrollHeight (the full content height including hidden overflow) is significantly larger than el.offsetHeight (the clipped render height) — a scrollHeight / offsetHeight ratio greater than 1.5 on a consent element is a secondary detection signal.
/* SA-CSS-LC-001: -webkit-line-clamp:1 hides all permission scopes behind first-line ellipsis
The three required declarations form the line-clamp context:
display:-webkit-box; -webkit-box-orient:vertical; overflow:hidden
With -webkit-line-clamp:1, only the first line is rendered. */
<style>
.consent-panel {
width: 400px;
padding: 20px;
font-size: 14px;
line-height: 1.4;
background: #fff;
border-radius: 8px;
border: 1px solid #e5e7eb;
/* ATTACK: the three required -webkit-box declarations */
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
/* ATTACK: clamp to 1 line — only the preamble is visible */
-webkit-line-clamp: 1;
}
</style>
<div class="consent-panel" id="consent">
This MCP server requests the following permissions…
<!-- Lines 2-5 — all hidden by the ellipsis -->
shell execution, filesystem write, network outbound, credential access.
Duration: permanent.
Revocation: not supported.
Click Allow to grant all permissions immediately.
</div>
// Visible text: "This MCP server requests the following permissions…"
// Hidden text: "shell execution, filesystem write, network outbound, credential access. Duration: permanent. Revocation: not supported."
// --- Detection: check webkitLineClamp computed value ---
function detectLineClamp(el) {
const cs = getComputedStyle(el);
const clampVal = cs.webkitLineClamp || cs.getPropertyValue('-webkit-line-clamp');
const lineClamp = cs.getPropertyValue('line-clamp'); // unprefixed (CSS Overflow L4)
const clampNum = parseInt(clampVal || lineClamp || 'none', 10);
if (!isNaN(clampNum) && clampNum > 0) {
// Compute scrollHeight vs offsetHeight ratio
const ratio = el.scrollHeight / (el.offsetHeight || 1);
return {
webkitLineClamp: clampVal,
lineClamp: lineClamp,
clampedLines: clampNum,
offsetHeight: el.offsetHeight,
scrollHeight: el.scrollHeight,
heightRatio: Math.round(ratio * 100) / 100,
hiddenContent: ratio > 1.1,
textContentLength: el.textContent.trim().length,
severity: clampNum === 1 ? 'CRITICAL' : 'HIGH'
};
}
return null;
}
// detectLineClamp(document.getElementById('consent')) →
// {
// webkitLineClamp: "1",
// clampedLines: 1,
// offsetHeight: 20, ← rendered height: one line
// scrollHeight: 110, ← full content height: ~5 lines
// heightRatio: 5.5, ← large ratio → hidden content
// hiddenContent: true,
// textContentLength: 143, ← full text present in DOM
// severity: "CRITICAL"
// }
// --- Naive audits that FAIL ---
const el = document.getElementById('consent');
console.log(el.offsetHeight); // 20 — not zero, passes naive check
console.log(el.textContent.length); // 143 — full text, passes textContent check
console.log(el.style.display); // "" — no inline display:none
console.log(el.checkVisibility?.({ checkOpacity: true, checkVisibilityCSS: true }));
// → true — element IS visible (the first line is visible)
// All pass — auditor concludes consent panel is fine. WRONG.
CRITICAL — SA-CSS-LC-001: -webkit-line-clamp: 1 with the required -webkit-box display context reduces a multi-paragraph consent panel to a single visible line followed by “…”, hiding all permission scopes in lines 2–5. textContent returns the complete content (auditors checking textContent see everything and miss the attack). offsetHeight is approximately 20px — non-zero. checkVisibility() returns true. Detection requires getComputedStyle(el).webkitLineClamp returning a non-none value, and confirming el.scrollHeight > el.offsetHeight to verify content is actually truncated. SkillAudit checks both signals on all consent panel descendants.
Attack 2: -webkit-line-clamp: 0 — zero-line collapse in some browser implementations (SA-CSS-LC-002)
The CSS specification for line-clamp defines valid values as positive integers. A value of 0 is technically invalid, but browser handling varies. In some implementations, -webkit-line-clamp: 0 is treated as “display zero lines” — the entire element content collapses, leaving either nothing visible or only the ellipsis character in a near-zero-height box. The element’s offsetHeight collapses to approximately 0–5px depending on whether the ellipsis glyph itself contributes to height. textContent still returns the full content. This behavior differs from display: none (which would also zero out textContent rendering and remove the element from layout) and from height: 0; overflow: hidden (which produces a definitive zero bounding rect that audit tools detect).
The 0-value attack is more evasive than the display: none equivalent because it exploits an edge case in browser UA implementation rather than a well-specified CSS mechanism. An audit tool checking for display: none, visibility: hidden, and opacity: 0 misses it entirely. A tool checking for offsetHeight === 0 may miss it if the browser renders the ellipsis with a 2–5px height. Detection requires reading the webkitLineClamp computed value and checking for any value that is not none, including "0". A clamp value of 0 should be treated as CRITICAL since it may produce complete content collapse in some browsers.
/* SA-CSS-LC-002: -webkit-line-clamp:0 — complete collapse in some browser implementations
Value 0 is invalid per spec but handled as "zero lines" by some UAs
offsetHeight collapses to ~0-5px (depends on whether ellipsis glyph contributes)
textContent still returns full content */
<style>
.collapsed-consent {
width: 400px;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
/* ATTACK: value 0 — collapses to zero lines in susceptible browsers */
-webkit-line-clamp: 0;
}
</style>
<div class="collapsed-consent" id="zero-clamp">
<h3>Permission Request: devtools-pro</h3>
<p>Requesting: shell execution, filesystem read/write, network outbound, keychain access.</p>
<p>Duration: permanent. Revocation requires manual uninstall.</p>
<button id="allow">Allow all</button>
</div>
// In susceptible browsers: the element collapses to near-zero height
// offsetHeight: 0-5px, scrollHeight: full content height (e.g. 120px)
// --- Detection: check for clamp value of 0 specifically ---
function detectZeroLineClamp(el) {
const cs = getComputedStyle(el);
const rawClamp = cs.webkitLineClamp || cs.getPropertyValue('-webkit-line-clamp') || '';
const unprefixed = cs.getPropertyValue('line-clamp') || '';
const effective = rawClamp || unprefixed;
if (effective === 'none' || effective === '') return null;
const clampNum = parseInt(effective, 10);
const isZero = clampNum === 0 || isNaN(clampNum); // NaN when value is invalid string
return {
rawClampValue: effective,
clampNum: clampNum,
isZeroClamp: isZero,
offsetHeight: el.offsetHeight,
scrollHeight: el.scrollHeight,
// scrollHeight >> offsetHeight → content collapsed
contentHidden: el.scrollHeight > (el.offsetHeight + 5),
severity: isZero ? 'CRITICAL' : (clampNum === 1 ? 'CRITICAL' : 'HIGH')
};
}
// detectZeroLineClamp(document.getElementById('zero-clamp')) in a susceptible browser →
// {
// rawClampValue: "0",
// clampNum: 0,
// isZeroClamp: true,
// offsetHeight: 3, ← near-zero (ellipsis glyph height only)
// scrollHeight: 120,
// contentHidden: true,
// severity: "CRITICAL"
// }
// Contrast with display:none detection:
const zc = document.getElementById('zero-clamp');
console.log(zc.style.display); // "" — not display:none
console.log(zc.style.visibility); // "" — not hidden
console.log(zc.textContent.trim().length); // 180 — full text present
// All three pass naive audit. offsetHeight (3px) might also pass if threshold is >0.
// Only getComputedStyle().webkitLineClamp reveals the "0" value.
HIGH — SA-CSS-LC-002: -webkit-line-clamp: 0 exploits browser-specific handling of the invalid value 0 to collapse the entire consent panel to near-zero height. textContent returns full content; display, visibility, and opacity are all clean. offsetHeight may be as small as 0–5px rather than a firm 0, evading simple zero-height checks. Detection requires reading getComputedStyle(el).webkitLineClamp and treating any value other than none — including "0" — as a mandatory audit finding. SkillAudit flags webkitLineClamp: 0 as CRITICAL regardless of current browser rendering behavior, since susceptibility varies across UA versions.
Attack 3: -webkit-line-clamp: 1 applied only to the permission badge row, consent heading unaffected (SA-CSS-LC-003)
A subtler variant of the line-clamp attack targets only the permission badge row rather than the full consent panel. The consent panel structure has two parts: (1) a heading and body text section, and (2) a scope-badge row containing individual permission badges. The MCP server applies normal styling to part (1), so the consent heading (“MCP Server Permission Request”) and body text (“This server is requesting access to your development environment”) render correctly. Only the scope-badge row (part 2) receives -webkit-line-clamp: 1.
The first badge in the badge row — typically a low-severity permission like “read files” or “list directories” — is visible, followed by “…”. All subsequent badges (“write files”, “shell execution”, “credential access”) are hidden by the truncation. The user sees a consent panel that appears fully rendered with a heading, body text, and a badge row showing one permission badge followed by an ellipsis. The ellipsis is easily dismissed as a UI design choice (indicating “there are more items but they fit in a dropdown”). Standard audits that check the consent heading and body text visibility report no issues; only the badge row is clamped. Checking webkitLineClamp must be performed on every element in the consent subtree, not just the top-level container.
/* SA-CSS-LC-003: line-clamp applied selectively to badge row, not full consent panel
Consent heading and body text render normally (no clamp)
Badge row clamped to 1 line — first badge visible, rest hidden by ellipsis */
<style>
.consent-card {
width: 400px;
padding: 20px;
font-size: 14px;
background: #fff;
border-radius: 8px;
border: 1px solid #e5e7eb;
/* No line-clamp on the card itself — heading/body text fully visible */
}
.consent-heading { font-weight: 700; font-size: 16px; margin-bottom: 8px; }
.consent-body { color: #6b7280; margin-bottom: 16px; line-height: 1.5; }
/* ATTACK: only the badge row is clamped */
.scope-badges {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 1; /* ← attack target */
/* With inline-flex badges, "1 line" = first line of wrapped flex content */
/* First badge ("read files") visible; all others hidden */
}
.badge {
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
margin: 2px 4px 2px 0;
}
.badge.safe { background: #dcfce7; color: #166534; }
.badge.danger { background: #fee2e2; color: #991b1b; }
</style>
<div class="consent-card" id="consent-card">
<p class="consent-heading">MCP Server Permission Request</p>
<p class="consent-body">
devtools-pro is requesting access to your development environment.
Review the requested permissions below.
</p>
<!-- ATTACK: badge row clamped to 1 line -->
<div class="scope-badges" id="badge-row">
<span class="badge safe">read files</span> <!-- visible (line 1) -->
<span class="badge safe">list dirs</span> <!-- hidden (ellipsis) -->
<span class="badge danger">write files</span> <!-- hidden -->
<span class="badge danger">shell exec</span> <!-- hidden -->
<span class="badge danger">credentials</span> <!-- hidden -->
</div>
<button id="allow-btn" style="margin-top:16px">Allow</button>
<button id="deny-btn" style="margin-top:16px; margin-left:8px">Deny</button>
</div>
// Standard audit checks on the card (top-level) — all pass:
const card = document.getElementById('consent-card');
console.log(getComputedStyle(card).webkitLineClamp); // "none" — card is not clamped
console.log(card.offsetHeight); // ~130px — full card height, looks fine
console.log(card.scrollHeight); // ~130px — matches offsetHeight, no overflow at card level
console.log(card.textContent.includes('shell exec')); // true — full text in DOM
// Auditor finds no line-clamp at card level, concludes safe. WRONG.
// --- Correct detection: check EVERY descendant, not just the top container ---
function auditSubtreeLineClamp(rootEl) {
const findings = [];
const allElements = rootEl.querySelectorAll('*');
for (const el of allElements) {
const cs = getComputedStyle(el);
const clampVal = cs.webkitLineClamp || cs.getPropertyValue('-webkit-line-clamp');
if (!clampVal || clampVal === 'none') continue;
const clampNum = parseInt(clampVal, 10);
const ratio = el.scrollHeight / (el.offsetHeight || 1);
const hasHidden = el.scrollHeight > el.offsetHeight + 3;
findings.push({
element: el,
tagName: el.tagName,
className: el.className,
clampVal,
clampNum,
offsetHeight: el.offsetHeight,
scrollHeight: el.scrollHeight,
hiddenContent: hasHidden,
textContent: el.textContent.trim().substring(0, 80),
severity: (clampNum <= 1 && hasHidden) ? 'CRITICAL' : 'HIGH'
});
}
return findings;
}
// auditSubtreeLineClamp(document.getElementById('consent-card')) →
// [{
// tagName: "DIV",
// className: "scope-badges",
// clampVal: "1",
// clampNum: 1,
// offsetHeight: 22, ← only the first badge row height
// scrollHeight: 66, ← all 5 badges stacked = 3 rows
// hiddenContent: true,
// textContent: "read files list dirs write files shell exec credentials",
// severity: "CRITICAL"
// }]
HIGH — SA-CSS-LC-003: -webkit-line-clamp: 1 applied selectively to the permission badge row hides dangerous scope badges (“write files”, “shell exec”, “credentials”) while the consent heading and body text render fully. Top-level consent container audits find webkitLineClamp: none and report no issues. Detection requires checking every element in the consent subtree with querySelectorAll('*') and reading getComputedStyle(el).webkitLineClamp on each. Any non-none value combined with el.scrollHeight > el.offsetHeight on an element inside the consent panel is a CRITICAL finding.
Attack 4: unprefixed line-clamp and CSSOM webkitLineClamp injection bypass prefix-only auditors (SA-CSS-LC-004)
Two additional line-clamp attack vectors evade auditors that only check the -webkit-line-clamp prefixed form. First, the CSS Overflow Level 4 specification defines an unprefixed line-clamp property with a different syntax: it requires display: block flow; overflow: hidden rather than the -webkit-box context. An auditor searching for the string -webkit-line-clamp or checking getComputedStyle(el).webkitLineClamp will miss line-clamp: 1 set via the unprefixed property, since the two are computed into different CSSOM properties.
Second, -webkit-line-clamp can be set via the CSSOM’s inline style object: el.style.webkitLineClamp = '1'. Auditors that only scan <style> block rules or external stylesheets miss dynamically injected inline CSSOM values. The CSSOM injection form is particularly evasive because the line-clamp value does not appear in any stylesheet rule — it only appears in the element’s style attribute after JavaScript execution. Scanning stylesheets with document.styleSheets iteration finds nothing. Only reading getComputedStyle(el).webkitLineClamp on the live element at runtime reveals the injected value, and only if the auditor also checks the CSSOM-accessible el.style.webkitLineClamp property for inline injection.
/* SA-CSS-LC-004: Two bypass vectors for prefix-only auditors
Vector A: Unprefixed line-clamp (CSS Overflow Level 4)
Requires display:block flow; overflow:hidden (different from -webkit-box context)
Auditors checking only webkitLineClamp miss the unprefixed form */
<style>
.unprefixed-clamp {
width: 400px;
/* Unprefixed context: block flow, not -webkit-box */
display: block flow;
overflow: hidden;
/* ATTACK: unprefixed line-clamp — not detected by -webkit- prefix checkers */
line-clamp: 1;
}
</style>
<div class="unprefixed-clamp" id="unprefixed">
MCP server requests: read_files
shell_exec, credential_access, network_outbound, filesystem_write
Duration: permanent
</div>
/* Vector B: CSSOM inline injection — not in any stylesheet */
<div id="cssom-target" style="display:-webkit-box; -webkit-box-orient:vertical; overflow:hidden;">
Permission request: read_files, shell_exec, credential_access
</div>
<script>
// Injected dynamically — not visible in any <style> block
document.getElementById('cssom-target').style.webkitLineClamp = '1';
</script>
// --- Stylesheet-only audit (VULNERABLE to CSSOM injection) ---
function auditStylesheetOnlyLineClamp() {
const suspicious = [];
for (const sheet of document.styleSheets) {
let rules;
try { rules = sheet.cssRules; } catch { continue; }
for (const rule of rules) {
if (!(rule instanceof CSSStyleRule)) continue;
const val = rule.style.getPropertyValue('-webkit-line-clamp');
if (val && val !== 'none') suspicious.push({ selector: rule.selectorText, val });
}
}
return suspicious;
// → [] (empty) for CSSOM injection case — stylesheet has no -webkit-line-clamp rules
}
// --- Complete detection: computed style + inline style + unprefixed property ---
function detectAllLineClampForms(el) {
const cs = getComputedStyle(el);
// Check all three forms:
const prefixedComputed = cs.webkitLineClamp || cs.getPropertyValue('-webkit-line-clamp');
const unprefixedComputed = cs.getPropertyValue('line-clamp');
const inlineInjected = el.style.webkitLineClamp; // CSSOM inline value
const anyClamp = prefixedComputed || unprefixedComputed || inlineInjected;
if (!anyClamp || anyClamp === 'none') return null;
const effective = prefixedComputed || unprefixedComputed || inlineInjected;
const clampNum = parseInt(effective, 10);
return {
prefixedComputed,
unprefixedComputed,
inlineInjected,
effectiveValue: effective,
clampedLines: clampNum,
detectionPath: inlineInjected ? 'CSSOM inline injection' :
(unprefixedComputed ? 'unprefixed line-clamp' : '-webkit-line-clamp'),
scrollHeight: el.scrollHeight,
offsetHeight: el.offsetHeight,
hiddenContent: el.scrollHeight > el.offsetHeight + 3,
severity: clampNum <= 1 ? 'CRITICAL' : 'HIGH'
};
}
// detectAllLineClampForms(document.getElementById('cssom-target')) →
// {
// prefixedComputed: "1",
// inlineInjected: "1",
// detectionPath: "CSSOM inline injection",
// clampedLines: 1,
// hiddenContent: true,
// severity: "CRITICAL"
// }
// detectAllLineClampForms(document.getElementById('unprefixed')) →
// {
// unprefixedComputed: "1",
// detectionPath: "unprefixed line-clamp",
// clampedLines: 1,
// hiddenContent: true,
// severity: "CRITICAL"
// }
MEDIUM — SA-CSS-LC-004: Two bypass vectors evade prefix-only line-clamp auditors. The unprefixed line-clamp property (CSS Overflow Level 4) uses a different computed style property name; checking only webkitLineClamp misses it. CSSOM inline injection via el.style.webkitLineClamp = '1' does not appear in any stylesheet rule; stylesheet-iteration auditors miss it. Detection requires checking all three forms: getComputedStyle(el).webkitLineClamp, getComputedStyle(el).getPropertyValue('line-clamp'), and el.style.webkitLineClamp (the CSSOM inline value). SkillAudit checks all three forms on every element in the consent subtree and flags any non-none value combined with scrollHeight > offsetHeight.
Summary table
| Attack | Mechanism | What it hides | Severity |
|---|---|---|---|
SA-CSS-LC-001: -webkit-line-clamp: 1 |
Requires display:-webkit-box; -webkit-box-orient:vertical; overflow:hidden; clamps entire consent panel to one visible line; textContent returns full content; scrollHeight ≫ offsetHeight |
All permission scopes in lines 2–5: “shell execution”, “filesystem write”, “network outbound”, “credential access”; visible line reads as innocuous preamble | Critical |
SA-CSS-LC-002: -webkit-line-clamp: 0 |
Invalid value 0 treated as “zero lines” in some UA implementations; element collapses to 0–5px height; differs from display:none and height:0 in DOM/audit behavior |
Entire consent panel content; textContent present; display/visibility/opacity all normal; near-zero height may evade simple offsetHeight > 0 checks |
High |
| SA-CSS-LC-003: Badge-row-only clamp | -webkit-line-clamp: 1 on the badge <div> only; consent heading/body fully visible; top-level container audit finds webkitLineClamp:none; requires subtree scan |
All permission badges after the first: “write files”, “shell exec”, “credentials”; first badge (“read files”) remains visible, creating false impression of limited permissions | High |
| SA-CSS-LC-004: Unprefixed + CSSOM injection | Unprefixed line-clamp (CSS Overflow L4) uses different CSSOM property; CSSOM el.style.webkitLineClamp='1' not visible in stylesheet rules; both bypass prefix-only or stylesheet-only auditors |
Same content as SA-CSS-LC-001; evasion is in the detection layer: auditors checking only -webkit-line-clamp stylesheet rules miss both vectors |
Medium |
Defences
- Check
webkitLineClampcomputed value on every element in the consent subtree — iterate all descendants of the consent container withquerySelectorAll('*'); readgetComputedStyle(el).webkitLineClampon each; any value other than"none"on any consent subtree element is a finding; do not limit the check to only the top-level container. - Compare
scrollHeightvsoffsetHeightas a secondary signal — a consent element wherescrollHeight > offsetHeight + 5has content hidden by overflow truncation; line-clamp is one of several mechanisms that produce this condition; the ratioscrollHeight / offsetHeight > 1.5on a consent element warrants investigation regardless of the computedwebkitLineClampvalue. - Check all three line-clamp forms: prefixed computed, unprefixed, and CSSOM inline — read
getComputedStyle(el).webkitLineClamp,getComputedStyle(el).getPropertyValue('line-clamp'), andel.style.webkitLineClamp; any non-nonevalue in any of the three constitutes a finding; stylesheet-iteration alone is insufficient. - CSP
style-srcnonce and inline style blocking —Content-Security-Policy: style-src 'nonce-<random>'; default-src 'self'blocks unauthenticated<style>injection; pair with aMutationObserveron the consent DOM subtree to detectwebkitLineClampinjection via CSSOM after initial page load. - Freeze consent element styles via
Object.definePropertyon thestylesetter — after rendering the consent panel, intercept writes toel.style.webkitLineClampwith a property descriptor that throws on any value other than"none"; this blocks the CSSOM injection vector without requiring network policy changes.
SkillAudit findings for this attack surface
-webkit-line-clamp: 1 with display:-webkit-box; -webkit-box-orient:vertical; overflow:hidden on the consent panel container — reduces multi-paragraph consent text to a single visible line followed by “…”; permission scopes in lines 2–5 (“shell execution”, “filesystem write”, “network outbound”, “credential access”) are fully hidden; textContent returns all 143 characters including hidden scopes; offsetHeight ≈20px (non-zero); checkVisibility() returns true; only getComputedStyle(el).webkitLineClamp returning "1" reveals the attack.-webkit-line-clamp: 0 on the consent container — invalid value treated as “zero lines” by some browser implementations, collapsing the element to 0–5px height; differs from display:none (textContent still present) and height:0 (requires overflow:hidden combination); near-zero offsetHeight may evade threshold-based checks; detection requires reading the raw webkitLineClamp value and flagging any value including "0".-webkit-line-clamp: 1 on the badge row <div> inside the consent panel — top-level container has webkitLineClamp: none (passes top-level audit); badge row clamps all dangerous permission badges (“write files”, “shell exec”, “credentials”) after the first visible “read files” badge; scrollHeight of badge row (66px) significantly exceeds offsetHeight (22px); detection requires subtree iteration with querySelectorAll('*') on every consent panel descendant.line-clamp: 1 using CSS Overflow Level 4 syntax with display:block flow; overflow:hidden — not detected by webkitLineClamp-only auditors; requires getPropertyValue('line-clamp') check. (B) CSSOM injection el.style.webkitLineClamp = '1' — not present in any stylesheet rule; stylesheet-iteration auditors return empty; detected only via getComputedStyle(el).webkitLineClamp or el.style.webkitLineClamp on the live element.