diff --git a/app/logic/document_outputs.py b/app/logic/document_outputs.py index 28a33a6..5cc26b6 100644 --- a/app/logic/document_outputs.py +++ b/app/logic/document_outputs.py @@ -67,6 +67,7 @@ from reportlab.lib.utils import ImageReader from reportlab.pdfbase.pdfmetrics import stringWidth from reportlab.pdfgen import canvas from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont from sqlalchemy import func from sqlalchemy.orm import Session @@ -368,7 +369,32 @@ def _fit_font_size_for_bbox_text(text: str, box_width: float, box_height: float) def _safe_pdf_font_name(font_name: str | None) -> str: - candidate = (font_name or "Helvetica").strip() + raw = (font_name or "Helvetica").strip() + key = raw.lower() + + # ReportLab core PDF fonts only unless TTFs are registered. + # Map common UI font names to visible built-in PDF fonts. + aliases = { + "arial": "Helvetica", + "calibri": "Helvetica", + "verdana": "Helvetica", + "trebuchet ms": "Helvetica", + "helvetica": "Helvetica", + + "times new roman": "Times-Roman", + "times": "Times-Roman", + "georgia": "Times-Roman", + "cambria": "Times-Roman", + "liberation serif": "Times-Roman", + "dejavu serif": "Times-Roman", + + "courier new": "Courier", + "courier": "Courier", + "liberation mono": "Courier", + "dejavu sans mono": "Courier", + } + + candidate = aliases.get(key, raw) try: pdfmetrics.getFont(candidate) return candidate @@ -380,8 +406,9 @@ def _font_size_for_box(text: str, font_name: str, box_width: float, box_height: fitted = _fit_font_size_for_bbox_text(text, box_width, box_height) if saved_size and saved_size > 0: - # Saved UI/editor font size is allowed, but geometry wins for replica output. - return max(1.0, min(float(saved_size), float(fitted))) + # Layout Review is the source of truth after manual editing. + # Do not silently shrink manual font edits back to the fitted estimate. + return max(1.0, float(saved_size)) return max(1.0, float(fitted)) @@ -993,6 +1020,12 @@ def build_replica_layout(document: Document, mode: str = "shared") -> dict: current_file, raw_ocr, reviewed, source_layout, layout_source = _get_replica_source_context(document) reader = PdfReader(str(current_file)) + source_layout_meta = source_layout if isinstance(source_layout, dict) else {} + prefer_word_entries = ( + source_layout_meta.get("layout_sync_source") == "layout_review" + or source_layout_meta.get("layout_sync_status") == "synced" + ) + pages = [] page_layouts = {page["page"]: page for page in (source_layout.get("pages", []) if isinstance(source_layout, dict) else [])} @@ -1066,6 +1099,7 @@ def build_replica_layout(document: Document, mode: str = "shared") -> dict: "page_height": page_h, "image_width": src_w, "image_height": src_h, + "prefer_word_entries": prefer_word_entries, "lines": line_entries, "words": page_layout.get("words", []) or [], } @@ -1156,8 +1190,35 @@ def _render_replica_pdf_from_layout( page_layout = pages.get(page_num, {"lines": []}) + edited_words = [ + w for w in (page_layout.get("words") or []) + if (isinstance(w.get("manual_flags"), dict) and w.get("manual_flags", {}).get("style_edited")) + or str(w.get("text_color_guess") or "#000000").lower() != "#000000" + ] + if edited_words: + print( + "[replica-render-debug]", + "page=", page_num, + "prefer_word_entries=", page_layout.get("prefer_word_entries"), + "edited_words=", + [ + ( + w.get("id"), + w.get("text"), + w.get("font_size_guess"), + w.get("font_family_guess"), + w.get("text_color_guess"), + w.get("manual_flags"), + ) + for w in edited_words[:20] + ], + flush=True, + ) + render_entries = [] - if page_layout.get("lines"): + if page_layout.get("prefer_word_entries") and page_layout.get("words"): + render_entries = _build_word_entries_for_page(page_layout, page_h) + if not render_entries and page_layout.get("lines"): render_entries = _build_line_entries_for_page(page_layout, page_h) if not render_entries and page_layout.get("words"): render_entries = _build_word_entries_for_page(page_layout, page_h) @@ -1189,8 +1250,18 @@ def _render_replica_pdf_from_layout( c.setStrokeColorRGB(1, 0, 0) c.setFillColorRGB(1, 0, 0) else: - c.setStrokeColorRGB(0, 0, 0) - c.setFillColorRGB(0, 0, 0) + color = str(line.get("text_color_guess") or "#000000").lstrip("#") + try: + if len(color) == 6: + r = int(color[0:2], 16) / 255.0 + g = int(color[2:4], 16) / 255.0 + b = int(color[4:6], 16) / 255.0 + else: + r = g = b = 0 + except Exception: + r = g = b = 0 + c.setStrokeColorRGB(r, g, b) + c.setFillColorRGB(r, g, b) text_obj.textLine(text_line) c.drawText(text_obj) diff --git a/app/models/__init__.py b/app/models/__init__.py index ab7e0db..d11d447 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,4 +1,5 @@ from app.models.document import Document +from app.models.document_review_state import DocumentReviewState from app.models.document_version import DocumentVersion from app.models.text_version import TextVersion from app.models.extracted_field import ExtractedField @@ -8,6 +9,8 @@ from app.models.document_additional_field import DocumentAdditionalField from app.models.document_preset import DocumentPreset __all__ = [ + "DocumentReplicaReviewState", + "DocumentReviewState", "DocumentAnalysisVersion", "Document", "DocumentVersion", diff --git a/app/routes/documents.py b/app/routes/documents.py index 39bb27b..bc567aa 100644 --- a/app/routes/documents.py +++ b/app/routes/documents.py @@ -19,7 +19,7 @@ from io import BytesIO from fastapi import APIRouter, Depends, Form, Query, Request from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse, Response from fastapi.templating import Jinja2Templates -from sqlalchemy import distinct +from sqlalchemy import distinct, text from sqlalchemy import func from sqlalchemy.orm import Session, selectinload from pypdf import PdfReader @@ -2023,12 +2023,30 @@ async def save_layout_review(document_id: str, request: Request, db: Session = D x_right = max(x1, x2) y_top = min(y1, y2) y_bottom = max(y1, y2) - if abs(x_right - x_left) < 1.0 or abs(y_bottom - y_top) < 1.0: continue - font_size_guess = float(word.get("font_size_guess") or max(6.0, (y_bottom - y_top) * 0.75)) - font_family_guess = (word.get("font_family_guess") or "Helvetica") + manual_flags = word.get("manual_flags") if isinstance(word.get("manual_flags"), dict) else {} + style_edited = manual_flags.get("style_edited") is True + override_style = word.get("override_style") if style_edited and isinstance(word.get("override_style"), dict) else {} + resolved_style = word.get("resolved_style") if style_edited and isinstance(word.get("resolved_style"), dict) else {} + + font_size_guess = float(word.get("font_size_guess") or override_style.get("font_size") or max(6.0, (y_bottom - y_top) * 0.75)) + font_family_guess = word.get("font_family_guess") or override_style.get("font_family") or "Helvetica" + font_weight_guess = int(word.get("font_weight_guess") or resolved_style.get("font_weight") or 400) + font_style_guess = word.get("font_style_guess") or resolved_style.get("font_style") or "normal" + letter_spacing_guess = float(word.get("letter_spacing_guess") or resolved_style.get("letter_spacing") or 0) + text_color_guess = word.get("text_color_guess") or override_style.get("text_color") or "#000000" + + if style_edited: + override_style = dict(override_style) + override_style.update({"font_family": font_family_guess, "font_size": font_size_guess, "text_color": text_color_guess}) + resolved_style = dict(resolved_style) + resolved_style.update(override_style) + else: + override_style = {} + resolved_style = {} + manual_flags["style_edited"] = False words.append({ "id": int(word.get("id") or word_idx), @@ -2037,6 +2055,13 @@ async def save_layout_review(document_id: str, request: Request, db: Session = D "confidence": None, "font_size_guess": font_size_guess, "font_family_guess": font_family_guess, + "font_weight_guess": font_weight_guess, + "font_style_guess": font_style_guess, + "letter_spacing_guess": letter_spacing_guess, + "text_color_guess": text_color_guess, + "override_style": override_style, + "resolved_style": resolved_style, + "manual_flags": manual_flags, }) words.sort(key=lambda w: (w["bbox"][1], w["bbox"][0])) @@ -2186,12 +2211,27 @@ def document_detail(document_id: str, request: Request, queue: str | None = None bbox = word.get("bbox") or [0, 0, 0, 0] if not isinstance(bbox, (list, tuple)) or len(bbox) != 4: continue + resolved_style = word.get("resolved_style") if isinstance(word.get("resolved_style"), dict) else {} + override_style = word.get("override_style") if isinstance(word.get("override_style"), dict) else {} + inferred_style = word.get("inferred_style") if isinstance(word.get("inferred_style"), dict) else {} + manual_flags = word.get("manual_flags") if isinstance(word.get("manual_flags"), dict) else {} + font_size_value = word.get("font_size_guess") or override_style.get("font_size") or resolved_style.get("font_size") or max(6.0, (float(bbox[3]) - float(bbox[1])) * 0.75) + font_family_value = word.get("font_family_guess") or override_style.get("font_family") or resolved_style.get("font_family") or "Helvetica" + text_color_value = word.get("text_color_guess") or override_style.get("text_color") or resolved_style.get("text_color") or "#000000" word_row = { "id": idx, "text": (word.get("text") or "").strip(), "bbox": [float(bbox[0]), float(bbox[1]), float(bbox[2]), float(bbox[3])], - "font_size_guess": float(word.get("font_size_guess") or max(6.0, (float(bbox[3]) - float(bbox[1])) * 0.75)), - "font_family_guess": (word.get("font_family_guess") or "Helvetica"), + "font_size_guess": float(font_size_value), + "font_family_guess": font_family_value, + "font_weight_guess": int(word.get("font_weight_guess") or resolved_style.get("font_weight") or 400), + "font_style_guess": word.get("font_style_guess") or resolved_style.get("font_style") or "normal", + "letter_spacing_guess": float(word.get("letter_spacing_guess") or resolved_style.get("letter_spacing") or 0), + "text_color_guess": text_color_value, + "inferred_style": inferred_style, + "override_style": override_style, + "resolved_style": resolved_style, + "manual_flags": manual_flags, } words.append(word_row) @@ -3293,78 +3333,151 @@ async def run_diagnostic_candidates(document_id: str, db: Session = Depends(get_ ) -@router.get("/{document_id}/diagnostic-output/{output_id}/download") -async def download_diagnostic_output(document_id: str, output_id: int, db: Session = Depends(get_db)): - document = db.query(Document).filter(Document.document_id == document_id).first() - if document is None: - return HTMLResponse(content="Document not found", status_code=404) - row = db.execute( - text(""" - SELECT file_path, engine, output_type, version_number - FROM document_diagnostic_outputs - WHERE id = :id AND document_id = :document_id - """), - {"id": output_id, "document_id": document.id}, - ).mappings().first() +@router.get("/{document_id}/diagnostic-output/{output_id}/download") +async def download_diagnostic_output(document_id: str, output_id: int): + with engine.connect() as conn: + row = conn.execute( + text(""" + SELECT ddo.file_path, ddo.engine, ddo.output_type, ddo.version_number + FROM document_diagnostic_outputs ddo + JOIN documents d ON d.id = ddo.document_id + WHERE ddo.id = :id AND d.document_id = :document_id + """), + {"id": output_id, "document_id": document_id}, + ).mappings().first() if not row or not row["file_path"]: return HTMLResponse(content="Diagnostic output not found", status_code=404) path = Path(row["file_path"]) if not path.exists(): - return HTMLResponse(content="Diagnostic output file missing", status_code=404) + return HTMLResponse(content=f"Diagnostic output file missing: {path}", status_code=404) return FileResponse(path=str(path), filename=path.name) @router.post("/{document_id}/diagnostic-output/{output_id}/select") -async def select_diagnostic_output(document_id: str, output_id: int, db: Session = Depends(get_db)): - document = db.query(Document).filter(Document.document_id == document_id).first() - if document is None: - return HTMLResponse(content="Document not found", status_code=404) +async def select_diagnostic_output(document_id: str, output_id: int): + with engine.begin() as conn: + row = conn.execute( + text(""" + SELECT ddo.id, ddo.document_id, ddo.engine, ddo.output_type + FROM document_diagnostic_outputs ddo + JOIN documents d ON d.id = ddo.document_id + WHERE ddo.id = :id AND d.document_id = :document_id + """), + {"id": output_id, "document_id": document_id}, + ).mappings().first() - row = db.execute( - text(""" - SELECT engine, output_type - FROM document_diagnostic_outputs - WHERE id = :id AND document_id = :document_id - """), - {"id": output_id, "document_id": document.id}, - ).mappings().first() + if not row: + return HTMLResponse(content="Diagnostic output not found", status_code=404) - if not row: - return HTMLResponse(content="Diagnostic output not found", status_code=404) + conn.execute( + text(""" + UPDATE document_diagnostic_outputs + SET is_selected = false + WHERE document_id = :document_pk + AND engine = :engine + AND output_type = :output_type + """), + { + "document_pk": row["document_id"], + "engine": row["engine"], + "output_type": row["output_type"], + }, + ) - db.execute( - text(""" - UPDATE document_diagnostic_outputs - SET is_selected = false - WHERE document_id = :document_id - AND engine = :engine - AND output_type = :output_type - """), - { - "document_id": document.id, - "engine": row["engine"], - "output_type": row["output_type"], - }, - ) - - db.execute( - text(""" - UPDATE document_diagnostic_outputs - SET is_selected = true, updated_at = NOW() - WHERE id = :id AND document_id = :document_id - """), - {"id": output_id, "document_id": document.id}, - ) - - db.commit() + conn.execute( + text(""" + UPDATE document_diagnostic_outputs + SET is_selected = true, updated_at = NOW() + WHERE id = :id + """), + {"id": output_id}, + ) return RedirectResponse( url=f"/documents/{document_id}?tab=ocr-review&success=diagnostic_candidate_selected", status_code=303, ) + + + +@router.post("/{document_id}/diagnostic-output/select") +async def select_diagnostic_output_from_form(document_id: str, diagnostic_output_id: int = Form(...)): + return await select_diagnostic_output(document_id, diagnostic_output_id) + + +@router.get("/{document_id}/diagnostic-output/{output_id}/view") +async def view_diagnostic_output(document_id: str, output_id: int): + with engine.connect() as conn: + row = conn.execute( + text(""" + SELECT ddo.file_path, ddo.engine, ddo.output_type, ddo.version_number + FROM document_diagnostic_outputs ddo + JOIN documents d ON d.id = ddo.document_id + WHERE ddo.id = :id AND d.document_id = :document_id + """), + {"id": output_id, "document_id": document_id}, + ).mappings().first() + + if not row or not row["file_path"]: + return HTMLResponse(content="Diagnostic output not found", status_code=404) + + path = Path(row["file_path"]) + if not path.exists(): + return HTMLResponse(content=f"Diagnostic output file missing: {path}", status_code=404) + + suffix = path.suffix.lower() + + if suffix == ".pdf": + return FileResponse(path=str(path), filename=path.name, media_type="application/pdf") + + if suffix == ".docx": + with open(path, "rb") as f: + result = mammoth.convert_to_html(f) + + body = result.value or "" + return HTMLResponse(content=f""" + + + + + + + + +
+ {body} +
+ + +""") + + return FileResponse(path=str(path), filename=path.name) + + # --- diagnostic candidate routes end --- diff --git a/app/templates/documents/detail.html b/app/templates/documents/detail.html index a3be104..a8b25c6 100644 --- a/app/templates/documents/detail.html +++ b/app/templates/documents/detail.html @@ -1,3 +1,12 @@ + + + {% extends "base.html" %} {% block title %}Document Detail @@ -263,56 +316,6 @@ document.addEventListener("DOMContentLoaded", () => { -
-
-

Diagnostic candidates

-
- -
-
- - {% if diagnostic_outputs %} -
- {% for out in diagnostic_outputs %} -
-
-
-
- {% if out.is_selected %}✓ {% endif %}{{ out.engine }} · {{ out.output_type }} -
-
- v{{ out.version_number }} · {{ out.status }} -
-
-
- {{ out.created_at }} -
-
- - {% if out.error_message %} -
- {{ out.error_message }} -
- {% endif %} - -
- {% if out.file_path %} - Download -
- -
- {% else %} - No file output - {% endif %} -
-
- {% endfor %} -
- {% else %} -

No diagnostic candidates saved yet.

- {% endif %} -
-
@@ -351,7 +354,63 @@ document.addEventListener("DOMContentLoaded", () => { Words
- {% endif %} +
+
+ Diagnostics: +
+ +
+ + {% set selected_diag = diagnostic_outputs | selectattr("is_selected") | list | first %} + {% set default_diag = selected_diag or (diagnostic_outputs | selectattr("file_path") | list | first) %} + + {% if diagnostic_outputs %} +
+ + +
+
+ + View + Download + + + {% else %} + none + {% endif %} +
+ +{% endif %}
@@ -614,24 +673,1324 @@ document.addEventListener("DOMContentLoaded", () => { gap:0.35rem; align-items:center; } - -
- - - - - - - - - - 100% - - Ready -
-
+ + /* Word-like floating mini toolbar for selected word */ + #layout-word-popover { + width: min(92vw, 30rem) !important; + padding: 0.55rem !important; + border: 1px solid #b8c6d8 !important; + border-radius: 0.65rem !important; + background: linear-gradient(#f8fbff, #eef4fc) !important; + box-shadow: 0 0.7rem 1.8rem rgba(15,23,42,0.18) !important; + } + + #layout-word-popover > div:first-child { + margin-bottom: 0.3rem !important; + font-size: 0.72rem !important; + color: #475569 !important; + text-transform: uppercase !important; + letter-spacing: 0.03em !important; + } + + #layout-word-popover input[type="text"], + #layout-word-popover input[type="number"] { + min-height: 1.75rem !important; + padding: 0.2rem 0.45rem !important; + border: 1px solid #cbd5e1 !important; + border-radius: 0.35rem !important; + font-size: 0.8rem !important; + background: #ffffff !important; + } + + #layout-word-popover input[type="color"] { + width: 1.55rem !important; + height: 1.55rem !important; + padding: 0.04rem !important; + border: 1px solid #94a3b8 !important; + border-radius: 0.25rem !important; + } + + #layout-word-popover label { + font-size: 0.72rem !important; + color: #475569 !important; + white-space: nowrap !important; + } + + #layout-word-popover button { + min-height: 1.75rem !important; + padding: 0.22rem 0.55rem !important; + border: 1px solid #b8c6d8 !important; + border-radius: 0.35rem !important; + background: linear-gradient(#ffffff, #e9eef7) !important; + color: #1f2937 !important; + font-size: 0.76rem !important; + } + + #layout-popover-apply { + background: linear-gradient(#dbeafe, #bfdbfe) !important; + border-color: #60a5fa !important; + color: #1e3a8a !important; + } + + #layout-popover-delete { + background: linear-gradient(#fff1f2, #ffe4e6) !important; + border-color: #fda4af !important; + color: #9f1239 !important; + } + + #layout-word-popover .layout-mini-grid { + grid-template-columns: 4rem auto 4.8rem auto auto auto !important; + gap: 0.28rem !important; + align-items: center !important; + } + + + /* Floating selected-word mini toolbar: Word-like font group */ + #layout-word-popover { + width: min(94vw, 34rem) !important; + padding: 0.55rem 0.65rem !important; + border: 1px solid #b8c6d8 !important; + border-radius: 0.65rem !important; + background: linear-gradient(#f8fbff, #eef4fc) !important; + box-shadow: 0 0.7rem 1.8rem rgba(15,23,42,0.18) !important; + } + + #layout-word-popover > div:first-child { + font-size: 0.72rem !important; + font-weight: 700 !important; + text-transform: uppercase !important; + letter-spacing: 0.04em !important; + color: #475569 !important; + margin-bottom: 0.35rem !important; + } + + #layout-word-popover > div:nth-child(2) { + display: grid !important; + grid-template-columns: 1.7fr 1fr auto auto auto auto auto !important; + gap: 0.3rem !important; + align-items: center !important; + } + + #layout-word-popover input[type="text"], + #layout-word-popover input[type="number"] { + min-height: 2rem !important; + padding: 0.25rem 0.45rem !important; + border: 1px solid #cbd5e1 !important; + border-radius: 0.32rem !important; + font-size: 0.86rem !important; + background: #ffffff !important; + } + + #layout-word-popover label { + font-size: 0.72rem !important; + color: #475569 !important; + white-space: nowrap !important; + } + + #layout-word-popover input[type="color"] { + width: 2rem !important; + height: 2rem !important; + padding: 0.04rem !important; + border: 1px solid #94a3b8 !important; + border-radius: 0.25rem !important; + } + + #layout-word-popover button { + min-height: 2rem !important; + padding: 0.25rem 0.55rem !important; + border: 1px solid #b8c6d8 !important; + border-radius: 0.32rem !important; + background: linear-gradient(#ffffff, #e9eef7) !important; + color: #1f2937 !important; + font-size: 0.82rem !important; + font-weight: 600 !important; + } + + #layout-word-popover .layout-mini-grid { + display: contents !important; + } + + #layout-popover-apply { + background: linear-gradient(#dbeafe, #bfdbfe) !important; + border-color: #60a5fa !important; + color: #1e3a8a !important; + } + + #layout-popover-delete { + background: linear-gradient(#fff1f2, #ffe4e6) !important; + border-color: #fda4af !important; + color: #9f1239 !important; + } + + @media (max-width: 760px) { + #layout-review-toolbar { + grid-template-columns: repeat(4, auto) !important; + } + + #layout-word-popover > div:nth-child(2) { + grid-template-columns: 1fr 4.5rem auto auto !important; + } + } + + + /* Word-like selected-word mini toolbar, compact not huge */ + #layout-word-popover { + width: min(92vw, 28rem) !important; + padding: 0.55rem !important; + border-radius: 0.65rem !important; + background: #f8fbff !important; + border: 1px solid #cbd5e1 !important; + box-shadow: 0 0.65rem 1.4rem rgba(15,23,42,0.18) !important; + } + + #layout-word-popover .layout-mini-grid { + grid-template-columns: 4.5rem auto auto auto auto auto !important; + gap: 0.25rem !important; + align-items: center !important; + } + + #layout-word-popover input[type="text"], + #layout-word-popover input[type="number"] { + min-height: 1.8rem !important; + font-size: 0.82rem !important; + padding: 0.22rem 0.4rem !important; + } + + #layout-word-popover button { + min-height: 1.8rem !important; + padding: 0.22rem 0.48rem !important; + font-size: 0.78rem !important; + } + + #layout-word-popover input[type="color"] { + width: 1.65rem !important; + height: 1.65rem !important; + } + + @media (max-width: 760px) { + #layout-review-toolbar { + gap: 0.22rem !important; + padding: 0.35rem !important; + } + + #layout-review-toolbar .layout-tool-btn, + #layout-review-toolbar button { + font-size: 0.76rem !important; + padding: 0.25rem 0.42rem !important; + } + + #layout-review-toolbar label { + font-size: 0.72rem !important; + padding: 0.15rem 0.28rem !important; + } + } + + + /* Rebuilt Word-style ribbon toolbar */ + #layout-review-toolbar.word-ribbon-toolbar { + display: flex !important; + flex-wrap: wrap !important; + align-items: stretch !important; + gap: 0 !important; + padding: 0.25rem 0.35rem !important; + border: 1px solid #c7d3e2 !important; + border-radius: 0.55rem !important; + background: #f7f9fc !important; + box-shadow: inset 0 -1px 0 #d8e1ec !important; + } + + .word-ribbon-group { + display: flex !important; + flex-direction: column !important; + justify-content: space-between !important; + gap: 0.18rem !important; + padding: 0.25rem 0.55rem 0.12rem !important; + border-right: 1px solid #cbd5e1 !important; + min-height: 4.1rem !important; + } + + .word-ribbon-row { + display: flex !important; + align-items: center !important; + gap: 0.25rem !important; + min-height: 1.55rem !important; + } + + .word-ribbon-label { + text-align: center !important; + font-size: 0.64rem !important; + color: #64748b !important; + line-height: 1 !important; + } + + #layout-review-toolbar .layout-tool-btn, + #layout-review-toolbar button { + min-height: 1.55rem !important; + padding: 0.18rem 0.45rem !important; + border: 1px solid #b8c6d8 !important; + border-radius: 0.22rem !important; + background: linear-gradient(#ffffff, #eef3fa) !important; + color: #1f2937 !important; + font-size: 0.76rem !important; + font-weight: 600 !important; + line-height: 1 !important; + } + + #layout-review-toolbar .layout-tool-btn.active, + #layout-review-toolbar .layout-tool-btn.primary { + background: #dbeafe !important; + border-color: #60a5fa !important; + color: #1e3a8a !important; + } + + #layout-review-toolbar .layout-tool-btn.danger { + background: #ffe4e6 !important; + border-color: #fda4af !important; + color: #9f1239 !important; + } + + #layout-review-toolbar label { + display: inline-flex !important; + align-items: center !important; + gap: 0.22rem !important; + font-size: 0.74rem !important; + color: #334155 !important; + white-space: nowrap !important; + } + + #layout-review-toolbar input[type="checkbox"] { + width: 0.95rem !important; + height: 0.95rem !important; + margin: 0 !important; + accent-color: #2563eb !important; + } + + #layout-review-toolbar input[type="color"] { + width: 1.45rem !important; + height: 1.45rem !important; + padding: 0 !important; + border: 1px solid #94a3b8 !important; + border-radius: 0.2rem !important; + background: #ffffff !important; + } + + .word-ribbon-select, + .word-ribbon-font-size { + height: 1.55rem !important; + display: inline-flex !important; + align-items: center !important; + padding: 0 0.4rem !important; + border: 1px solid #b8c6d8 !important; + border-radius: 0.2rem !important; + background: white !important; + font-size: 0.74rem !important; + color: #334155 !important; + } + + .word-ribbon-select { + min-width: 5.3rem !important; + } + + .word-ribbon-blue-a { + font-size: 1.05rem !important; + font-weight: 700 !important; + color: #2563eb !important; + } + + #layout-zoom-label, + #layout-review-status { + font-size: 0.8rem !important; + color: #334155 !important; + padding: 0 0.25rem !important; + white-space: nowrap !important; + } + + + /* Force Word-like single horizontal ribbon */ + #layout-review-toolbar.word-ribbon-toolbar { + display: flex !important; + flex-wrap: nowrap !important; + align-items: stretch !important; + gap: 0 !important; + padding: 0.25rem 0.35rem !important; + min-height: 4.85rem !important; + max-height: 5.25rem !important; + overflow-x: auto !important; + overflow-y: hidden !important; + border: 1px solid #c7d3e2 !important; + border-radius: 0.45rem !important; + background: #f6f8fb !important; + } + + #layout-review-toolbar .word-ribbon-group { + flex: 0 0 auto !important; + min-height: 4.3rem !important; + height: 4.3rem !important; + padding: 0.25rem 0.55rem 0.1rem !important; + border-right: 1px solid #cbd5e1 !important; + display: flex !important; + flex-direction: column !important; + justify-content: space-between !important; + } + + #layout-review-toolbar .word-ribbon-row { + display: flex !important; + align-items: center !important; + gap: 0.22rem !important; + min-height: 1.45rem !important; + white-space: nowrap !important; + } + + #layout-review-toolbar .word-ribbon-label { + height: 0.75rem !important; + font-size: 0.58rem !important; + color: #64748b !important; + text-align: center !important; + line-height: 0.75rem !important; + } + + #layout-review-toolbar button, + #layout-review-toolbar .layout-tool-btn { + min-height: 1.45rem !important; + height: 1.45rem !important; + padding: 0.12rem 0.38rem !important; + font-size: 0.68rem !important; + border-radius: 0.18rem !important; + line-height: 1 !important; + } + + #layout-review-toolbar label { + min-height: 1.45rem !important; + font-size: 0.68rem !important; + gap: 0.18rem !important; + padding: 0 !important; + } + + #layout-review-toolbar input[type="checkbox"] { + width: 0.82rem !important; + height: 0.82rem !important; + } + + #layout-review-toolbar input[type="color"] { + width: 1.25rem !important; + height: 1.25rem !important; + padding: 0 !important; + } + + #layout-review-toolbar .word-ribbon-select { + min-width: 4.6rem !important; + height: 1.45rem !important; + font-size: 0.68rem !important; + padding: 0 0.3rem !important; + } + + #layout-review-toolbar .word-ribbon-font-size { + height: 1.45rem !important; + font-size: 0.68rem !important; + padding: 0 0.32rem !important; + } + + #layout-review-toolbar .word-ribbon-blue-a { + font-size: 0.95rem !important; + } + + #layout-zoom-label, + #layout-review-status { + font-size: 0.72rem !important; + height: 1.45rem !important; + display: inline-flex !important; + align-items: center !important; + } + + + /* Refine Word-like ribbon: denser, usable horizontal scroll */ + #layout-review-toolbar.word-ribbon-toolbar { + min-height: 4.35rem !important; + max-height: 4.6rem !important; + padding: 0.18rem 0.25rem !important; + scrollbar-width: thin !important; + background: linear-gradient(#f8fafc, #eef3f8) !important; + } + + #layout-review-toolbar.word-ribbon-toolbar::-webkit-scrollbar { + height: 0.35rem !important; + } + + #layout-review-toolbar.word-ribbon-toolbar::-webkit-scrollbar-thumb { + background: #cbd5e1 !important; + border-radius: 999px !important; + } + + #layout-review-toolbar .word-ribbon-group { + height: 3.85rem !important; + min-height: 3.85rem !important; + padding: 0.2rem 0.42rem 0.08rem !important; + } + + #layout-review-toolbar .word-ribbon-row { + min-height: 1.35rem !important; + } + + #layout-review-toolbar button, + #layout-review-toolbar .layout-tool-btn { + height: 1.35rem !important; + min-height: 1.35rem !important; + padding: 0.08rem 0.34rem !important; + font-size: 0.66rem !important; + } + + #layout-review-toolbar label, + #layout-review-toolbar .word-ribbon-select, + #layout-review-toolbar .word-ribbon-font-size, + #layout-zoom-label, + #layout-review-status { + height: 1.35rem !important; + min-height: 1.35rem !important; + font-size: 0.66rem !important; + } + + #layout-review-toolbar input[type="color"] { + width: 1.18rem !important; + height: 1.18rem !important; + } + + #layout-review-toolbar input[type="checkbox"] { + width: 0.78rem !important; + height: 0.78rem !important; + } + + #layout-review-toolbar .word-ribbon-label { + height: 0.65rem !important; + line-height: 0.65rem !important; + font-size: 0.55rem !important; + } + + #layout-review-toolbar .word-ribbon-select { + min-width: 4.25rem !important; + } + + + /* arrow group row */ + #layout-word-popover > div:nth-child(2) > div:nth-of-type(2) { + grid-column: 1 / 2 !important; + display: flex !important; + align-items: center !important; + gap: 0.28rem !important; + flex-wrap: wrap !important; + } + + #layout-word-popover > div:nth-child(2) > div:nth-of-type(2) > div { + display: contents !important; + } + + /* apply/delete row */ + #layout-word-popover > div:nth-child(2) > div:nth-of-type(3) { + grid-column: 2 / 3 !important; + display: flex !important; + align-items: center !important; + justify-content: flex-end !important; + gap: 0.28rem !important; + flex-wrap: wrap !important; + } + + #layout-popover-apply { + background: linear-gradient(#dbeafe, #c9defc) !important; + border-color: #8cb4f4 !important; + color: #1e3a8a !important; + } + + #layout-popover-delete { + background: linear-gradient(#fff1f2, #ffe4e6) !important; + border-color: #f3b0b8 !important; + color: #9f1239 !important; + } + + @media (max-width: 760px) { + #layout-word-popover { + width: min(96vw, 30rem) !important; + } + + #layout-word-popover .layout-mini-grid { + grid-template-columns: minmax(7rem, 1fr) 3.2rem auto auto auto 1.8rem !important; + } + + #layout-popover-text, + #layout-popover-font-family, + #layout-popover-font-size { + font-size: 0.78rem !important; + } + + #layout-popover-font-down, + #layout-popover-font-up, + #layout-popover-font-all, + #layout-popover-up, + #layout-popover-left, + #layout-popover-down, + #layout-popover-right, + #layout-popover-apply, + #layout-popover-delete { + font-size: 0.76rem !important; + padding: 0.12rem 0.34rem !important; + } + } + + + /* Word-like selected-word popup */ + #layout-word-popover { + width: min(94vw, 38rem) !important; + padding: 0.32rem !important; + border: 1px solid #bfc8d4 !important; + border-radius: 0.12rem !important; + background: #ffffff !important; + box-shadow: 0 0.35rem 1.05rem rgba(15, 23, 42, 0.22) !important; + } + + #layout-word-popover > div:first-child { + display: none !important; + } + + #layout-word-popover > div:nth-child(2) { + display: grid !important; + grid-template-columns: auto auto auto auto auto auto auto auto auto !important; + grid-auto-rows: 1.9rem !important; + gap: 0.18rem !important; + align-items: center !important; + } + + #layout-popover-text { + grid-column: 1 / 4 !important; + grid-row: 1 !important; + width: 11.5rem !important; + min-height: 1.85rem !important; + padding: 0.16rem 0.36rem !important; + border: 1px solid #bfc8d4 !important; + border-radius: 0 !important; + font-size: 0.86rem !important; + background: #ffffff !important; + } + + #layout-word-popover .layout-mini-grid { + display: contents !important; + } + + #layout-popover-font-family { + grid-column: 1 / 3 !important; + grid-row: 2 !important; + width: 9.5rem !important; + } + + #layout-popover-font-size { + grid-column: 3 / 4 !important; + grid-row: 2 !important; + width: 3.6rem !important; + text-align: center !important; + } + + #layout-popover-font-family, + #layout-popover-font-size { + height: 1.85rem !important; + padding: 0.12rem 0.34rem !important; + border: 1px solid #bfc8d4 !important; + border-radius: 0 !important; + background: #ffffff !important; + font-size: 0.84rem !important; + } + + #layout-word-popover label[for="layout-popover-font-family"], + #layout-word-popover label[for="layout-popover-text-color"] { + display: none !important; + } + + #layout-popover-font-down, + #layout-popover-font-up, + #layout-popover-font-all, + #layout-popover-up, + #layout-popover-left, + #layout-popover-down, + #layout-popover-right, + #layout-popover-apply, + #layout-popover-delete { + height: 1.85rem !important; + min-height: 1.85rem !important; + padding: 0.12rem 0.42rem !important; + border: 1px solid #bfc8d4 !important; + border-radius: 0 !important; + background: linear-gradient(#ffffff, #edf2f8) !important; + color: #111827 !important; + font-size: 0.82rem !important; + font-weight: 600 !important; + line-height: 1 !important; + } + + #layout-popover-font-down { + grid-column: 4 !important; + grid-row: 1 !important; + } + + #layout-popover-font-up { + grid-column: 5 !important; + grid-row: 1 !important; + } + + #layout-popover-font-all { + grid-column: 6 !important; + grid-row: 1 !important; + } + + #layout-popover-text-color { + grid-column: 7 !important; + grid-row: 1 !important; + width: 1.85rem !important; + height: 1.85rem !important; + padding: 0 !important; + border: 1px solid #94a3b8 !important; + border-radius: 0 !important; + background: #ffffff !important; + } + + #layout-word-popover > div:nth-child(2) > div:nth-of-type(2) { + display: contents !important; + } + + #layout-word-popover > div:nth-child(2) > div:nth-of-type(2) > div { + display: contents !important; + } + + #layout-popover-up { + grid-column: 8 !important; + grid-row: 1 !important; + } + + #layout-popover-left { + grid-column: 8 !important; + grid-row: 2 !important; + } + + #layout-popover-down { + grid-column: 9 !important; + grid-row: 2 !important; + } + + #layout-popover-right { + grid-column: 10 !important; + grid-row: 2 !important; + } + + #layout-word-popover > div:nth-child(2) > div:nth-of-type(3) { + display: contents !important; + } + + #layout-popover-apply { + grid-column: 4 / 6 !important; + grid-row: 2 !important; + background: linear-gradient(#ffffff, #edf2f8) !important; + } + + #layout-popover-delete { + grid-column: 6 / 8 !important; + grid-row: 2 !important; + background: linear-gradient(#fff7f8, #ffe4e6) !important; + color: #9f1239 !important; + } + + @media (max-width: 760px) { + #layout-word-popover { + width: min(96vw, 34rem) !important; + overflow-x: auto !important; + } + + #layout-word-popover > div:nth-child(2) { + min-width: 32rem !important; + } + } + + + + /* Lock-size control in selected-word popup */ + #layout-word-popover .layout-popover-lock-size-label { + height: 1.85rem !important; + padding: 0 0.35rem !important; + border: 1px solid #bfc8d4 !important; + background: #ffffff !important; + font-size: 0.78rem !important; + color: #334155 !important; + box-sizing: border-box !important; + } + + #layout-popover-lock-size { + width: 0.9rem !important; + height: 0.9rem !important; + margin: 0 !important; + } + + + /* Move selected-word controls into ribbon */ + #layout-ribbon-selection-group { + min-width: 42rem !important; + } + + #layout-ribbon-selection-group .word-ribbon-row { + gap: 0.25rem !important; + } + + #layout-ribbon-selection-group .ribbon-inline-field { + display: inline-flex !important; + align-items: center !important; + gap: 0.2rem !important; + margin: 0 !important; + padding: 0 !important; + border: none !important; + min-height: 1.35rem !important; + } + + #layout-ribbon-selection-group .ribbon-inline-field span { + font-size: 0.62rem !important; + color: #64748b !important; + } + + #layout-ribbon-selection-group input, + #layout-ribbon-selection-group select { + height: 1.35rem !important; + min-height: 1.35rem !important; + padding: 0.08rem 0.3rem !important; + font-size: 0.66rem !important; + border: 1px solid #b8c6d8 !important; + border-radius: 0.18rem !important; + background: #fff !important; + box-sizing: border-box !important; + } + + #layout-ribbon-selection-group .ribbon-text-field input { + width: 7.5rem !important; + } + + #layout-ribbon-selection-group .ribbon-font-field input { + width: 6.5rem !important; + } + + #layout-ribbon-selection-group .ribbon-size-field input { + width: 3.2rem !important; + text-align: center !important; + } + + #layout-ribbon-selection-group .ribbon-color-field input[type="color"] { + width: 1.35rem !important; + padding: 0 !important; + } + + #layout-ribbon-selection-group .ribbon-small-select select { + width: 4.8rem !important; + } + + #layout-ribbon-selection-group .ribbon-small-field input, + #layout-ribbon-selection-group .ribbon-xy-field input { + width: 3.6rem !important; + } + + #layout-ribbon-selection-group button { + height: 1.35rem !important; + min-height: 1.35rem !important; + padding: 0.08rem 0.32rem !important; + font-size: 0.64rem !important; + } + + #layout-props-card { + display: none !important; + } + + + /* Better fit selected-word ribbon controls */ + #layout-review-toolbar.word-ribbon-toolbar { + overflow-x: auto !important; + overflow-y: hidden !important; + max-width: 100% !important; + } + + #layout-ribbon-selection-group { + min-width: 36rem !important; + max-width: none !important; + flex: 0 0 auto !important; + padding-left: 0.4rem !important; + padding-right: 0.4rem !important; + } + + #layout-ribbon-selection-group .word-ribbon-row { + display: flex !important; + flex-wrap: nowrap !important; + align-items: center !important; + gap: 0.18rem !important; + overflow: hidden !important; + } + + #layout-ribbon-selection-group .ribbon-inline-field { + display: inline-flex !important; + align-items: center !important; + gap: 0.12rem !important; + min-width: 0 !important; + white-space: nowrap !important; + } + + #layout-ribbon-selection-group .ribbon-inline-field span { + font-size: 0.58rem !important; + max-width: 3.4rem !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + } + + #layout-ribbon-selection-group input, + #layout-ribbon-selection-group select, + #layout-ribbon-selection-group button { + height: 1.25rem !important; + min-height: 1.25rem !important; + font-size: 0.6rem !important; + padding: 0.05rem 0.22rem !important; + } + + #layout-ribbon-selection-group .ribbon-text-field input { + width: 7rem !important; + } + + #layout-ribbon-selection-group .ribbon-font-field input { + width: 5.8rem !important; + } + + #layout-ribbon-selection-group .ribbon-size-field input { + width: 2.6rem !important; + text-align: center !important; + } + + #layout-ribbon-selection-group .ribbon-color-field input[type="color"] { + width: 1.25rem !important; + min-width: 1.25rem !important; + padding: 0 !important; + } + + #layout-ribbon-selection-group .ribbon-small-select select { + width: 4.2rem !important; + } + + #layout-ribbon-selection-group .ribbon-small-field input { + width: 3rem !important; + } + + #layout-ribbon-selection-group .ribbon-xy-field { + display: none !important; + } + + #layout-ribbon-selection-group #layout-apply-style-line, + #layout-ribbon-selection-group #layout-apply-style-page, + #layout-ribbon-selection-group #layout-reset-style-word { + display: none !important; + } + + #layout-ribbon-selection-group #layout-delete-word-inline { + color: #9f1239 !important; + background: #ffe4e6 !important; + border-color: #fda4af !important; + } + + @media (max-width: 760px) { + #layout-ribbon-selection-group { + min-width: 31rem !important; + } + + #layout-ribbon-selection-group .ribbon-text-field input { + width: 6rem !important; + } + + #layout-ribbon-selection-group .ribbon-font-field input { + width: 5rem !important; + } + + #layout-ribbon-selection-group .ribbon-inline-field span { + display: none !important; + } + } + + + /* Selection ribbon polish with compact position controls */ + #layout-ribbon-selection-group { + min-width: 38rem !important; + padding-left: 0.25rem !important; + padding-right: 0.25rem !important; + } + + #layout-ribbon-selection-group .word-ribbon-row { + gap: 0.12rem !important; + } + + #layout-ribbon-selection-group .ribbon-inline-field { + gap: 0.08rem !important; + } + + #layout-ribbon-selection-group .ribbon-inline-field span { + display: inline-flex !important; + font-size: 0.54rem !important; + max-width: 2.4rem !important; + color: #64748b !important; + } + + #layout-ribbon-selection-group input, + #layout-ribbon-selection-group select, + #layout-ribbon-selection-group button { + height: 1.18rem !important; + min-height: 1.18rem !important; + font-size: 0.58rem !important; + padding: 0.03rem 0.18rem !important; + } + + #layout-ribbon-selection-group .ribbon-text-field input { + width: 6.2rem !important; + } + + #layout-ribbon-selection-group .ribbon-font-field input { + width: 4.8rem !important; + } + + #layout-ribbon-selection-group .ribbon-size-field input { + width: 2.25rem !important; + text-align: center !important; + } + + #layout-ribbon-selection-group .ribbon-color-field input[type="color"] { + width: 1.12rem !important; + min-width: 1.12rem !important; + padding: 0 !important; + } + + #layout-ribbon-selection-group .ribbon-small-select select { + width: 3.55rem !important; + } + + #layout-ribbon-selection-group .ribbon-small-field input { + width: 2.55rem !important; + } + + /* Restore compact x/y position controls */ + #layout-ribbon-selection-group .ribbon-xy-field { + display: inline-flex !important; + } + + #layout-ribbon-selection-group .ribbon-xy-field span { + display: inline-flex !important; + max-width: 1rem !important; + font-size: 0.52rem !important; + } + + #layout-ribbon-selection-group .ribbon-xy-field input { + width: 2.45rem !important; + text-align: right !important; + } + + #layout-ribbon-selection-group #layout-apply-style-word { + min-width: 3.2rem !important; + } + + #layout-ribbon-selection-group #layout-apply-word { + min-width: 2.5rem !important; + } + + #layout-ribbon-selection-group #layout-delete-word-inline { + min-width: 4.3rem !important; + color: #9f1239 !important; + background: #ffe4e6 !important; + border-color: #fda4af !important; + } + + #layout-ribbon-selection-group .word-ribbon-label { + margin-top: 0.02rem !important; + } + + @media (max-width: 760px) { + #layout-ribbon-selection-group { + min-width: 37rem !important; + } + + #layout-ribbon-selection-group .ribbon-inline-field span { + display: inline-flex !important; + } + + #layout-ribbon-selection-group .ribbon-text-field input { + width: 5.6rem !important; + } + + #layout-ribbon-selection-group .ribbon-font-field input { + width: 4.4rem !important; + } + + #layout-ribbon-selection-group .ribbon-xy-field input { + width: 2.25rem !important; + } + } + + + + /* Real paired font select */ + .layout-font-family-select { + height: 1.18rem !important; + min-height: 1.18rem !important; + width: 1.35rem !important; + padding: 0 !important; + margin-left: 0.08rem !important; + font-size: 0.58rem !important; + border: 1px solid #b8c6d8 !important; + border-radius: 0.18rem !important; + background: #ffffff !important; + box-sizing: border-box !important; + } + + #layout-popover-font-family-select { + width: 1.8rem !important; + height: 1.9rem !important; + min-height: 1.9rem !important; + grid-column: 1 / 2 !important; + grid-row: 2 !important; + justify-self: end !important; + z-index: 2 !important; + } + + #layout-popover-font-family { + padding-right: 2rem !important; + } + + #layout-ribbon-selection-group .ribbon-font-field { + min-width: 6.5rem !important; + } + + #layout-ribbon-selection-group .ribbon-font-field input { + width: 4.6rem !important; + } + + + + /* Popup: anchored normally on desktop, bottom bar only on mobile */ + #layout-word-popover { + box-sizing: border-box !important; + z-index: 9998 !important; + } + + @media (max-width: 760px) { + #layout-word-popover { + position: fixed !important; + left: 4.35rem !important; + right: 0.35rem !important; + bottom: 0.45rem !important; + top: auto !important; + width: auto !important; + max-width: none !important; + transform: none !important; + overflow: visible !important; + } + } + + /* Keep ribbon from loading half-scrolled / visually clipped */ + #layout-review-toolbar.word-ribbon-toolbar { + scroll-behavior: auto !important; + } + + #layout-ribbon-selection-group { + max-width: none !important; + } + + + /* Mobile popup final cleanup */ + #layout-review-debug { + display: none !important; + } + + @media (max-width: 760px) { + #layout-word-popover { + position: fixed !important; + left: 4.25rem !important; + right: 0.35rem !important; + bottom: calc(env(safe-area-inset-bottom, 0px) + 0.6rem) !important; + top: auto !important; + width: auto !important; + max-width: none !important; + max-height: 9.5rem !important; + overflow: hidden !important; + padding: 0.35rem !important; + box-sizing: border-box !important; + z-index: 9998 !important; + } + + #layout-word-popover > div:nth-child(2) { + display: grid !important; + grid-template-columns: minmax(0, 1fr) 3.4rem 3.4rem 3.4rem !important; + grid-auto-rows: 1.9rem !important; + gap: 0.25rem !important; + min-width: 0 !important; + max-width: 100% !important; + align-items: center !important; + } + + #layout-popover-text { + grid-column: 1 / 5 !important; + grid-row: 1 !important; + width: 100% !important; + } + + #layout-popover-font-family { + grid-column: 1 / 2 !important; + grid-row: 2 !important; + width: 100% !important; + min-width: 0 !important; + padding-right: 0.35rem !important; + } + + #layout-popover-font-family-select { + grid-column: 2 / 3 !important; + grid-row: 2 !important; + width: 100% !important; + height: 1.9rem !important; + min-height: 1.9rem !important; + position: static !important; + justify-self: stretch !important; + } + + #layout-popover-font-size { + grid-column: 3 / 4 !important; + grid-row: 2 !important; + width: 100% !important; + text-align: center !important; + } + + #layout-popover-text-color { + grid-column: 4 / 5 !important; + grid-row: 2 !important; + width: 100% !important; + height: 1.9rem !important; + position: static !important; + justify-self: stretch !important; + } + + #layout-popover-font-down { + grid-column: 1 / 2 !important; + grid-row: 3 !important; + } + + #layout-popover-font-up { + grid-column: 2 / 3 !important; + grid-row: 3 !important; + } + + #layout-popover-font-all { + grid-column: 3 / 4 !important; + grid-row: 3 !important; + } + + #layout-popover-apply { + grid-column: 1 / 3 !important; + grid-row: 4 !important; + } + + #layout-popover-delete { + grid-column: 3 / 5 !important; + grid-row: 4 !important; + } + + .layout-popover-lock-size-label { + grid-column: 1 / 5 !important; + grid-row: 5 !important; + height: 1.5rem !important; + min-height: 1.5rem !important; + padding: 0.15rem 0.3rem !important; + } + + #layout-popover-up, + #layout-popover-left, + #layout-popover-down, + #layout-popover-right { + display: none !important; + } + + #layout-word-popover input, + #layout-word-popover select, + #layout-word-popover button { + min-width: 0 !important; + box-sizing: border-box !important; + } + } + + + +
+
+
+ + + + +
+
Tools
+
+ +
+
+ + +
+
Edit
+
+ +
+
+ + + + 100% +
+
Zoom
+
+ +
+
+ OCR Text + 11 + +
+
+ + +
+
Font
+
+ +
+
+ + + + +
+
+ + + +
+
Display
+
+ + +
+
+
+
Selection
+
+
+
+ + Ready +
+
Document
+
+
+
@@ -650,7 +2009,30 @@ document.addEventListener("DOMContentLoaded", () => {
- + + + + + + + +
@@ -697,7 +2079,20 @@ document.addEventListener("DOMContentLoaded", () => {
- +
@@ -769,7 +2164,94 @@ document.addEventListener("DOMContentLoaded", () => { - + + + + + + - - - - +