feat: add green overlay, font toolbar, and text edit bar for active redactions

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Alex-GilbertandClaude Opus 4.6 committed 2026-02-16 15:30:37 -06:00
1 parent 3c867de6aa
commit bd42f32cb9
3 files changed
+520 -59

No files matched your search

+337 -59
View File
@@ -33,6 +33,26 @@ const solveStop = document.getElementById("solve-stop");
const solveAccept = document.getElementById("solve-accept");
const solveStatus = document.getElementById("solve-status");
const solveResults = document.getElementById("solve-results");
const fontToolbar = document.getElementById("font-toolbar");
const fontSelect = document.getElementById("font-select");
const sizeSlider = document.getElementById("size-slider");
const sizeValue = document.getElementById("size-value");
const sizeDown = document.getElementById("size-down");
const sizeUp = document.getElementById("size-up");
const posUp = document.getElementById("pos-up");
const posDown = document.getElementById("pos-down");
const posLeft = document.getElementById("pos-left");
const posRight = document.getElementById("pos-right");
const posReset = document.getElementById("pos-reset");
const posDisplay = document.getElementById("pos-display");
const gapDown = document.getElementById("gap-down");
const gapUp = document.getElementById("gap-up");
const gapValue = document.getElementById("gap-value");
const textEditBar = document.getElementById("text-edit-bar");
const leftTextInput = document.getElementById("left-text-input");
const rightTextInput = document.getElementById("right-text-input");
const redactionMarker = document.getElementById("redaction-marker");
const textReset = document.getElementById("text-reset");
// ── State ──
@@ -71,6 +91,14 @@ async function loadFonts() {
await Promise.all(promises);
state.fontsReady = true;
fontSelect.innerHTML = "";
for (const f of state.fonts.filter(f => f.available)) {
const opt = document.createElement("option");
opt.value = f.id;
opt.textContent = f.name;
fontSelect.appendChild(opt);
}
}
async function loadAssociates() {
@@ -312,6 +340,15 @@ async function analyzeRedaction(id) {
const data = await resp.json();
r.status = "analyzed";
r.analysis = data;
r.overrides = {
fontId: data.font.id,
fontSize: data.font.size,
offsetX: data.offset_x || 0,
offsetY: data.offset_y || 0,
gapWidth: data.gap.w,
leftText: data.segments.length > 0 ? data.segments[0].text : "",
rightText: data.segments.length > 1 ? data.segments[1].text : "",
};
renderRedactionList();
renderCanvas();
@@ -360,15 +397,122 @@ function openPopover(id) {
solveAccept.hidden = !!(r.preview === null);
popover.hidden = false;
fontToolbar.hidden = false;
fontSelect.value = r.overrides.fontId;
sizeSlider.value = r.overrides.fontSize;
sizeValue.textContent = r.overrides.fontSize;
gapValue.textContent = Math.round(r.overrides.gapWidth);
updatePosDisplay();
textEditBar.hidden = false;
leftTextInput.value = r.overrides.leftText;
rightTextInput.value = r.overrides.rightText;
redactionMarker.textContent = r.preview || "???";
redactionMarker.className = r.preview ? "redaction-marker preview" : "redaction-marker";
}
function closePopover() {
popover.hidden = true;
fontToolbar.hidden = true;
textEditBar.hidden = true;
stopSolve();
}
popoverClose.addEventListener("click", closePopover);
fontSelect.addEventListener("change", () => {
const r = state.redactions[state.activeRedaction];
if (!r?.overrides) return;
r.overrides.fontId = fontSelect.value;
renderCanvas();
});
sizeSlider.addEventListener("input", () => {
const r = state.redactions[state.activeRedaction];
if (!r?.overrides) return;
r.overrides.fontSize = parseInt(sizeSlider.value);
sizeValue.textContent = sizeSlider.value;
renderCanvas();
});
sizeDown.addEventListener("click", () => adjustSize(-1));
sizeUp.addEventListener("click", () => adjustSize(1));
function adjustSize(delta) {
const r = state.redactions[state.activeRedaction];
if (!r?.overrides) return;
r.overrides.fontSize = Math.max(8, Math.min(120, r.overrides.fontSize + delta));
sizeSlider.value = r.overrides.fontSize;
sizeValue.textContent = r.overrides.fontSize;
renderCanvas();
}
posUp.addEventListener("click", () => nudge(0, -1));
posDown.addEventListener("click", () => nudge(0, 1));
posLeft.addEventListener("click", () => nudge(-1, 0));
posRight.addEventListener("click", () => nudge(1, 0));
function nudge(dx, dy) {
const r = state.redactions[state.activeRedaction];
if (!r?.overrides) return;
r.overrides.offsetX += dx;
r.overrides.offsetY += dy;
updatePosDisplay();
renderCanvas();
}
function updatePosDisplay() {
const r = state.redactions[state.activeRedaction];
if (!r?.overrides) return;
posDisplay.textContent = `${Math.round(r.overrides.offsetX)}, ${Math.round(r.overrides.offsetY)}`;
}
posReset.addEventListener("click", () => {
const r = state.redactions[state.activeRedaction];
if (!r?.overrides || !r.analysis) return;
r.overrides.offsetX = r.analysis.offset_x || 0;
r.overrides.offsetY = r.analysis.offset_y || 0;
updatePosDisplay();
renderCanvas();
});
gapDown.addEventListener("click", () => adjustGap(-1));
gapUp.addEventListener("click", () => adjustGap(1));
function adjustGap(delta) {
const r = state.redactions[state.activeRedaction];
if (!r?.overrides) return;
r.overrides.gapWidth = Math.max(1, r.overrides.gapWidth + delta);
gapValue.textContent = Math.round(r.overrides.gapWidth);
renderCanvas();
}
leftTextInput.addEventListener("input", () => {
const r = state.redactions[state.activeRedaction];
if (!r?.overrides) return;
r.overrides.leftText = leftTextInput.value;
renderCanvas();
});
rightTextInput.addEventListener("input", () => {
const r = state.redactions[state.activeRedaction];
if (!r?.overrides) return;
r.overrides.rightText = rightTextInput.value;
renderCanvas();
});
textReset.addEventListener("click", () => {
const r = state.redactions[state.activeRedaction];
if (!r?.overrides || !r.analysis) return;
const a = r.analysis;
r.overrides.leftText = a.segments.length > 0 ? a.segments[0].text : "";
r.overrides.rightText = a.segments.length > 1 ? a.segments[1].text : "";
leftTextInput.value = r.overrides.leftText;
rightTextInput.value = r.overrides.rightText;
renderCanvas();
});
solveTolerance.addEventListener("input", () => {
solveTolValue.textContent = solveTolerance.value;
});
@@ -424,74 +568,205 @@ function drawRedactionUnanalyzed(r, isActive) {
}
function drawRedactionAnalyzed(r, isActive) {
const alpha = isActive ? 0.35 : 0.2;
const borderAlpha = isActive ? 0.9 : 0.5;
if (!isActive) {
// Non-active: just blue box like before
drawRedactionUnanalyzed(r, false);
return;
}
ctx.fillStyle = `rgba(66, 133, 244, ${alpha})`;
ctx.fillRect(r.x, r.y, r.w, r.h);
// Active: render green overlay with segments and gap
const a = r.analysis;
const o = r.overrides || {};
const fontName = state.fonts.find(f => f.id === (o.fontId ?? a.font.id))?.name ?? a.font.name;
const fontSize = o.fontSize ?? a.font.size;
const fontStr = `${fontSize}px "${fontName}"`;
const gapW = o.gapWidth ?? a.gap.w;
ctx.strokeStyle = `rgba(66, 133, 244, ${borderAlpha})`;
ctx.lineWidth = isActive ? 2.5 : 1.5;
ctx.strokeRect(r.x, r.y, r.w, r.h);
const startX = a.line.x + (o.offsetX ?? 0);
const startY = a.line.y + (o.offsetY ?? 0);
// Draw a small "?" label centered in the redaction
const labelSize = Math.min(r.h * 0.5, 18);
ctx.fillStyle = `rgba(255, 255, 255, ${isActive ? 0.8 : 0.5})`;
ctx.font = `bold ${labelSize}px sans-serif`;
ctx.textBaseline = "middle";
const label = "?";
const lw = ctx.measureText(label).width;
ctx.fillText(label, r.x + (r.w - lw) / 2, r.y + r.h / 2);
ctx.font = fontStr;
ctx.textBaseline = "top";
let cursorX = startX;
// Draw left segment in green
const leftText = o.leftText ?? "";
if (leftText) {
ctx.fillStyle = "rgba(0, 200, 0, 0.7)";
ctx.fillText(leftText, cursorX, startY);
cursorX += ctx.measureText(leftText).width;
}
// Draw redaction gap as red box
const pad = fontSize * 0.15;
ctx.fillStyle = "rgba(211, 47, 47, 0.5)";
ctx.fillRect(cursorX, startY - pad, gapW, fontSize + pad * 2);
ctx.strokeStyle = "rgba(211, 47, 47, 0.8)";
ctx.lineWidth = 2;
ctx.strokeRect(cursorX, startY - pad, gapW, fontSize + pad * 2);
// Gap width label
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
ctx.font = `bold ${Math.min(fontSize * 0.5, 16)}px sans-serif`;
const label = `${Math.round(gapW)}px`;
const labelW = ctx.measureText(label).width;
ctx.fillText(label, cursorX + (gapW - labelW) / 2, startY + fontSize * 0.3);
ctx.font = fontStr; // restore
cursorX += gapW;
// Draw right segment in green
const rightText = o.rightText ?? "";
if (rightText) {
ctx.fillStyle = "rgba(0, 200, 0, 0.7)";
ctx.fillText(rightText, cursorX, startY);
}
// Draw bounding box for the original line area
ctx.strokeStyle = "rgba(0, 200, 0, 0.3)";
ctx.lineWidth = 1;
ctx.strokeRect(a.line.x, a.line.y, a.line.w, a.line.h);
}
function drawRedactionPreview(r, isActive) {
if (!r.analysis) return;
const a = r.analysis;
const font = a.font;
const fontName = font.name;
const fontSize = font.size;
const o = r.overrides || {};
const fontName = state.fonts.find(f => f.id === (o.fontId ?? a.font.id))?.name ?? a.font.name;
const fontSize = o.fontSize ?? a.font.size;
const fontStr = `${fontSize}px "${fontName}"`;
const gapW = o.gapWidth ?? a.gap.w;
// Yellow highlight behind the gap area
const pad = fontSize * 0.1;
ctx.fillStyle = isActive ? "rgba(255, 200, 0, 0.2)" : "rgba(255, 200, 0, 0.12)";
ctx.fillRect(a.gap.x, r.y - pad, a.gap.w, r.h + pad * 2);
if (isActive) {
// Active: draw left green text, yellow gap with preview text, right green text
const startX = a.line.x + (o.offsetX ?? 0);
const startY = a.line.y + (o.offsetY ?? 0);
// Yellow border
ctx.strokeStyle = isActive ? "rgba(255, 200, 0, 0.8)" : "rgba(255, 200, 0, 0.5)";
ctx.lineWidth = isActive ? 2 : 1;
ctx.strokeRect(a.gap.x, r.y - pad, a.gap.w, r.h + pad * 2);
ctx.font = fontStr;
ctx.textBaseline = "top";
// Draw preview text
ctx.font = fontStr;
ctx.textBaseline = "top";
ctx.fillStyle = "rgba(255, 200, 0, 0.9)";
ctx.fillText(r.preview, a.gap.x, a.line.y);
let cursorX = startX;
// Draw left segment in green
const leftText = o.leftText ?? "";
if (leftText) {
ctx.fillStyle = "rgba(0, 200, 0, 0.7)";
ctx.fillText(leftText, cursorX, startY);
cursorX += ctx.measureText(leftText).width;
}
// Draw yellow gap with preview text
const pad = fontSize * 0.15;
ctx.fillStyle = "rgba(255, 200, 0, 0.2)";
ctx.fillRect(cursorX, startY - pad, gapW, fontSize + pad * 2);
ctx.strokeStyle = "rgba(255, 200, 0, 0.8)";
ctx.lineWidth = 2;
ctx.strokeRect(cursorX, startY - pad, gapW, fontSize + pad * 2);
// Preview text
ctx.fillStyle = "rgba(255, 200, 0, 0.9)";
ctx.font = fontStr;
ctx.fillText(r.preview, cursorX, startY);
cursorX += gapW;
// Draw right segment in green
const rightText = o.rightText ?? "";
if (rightText) {
ctx.fillStyle = "rgba(0, 200, 0, 0.7)";
ctx.fillText(rightText, cursorX, startY);
}
// Draw bounding box for the original line area
ctx.strokeStyle = "rgba(0, 200, 0, 0.3)";
ctx.lineWidth = 1;
ctx.strokeRect(a.line.x, a.line.y, a.line.w, a.line.h);
} else {
// Non-active: original behavior using gap position
const pad = fontSize * 0.1;
ctx.fillStyle = "rgba(255, 200, 0, 0.12)";
ctx.fillRect(a.gap.x, r.y - pad, gapW, r.h + pad * 2);
ctx.strokeStyle = "rgba(255, 200, 0, 0.5)";
ctx.lineWidth = 1;
ctx.strokeRect(a.gap.x, r.y - pad, gapW, r.h + pad * 2);
ctx.font = fontStr;
ctx.textBaseline = "top";
ctx.fillStyle = "rgba(255, 200, 0, 0.9)";
ctx.fillText(r.preview, a.gap.x, a.line.y);
}
}
function drawRedactionSolution(r, isActive) {
if (!r.analysis) return;
const a = r.analysis;
const font = a.font;
const fontName = font.name;
const fontSize = font.size;
const o = r.overrides || {};
const fontName = state.fonts.find(f => f.id === (o.fontId ?? a.font.id))?.name ?? a.font.name;
const fontSize = o.fontSize ?? a.font.size;
const fontStr = `${fontSize}px "${fontName}"`;
const gapW = o.gapWidth ?? a.gap.w;
// Green highlight
const pad = fontSize * 0.1;
ctx.fillStyle = isActive ? "rgba(0, 212, 116, 0.15)" : "rgba(0, 212, 116, 0.08)";
ctx.fillRect(a.gap.x, r.y - pad, a.gap.w, r.h + pad * 2);
if (isActive) {
// Active: draw left green text, green gap with solution text, right green text
const startX = a.line.x + (o.offsetX ?? 0);
const startY = a.line.y + (o.offsetY ?? 0);
// Green border
ctx.strokeStyle = isActive ? "rgba(0, 212, 116, 0.8)" : "rgba(0, 212, 116, 0.4)";
ctx.lineWidth = isActive ? 2 : 1;
ctx.strokeRect(a.gap.x, r.y - pad, a.gap.w, r.h + pad * 2);
ctx.font = fontStr;
ctx.textBaseline = "top";
// Draw solution text
ctx.font = fontStr;
ctx.textBaseline = "top";
ctx.fillStyle = "rgba(0, 212, 116, 0.95)";
ctx.fillText(r.solution.text, a.gap.x, a.line.y);
let cursorX = startX;
// Draw left segment in green
const leftText = o.leftText ?? "";
if (leftText) {
ctx.fillStyle = "rgba(0, 200, 0, 0.7)";
ctx.fillText(leftText, cursorX, startY);
cursorX += ctx.measureText(leftText).width;
}
// Draw green gap with solution text
const pad = fontSize * 0.15;
ctx.fillStyle = "rgba(0, 212, 116, 0.15)";
ctx.fillRect(cursorX, startY - pad, gapW, fontSize + pad * 2);
ctx.strokeStyle = "rgba(0, 212, 116, 0.8)";
ctx.lineWidth = 2;
ctx.strokeRect(cursorX, startY - pad, gapW, fontSize + pad * 2);
// Solution text
ctx.fillStyle = "rgba(0, 212, 116, 0.95)";
ctx.font = fontStr;
ctx.fillText(r.solution.text, cursorX, startY);
cursorX += gapW;
// Draw right segment in green
const rightText = o.rightText ?? "";
if (rightText) {
ctx.fillStyle = "rgba(0, 200, 0, 0.7)";
ctx.fillText(rightText, cursorX, startY);
}
// Draw bounding box for the original line area
ctx.strokeStyle = "rgba(0, 200, 0, 0.3)";
ctx.lineWidth = 1;
ctx.strokeRect(a.line.x, a.line.y, a.line.w, a.line.h);
} else {
// Non-active: original behavior using gap position
const pad = fontSize * 0.1;
ctx.fillStyle = "rgba(0, 212, 116, 0.08)";
ctx.fillRect(a.gap.x, r.y - pad, gapW, r.h + pad * 2);
ctx.strokeStyle = "rgba(0, 212, 116, 0.4)";
ctx.lineWidth = 1;
ctx.strokeRect(a.gap.x, r.y - pad, gapW, r.h + pad * 2);
ctx.font = fontStr;
ctx.textBaseline = "top";
ctx.fillStyle = "rgba(0, 212, 116, 0.95)";
ctx.fillText(r.solution.text, a.gap.x, a.line.y);
}
}
// ── Canvas hit-testing ──
@@ -601,7 +876,7 @@ zoomFitBtn.addEventListener("click", zoomToFit);
// Mouse-wheel zoom toward cursor
rightPanel.addEventListener("wheel", (e) => {
if (popover.contains(e.target)) return;
if (popover.contains(e.target) || fontToolbar.contains(e.target) || textEditBar.contains(e.target)) return;
e.preventDefault();
const rect = rightPanel.getBoundingClientRect();
const sx = e.clientX - rect.left;
@@ -612,7 +887,7 @@ rightPanel.addEventListener("wheel", (e) => {
// Double-click to zoom in
rightPanel.addEventListener("dblclick", (e) => {
if (popover.contains(e.target)) return;
if (popover.contains(e.target) || fontToolbar.contains(e.target) || textEditBar.contains(e.target)) return;
const rect = rightPanel.getBoundingClientRect();
const sx = e.clientX - rect.left;
const sy = e.clientY - rect.top;
@@ -625,7 +900,7 @@ let drag = null;
rightPanel.addEventListener("mousedown", (e) => {
if (e.button !== 0) return;
if (popover.contains(e.target)) return;
if (popover.contains(e.target) || fontToolbar.contains(e.target) || textEditBar.contains(e.target)) return;
drag = {
startX: e.clientX,
startY: e.clientY,
@@ -725,14 +1000,13 @@ function startSolve() {
if (!r || !r.analysis) return;
const a = r.analysis;
const fontId = a.font.id;
const fontSize = a.font.size;
const gapWidth = a.gap.w;
const o = r.overrides || {};
const fontId = o.fontId ?? a.font.id;
const fontSize = o.fontSize ?? a.font.size;
const gapWidth = o.gapWidth ?? a.gap.w;
// Context: last char before gap, first char after gap
const segs = a.segments;
const leftText = segs.length > 0 ? segs[0].text : "";
const rightText = segs.length > 1 ? segs[1].text : "";
const leftText = o.leftText ?? (a.segments.length > 0 ? a.segments[0].text : "");
const rightText = o.rightText ?? (a.segments.length > 1 ? a.segments[1].text : "");
const leftCtx = leftText.length > 0 ? leftText[leftText.length - 1] : "";
const rightCtx = rightText.length > 0 ? rightText[0] : "";
@@ -847,6 +1121,8 @@ function handleSolveEvent(data, redactionId) {
const r = state.redactions[redactionId];
if (!r) return;
r.preview = data.text;
redactionMarker.textContent = data.text;
redactionMarker.className = "redaction-marker preview";
renderCanvas();
solveResults.querySelectorAll(".solve-result").forEach(el => el.classList.remove("active"));
div.classList.add("active");
@@ -898,10 +1174,12 @@ function acceptSolution() {
if (!r || !r.preview) return;
r.status = "solved";
const o = r.overrides || {};
const solFontName = state.fonts.find(f => f.id === (o.fontId || r.analysis.font.id))?.name || r.analysis.font.name;
r.solution = {
text: r.preview,
fontName: r.analysis.font.name,
fontSize: r.analysis.font.size,
fontName: solFontName,
fontSize: o.fontSize || r.analysis.font.size,
};
r.preview = null;
+41
View File
@@ -38,6 +38,41 @@
</div>
<div id="right-panel">
<div id="font-toolbar" hidden>
<label>
Font
<select id="font-select"></select>
</label>
<span class="toolbar-sep"></span>
<div class="size-control">
<span>Size</span>
<button id="size-down" class="tb-btn">-</button>
<input type="range" id="size-slider" min="8" max="120" step="1">
<button id="size-up" class="tb-btn">+</button>
<span id="size-value">32</span>px
</div>
<span class="toolbar-sep"></span>
<div class="pos-control">
<span>Pos</span>
<div class="dpad">
<button id="pos-up" class="tb-btn dpad-up">&#9650;</button>
<div class="dpad-mid">
<button id="pos-left" class="tb-btn">&#9664;</button>
<span id="pos-display" class="pos-display">0, 0</span>
<button id="pos-right" class="tb-btn">&#9654;</button>
</div>
<button id="pos-down" class="tb-btn dpad-down">&#9660;</button>
</div>
<button id="pos-reset" class="tb-btn" style="font-size:0.65rem">Reset</button>
</div>
<span class="toolbar-sep"></span>
<div class="gap-control">
<span>Gap</span>
<button id="gap-down" class="tb-btn">-</button>
<span id="gap-value">0</span>px
<button id="gap-up" class="tb-btn">+</button>
</div>
</div>
<div id="popover" hidden>
<div class="popover-header">
<span class="popover-title">Analyze Redaction</span>
@@ -102,6 +137,12 @@
<img id="doc-image" alt="Document page">
<canvas id="overlay-canvas"></canvas>
</div>
<div id="text-edit-bar" hidden>
<input type="text" id="left-text-input" class="seg-input" spellcheck="false" autocomplete="off" placeholder="left text">
<span id="redaction-marker" class="redaction-marker">???</span>
<input type="text" id="right-text-input" class="seg-input" spellcheck="false" autocomplete="off" placeholder="right text">
<button id="text-reset" class="tb-btn" title="Reset to OCR">Reset</button>
</div>
</div>
</div>
</section>
+142
View File
@@ -544,3 +544,145 @@ main {
font-size: 0.7rem;
margin-top: 2px;
}
/* Font toolbar */
#font-toolbar {
position: absolute;
top: 0.5rem;
left: 0.5rem;
right: 340px;
z-index: 20;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.6rem;
background: rgba(22, 33, 62, 0.95);
backdrop-filter: blur(6px);
border-radius: 6px;
border: 1px solid #0f3460;
font-size: 0.75rem;
color: #aaa;
flex-wrap: wrap;
}
#font-toolbar label {
display: flex;
align-items: center;
gap: 4px;
}
#font-toolbar select {
background: #0a1628;
color: #e0e0e0;
border: 1px solid #1a3a6e;
border-radius: 3px;
padding: 2px 4px;
font-size: 0.75rem;
}
.toolbar-sep {
width: 1px;
height: 1.2rem;
background: #0f3460;
flex-shrink: 0;
}
.tb-btn {
background: #0f3460;
color: #e0e0e0;
border: 1px solid #1a3a6e;
border-radius: 3px;
width: 1.4rem;
height: 1.4rem;
cursor: pointer;
font-size: 0.7rem;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.tb-btn:hover {
background: #1a3a6e;
}
.size-control, .pos-control, .gap-control {
display: flex;
align-items: center;
gap: 4px;
}
#size-slider {
width: 60px;
accent-color: #00d474;
}
.dpad {
display: flex;
flex-direction: column;
align-items: center;
gap: 1px;
}
.dpad-mid {
display: flex;
align-items: center;
gap: 1px;
}
.pos-display {
font-size: 0.65rem;
color: #00d474;
min-width: 3rem;
text-align: center;
}
/* Text edit bar */
#text-edit-bar {
position: absolute;
bottom: 0.5rem;
left: 0.5rem;
right: 0.5rem;
z-index: 20;
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.4rem 0.6rem;
background: rgba(22, 33, 62, 0.95);
backdrop-filter: blur(6px);
border-radius: 6px;
border: 1px solid #0f3460;
font-size: 0.8rem;
}
.seg-input {
flex: 1;
min-width: 3rem;
background: #0a1628;
color: #e0e0e0;
border: 1px solid #1a3a6e;
border-radius: 4px;
padding: 0.3rem 0.4rem;
font-family: monospace;
font-size: 0.85rem;
}
.seg-input:focus {
outline: none;
border-color: #00d474;
}
.redaction-marker {
flex-shrink: 0;
padding: 0.25rem 0.4rem;
background: #d32f2f;
color: #fff;
font-size: 0.65rem;
font-weight: 700;
border-radius: 3px;
}
.redaction-marker.preview {
background: #b8860b;
color: #ffe082;
}