.editor { flex: 1; /* Top padding clears the toolbar island overlaying this scroll area (height set by App.svelte); extra bottom padding clears the fixed status bar. */ padding: calc(var(--toolbar-overlay-h, 0px) + 1.25rem) 1rem 3rem; overflow: auto; /* Positioning context for the floating table toolbar (TableToolbar.svelte), which is an absolutely-positioned child that scrolls with the content. */ position: relative; } /* Reserves the SCALED footprint of the transformed .paper (a CSS transform takes no layout space of its own), so the scrollbars and horizontal centering reflect the zoomed page. Its width/height are sized in JS (Editor.svelte). */ .paper-scaler { margin: 0 auto; } /* A4 paper container (794px = 210mm at 96dpi by default; --user-page-width is set from the Layout panel and swaps to 1123px in landscape — pageOrientation.ts) */ .paper { width: var(--user-page-width, 794px); /* Zoom is a `transform: scale()` set inline (Editor.svelte); pin the origin so the page scales down/right from the top-left of its reserved footprint. */ transform-origin: top left; /* Positioning context for the table page-break overlay (.band-layer in Editor.svelte), which lives inside .paper so the same transform scales it identically to the .tiptap page background. */ position: relative; } .paper .tiptap { /* Driven by --user-page-height (pageOrientation.ts); fallback = A4 portrait. */ --page-height: var(--user-page-height, 1123px); --page-gap: 20px; /* Driven by --user-margin-* set on :root from the Layout panel (pageMargins.ts). Fallbacks preserve the original hardcoded layout when unset. */ --page-margin-top: var(--user-margin-top, 96px); --page-margin-bottom: var(--user-margin-bottom, 96px); --page-margin-left: var(--user-margin-left, 80px); --page-margin-right: var(--user-margin-right, 80px); --page-cycle: calc(var(--page-height) + var(--page-gap)); position: relative; width: 100%; min-height: var(--page-height); padding: var(--page-margin-top) var(--page-margin-right) var(--page-margin-bottom) var(--page-margin-left); outline: none; font-family: var(--font-serif); /* 12pt matches odf-kit's Standard style and the toolbar's declared default. */ font-size: 12pt; /* "Single" spacing is the font's natural line height (Liberation Serif ≈ 1.15× em), not CSS unitless 1. A paragraph's line spacing multiplies that, so the two ride as separate variables and the block rule below is the one place they meet. */ --natural-line: 1.15; --line-factor: 1; line-height: 1.15; color: var(--color-page-text); /* Multi-page background: page surfaces separated by editor background */ background: /* Left border per page */ repeating-linear-gradient( to bottom, var(--color-page-border) 0px, var(--color-page-border) var(--page-height), transparent var(--page-height), transparent var(--page-cycle) ), /* Right border per page */ repeating-linear-gradient( to bottom, var(--color-page-border) 0px, var(--color-page-border) var(--page-height), transparent var(--page-height), transparent var(--page-cycle) ), /* Top/bottom border lines of each page */ repeating-linear-gradient( to bottom, var(--color-page-border) 0px, var(--color-page-border) 1px, transparent 1px, transparent calc(var(--page-height) - 1px), var(--color-page-border) calc(var(--page-height) - 1px), var(--color-page-border) var(--page-height), transparent var(--page-height), transparent var(--page-cycle) ), /* Page surface (white) alternating with gap (transparent = editor bg shows) */ repeating-linear-gradient( to bottom, var(--color-page-bg) 0px, var(--color-page-bg) var(--page-height), transparent var(--page-height), transparent var(--page-cycle) ); background-size: 1px var(--page-cycle), 1px var(--page-cycle), 100% var(--page-cycle), 100% var(--page-cycle); background-position: left top, right top, left top, left top; background-repeat: repeat-y; border: none; /* Subtle overall shadow for depth */ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); } /* Page break spacer widgets (inserted by ProseMirror decorations). line-height/font-size are zeroed so the spacer doesn't pick up the inherited inline strut when it sits mid-paragraph between text nodes — otherwise the browser adds a phantom line of height around the widget and the continuation text on the next page lands too low. Scoped to div so it never overrides the display of the table-row spacer below. */ .paper .tiptap div[data-page-break-spacer] { display: block; width: 100%; margin: 0; padding: 0; border: none; background: none; line-height: 0; font-size: 0; overflow: hidden; } /* Table page-break spacer: a borderless, empty / that bridges the page gap so the following rows start on the next page (a table can't hold a block spacer between its rows). Keeps default table-row/cell display. */ .paper .tiptap tr[data-page-break-spacer] { background: none; } .paper .tiptap td[data-page-break-spacer-cell] { padding: 0; border: none; line-height: 0; font-size: 0; } /* No paragraph spacing by default, like LibreOffice's Default Paragraph Style and Word's implied 0 — an editor default here would show up as extra space in every imported document (export/odt.ts zeroes odf-kit's Standard style to match). */ .paper .tiptap p { margin-top: 0; margin-bottom: 0; } /* Paragraph "colored field" + rule line (paragraphBox.ts). Background-color and per-side borders are inline styles from renderHTML; a bordered block gets a little padding so the text doesn't touch the rule (matches LibreOffice's fo:padding on such paragraphs). */ .paper .tiptap [data-pborder-top], .paper .tiptap [data-pborder-bottom], .paper .tiptap [data-pborder-left], .paper .tiptap [data-pborder-right] { padding: 0.04cm 0.08cm; } /* Hyperlinks (link.ts). Ctrl/Cmd+click opens them (Editor.svelte). */ .paper .tiptap a { color: #1a56db; text-decoration: underline; cursor: text; } /* Find & Replace match highlights (searchReplace.ts); current match is stronger. */ .paper .tiptap .search-match { background: rgba(255, 213, 0, 0.45); border-radius: 2px; } .paper .tiptap .search-match-current { background: rgba(255, 145, 0, 0.8); } /* Generated table of contents (tableOfContents.ts). A non-editable block listing headings with dot leaders to their live page number; entries are clickable. */ .paper .tiptap .toc { margin: 0 0 0.5cm; user-select: none; cursor: default; } .paper .tiptap .toc-title { font-size: 16pt; font-weight: bold; margin-bottom: 0.3cm; } .paper .tiptap .toc-empty { font-style: italic; opacity: 0.6; } .paper .tiptap .toc-entry { display: flex; align-items: baseline; cursor: pointer; line-height: 1.5; } .paper .tiptap .toc-entry:hover .toc-text { text-decoration: underline; } .paper .tiptap .toc-level-2 { padding-left: 0.6cm; } .paper .tiptap .toc-level-3 { padding-left: 1.2cm; } .paper .tiptap .toc-level-4 { padding-left: 1.8cm; } .paper .tiptap .toc-level-5 { padding-left: 2.4cm; } .paper .tiptap .toc-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 0 1 auto; } /* Leader dots filling the gap: the row grants the span the space that is left and clips the dots that do not fit, so the page number stays put. */ .paper .tiptap .toc-leader { flex: 1 1 0; /* basis 0: the dots are decoration, they must not size the row */ min-width: 1em; overflow: hidden; white-space: nowrap; } .paper .tiptap .toc-page { flex: 0 0 auto; white-space: nowrap; } /* Inline, as-character images (image.ts). The wrapper reserves the rotated bounding box; the rotor holds the , resize handles and rotate grip, rotating together. */ .paper .tiptap .image-node { position: relative; display: inline-block; vertical-align: bottom; line-height: 0; } /* While selected, lift the node (and its handles) above the header/footer layer (z-index 22) so a rotate/resize handle poking into a margin stays grabbable. */ .paper .tiptap .image-node.image-selected { z-index: 25; } .paper .tiptap .image-rotor { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); } .paper .tiptap .image-rotor img { width: 100%; height: 100%; display: block; } .paper .tiptap .image-node.image-selected .image-rotor img { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; } .paper .tiptap .image-resize-handle { display: none; position: absolute; width: 10px; height: 10px; background: var(--accent, #3b82f6); border: 1px solid #fff; border-radius: 50%; z-index: 30; } .paper .tiptap .image-node.image-selected .image-resize-handle, .paper .tiptap .image-node.image-selected .image-rotate-handle, .paper .tiptap .textbox-node.textbox-selected .image-resize-handle, .paper .tiptap .textbox-node.textbox-selected .image-rotate-handle, .paper .tiptap .textbox-node.textbox-active .image-resize-handle, .paper .tiptap .textbox-node.textbox-active .image-rotate-handle { display: block; } .paper .tiptap .image-resize-nw { top: -5px; left: -5px; cursor: nwse-resize; } .paper .tiptap .image-resize-n { top: -5px; left: 50%; margin-left: -5px; cursor: ns-resize; } .paper .tiptap .image-resize-ne { top: -5px; right: -5px; cursor: nesw-resize; } .paper .tiptap .image-resize-e { top: 50%; right: -5px; margin-top: -5px; cursor: ew-resize; } .paper .tiptap .image-resize-se { bottom: -5px; right: -5px; cursor: nwse-resize; } .paper .tiptap .image-resize-s { bottom: -5px; left: 50%; margin-left: -5px; cursor: ns-resize; } .paper .tiptap .image-resize-sw { bottom: -5px; left: -5px; cursor: nesw-resize; } .paper .tiptap .image-resize-w { top: 50%; left: -5px; margin-top: -5px; cursor: ew-resize; } /* Rotate grip floats above top-centre with a short connector line; the icon is a circular rotation arrow. */ .paper .tiptap .image-rotate-handle { display: none; position: absolute; top: -28px; left: 50%; width: 16px; height: 16px; margin-left: -8px; background: #fff center / 11px 11px no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%233b82f6' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 12a9 9 0 1 1-2.6-6.4'/%3E%3Cpath d='M21 4v5h-5'/%3E%3C/svg%3E"); border: 1px solid var(--accent, #3b82f6); border-radius: 50%; cursor: grab; z-index: 31; } .paper .tiptap .image-rotate-handle::before { content: ''; position: absolute; top: 15px; left: 50%; width: 1px; height: 13px; margin-left: -0.5px; background: var(--accent, #3b82f6); } /* Live size readout while resizing. */ .paper .tiptap .image-size-badge { display: none; position: absolute; top: 6px; left: 50%; transform: translateX(-50%); padding: 2px 7px; background: rgba(0, 0, 0, 0.75); color: #fff; font-size: 11px; line-height: 1.4; white-space: nowrap; border-radius: 4px; pointer-events: none; z-index: 32; } /* Text boxes / shapes (textBox.ts). Same wrapper/rotor split as images: the wrapper reserves the rotated bounding box, the rotor carries fill/stroke/rotation and the shared image-* handles; .textbox-content is the editable area. */ .paper .tiptap .textbox-node { position: relative; } .paper .tiptap .textbox-node.textbox-selected, .paper .tiptap .textbox-node.textbox-active { z-index: 25; } .paper .tiptap .textbox-rotor { position: absolute; left: 50%; top: 50%; box-sizing: border-box; transform: translate(-50%, -50%); } .paper .tiptap .textbox-node.textbox-selected .textbox-rotor, .paper .tiptap .textbox-node.textbox-active .textbox-rotor { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; } /* Grab affordance on the frame edge (outside the text area). */ .paper .tiptap .textbox-rotor { cursor: move; } .paper .tiptap .textbox-content { cursor: text; min-height: 1em; } .paper .tiptap .textbox-content > :first-child { margin-top: 0; } .paper .tiptap .textbox-content > :last-child { margin-bottom: 0; } /* Multi-column section (columns.ts); column-count/gap come from the node's inline style. Balanced fill matches ODF/Word section behavior; no visual affordance — the section must read as plain flowing text. */ .paper .tiptap .columns-node { column-fill: balance; } .paper .tiptap .columns-node > :first-child { margin-top: 0; } .paper .tiptap .columns-node > :last-child { margin-bottom: 0; } /* Tab stops every 1.25cm, matching ODF's default tab interval. */ .paper .tiptap { tab-size: 1.25cm; /* prosemirror-view sets `break-spaces`, which forbids a line-end space from hanging into the margin, so lines break one word earlier than in Word/LibreOffice. `pre-wrap` lets it hang, as they do. */ white-space: pre-wrap; } .paper .tiptap h1, .paper .tiptap h2, .paper .tiptap h3, .paper .tiptap h4, .paper .tiptap h5 { /* Neutral base: font, size and margins come from the named paragraph styles (styles/styleSheet.ts → the #document-styles element Editor.svelte writes). */ font: inherit; margin: 0; } /* The line box: the paragraph's spacing factor times its font's natural line height. Either half may come from the style rule or, nearer, from the block's own attrs (lineHeight.ts / blockFontSize.ts) — multiplying them here is what keeps both. */ .paper .tiptap :is(p, h1, h2, h3, h4, h5, li) { line-height: calc(var(--line-factor) * var(--natural-line)); } .paper .tiptap ul, .paper .tiptap ol { /* One level = LIST_LEFT_STEP_CM (export/docx.ts, and the 1.27cm base the ODT list styles use), so the on-screen item text sits where both exports place it. */ padding-left: 1.27cm; margin-top: 0; /* Matches the exported List_20_* styles, which inherit Standard's zeroed margin. */ margin-bottom: 0; } /* Ordered-list numbering, keyed by data-eff-list-style — the effective numbering orderedList.ts's plugin resolves per
    from its attr + ancestor chain. Dot variants reuse the built-in counter styles; bracket variants override the suffix. */ @counter-style oledit-decimal-paren { system: extends decimal; suffix: ") "; } @counter-style oledit-lower-alpha-paren { system: extends lower-alpha; suffix: ") "; } @counter-style oledit-upper-alpha-paren { system: extends upper-alpha; suffix: ") "; } @counter-style oledit-lower-roman-paren { system: extends lower-roman; suffix: ") "; } @counter-style oledit-upper-roman-paren { system: extends upper-roman; suffix: ") "; } .paper .tiptap ol[data-eff-list-style="decimal"] { list-style-type: decimal; } .paper .tiptap ol[data-eff-list-style="decimal-paren"] { list-style-type: oledit-decimal-paren; } .paper .tiptap ol[data-eff-list-style="lower-alpha"] { list-style-type: lower-alpha; } .paper .tiptap ol[data-eff-list-style="lower-alpha-paren"] { list-style-type: oledit-lower-alpha-paren; } .paper .tiptap ol[data-eff-list-style="upper-alpha"] { list-style-type: upper-alpha; } .paper .tiptap ol[data-eff-list-style="upper-alpha-paren"] { list-style-type: oledit-upper-alpha-paren; } .paper .tiptap ol[data-eff-list-style="lower-roman"] { list-style-type: lower-roman; } .paper .tiptap ol[data-eff-list-style="lower-roman-paren"] { list-style-type: oledit-lower-roman-paren; } .paper .tiptap ol[data-eff-list-style="upper-roman"] { list-style-type: upper-roman; } .paper .tiptap ol[data-eff-list-style="upper-roman-paren"] { list-style-type: oledit-upper-roman-paren; } /* Legal/outline numbering (1., 1.1., 1.2.1. …): the whole chain rendered via CSS counters. The plugin tags the multilevel list and every attr-less member of its chain with data-eff-list-style="multilevel". */ .paper .tiptap ol[data-eff-list-style="multilevel"] > li::marker { content: counters(list-item, ".") ". "; } /* The marker follows the item's first text portion (listMarker.ts sets the custom properties on the
  1. ): a mark sits inside the paragraph and never reaches ::marker, which only inherits the item's own font. */ .paper .tiptap li::marker { font-family: var(--marker-family, inherit); font-weight: var(--marker-weight, inherit); font-style: var(--marker-style, inherit); font-size: var(--marker-size, inherit); color: var(--marker-color, inherit); } /* Bullet markers render through the bundled symbol shim first: it holds only the glyphs Liberation Serif lacks (❖ ➢ ⇨ ✓ ▸ ◆), so those come out compact and platform-independent while every other char falls through to the text font. */ .paper .tiptap ul > li::marker { font-family: 'EdenText Symbols', var(--marker-family, var(--font-serif)), serif; } /* Default bullet markers per nesting depth — the same cycle the ODT/DOCX export writes (DEFAULT_BULLET_CYCLE in bulletListTypes.ts). A custom `bulletChar` attr overrides these via an inline list-style-type from bulletList.ts renderHTML. */ .paper .tiptap ul { list-style-type: "• "; } .paper .tiptap ul ul { list-style-type: "◦ "; } .paper .tiptap ul ul ul { list-style-type: "▪ "; } .paper .tiptap ul ul ul ul { list-style-type: "▸ "; } .paper .tiptap ul ul ul ul ul { list-style-type: "– "; } .paper .tiptap ul ul ul ul ul ul { list-style-type: "· "; } .paper .tiptap li { margin-top: 0; margin-bottom: 0; } .paper .tiptap li p { margin-bottom: 0; } /* Tables. The visual border/padding must match the ODT export so the editor and the exported .odt look identical (see export/odt.ts CUST_TABLE handler): • border → export emits 0.5pt solid #000000; 1px solid renders crisply on screen and is the visual equivalent of a thin 0.5pt line. • padding → Word's/LibreOffice's cell margins. The fallback below is Word's own default; a file's own value rides --cell-pad (tableCellPadding.ts). Tables are wrapped in
    by TipTap's node view. */ .paper .tiptap .tableWrapper { /* Default is flush with the text area; a dragged outer edge sets an inline margin-left/-right here (tableView.ts), which is what the export writes. */ margin: 0; overflow-x: auto; } .paper .tiptap table { width: 100%; /* fixed layout → the per-column percentage widths from the custom node view (tableView.ts) are authoritative; width:100% keeps the table the full text width, matching the ODT export (export/odt.ts emits per-column widths summing to the text width with table:align="margins"). */ table-layout: fixed; border-collapse: collapse; } .paper .tiptap td, .paper .tiptap th { border: 1px solid #000; padding: var(--cell-pad, 0 0.19cm); vertical-align: top; /* Positioning context for the column-resize handle widget (a child div the resize plugin places at the cell's right edge). */ position: relative; /* Cells hold paragraphs; suppress their bottom margin so a single-line cell isn't taller than its text (matches the list-item paragraph treatment). */ } .paper .tiptap td > p, .paper .tiptap th > p { margin-bottom: 0; } /* Header-row cells (tableHeaderRow.ts toggle): bold rendered presentationally so all text — existing, typed, or pasted — is bold. The light fill comes from the cell's inline background-color; export bakes the bold for Word/LibreOffice consistency. */ .paper .tiptap td.cell-header, .paper .tiptap th.cell-header { font-weight: 600; } /* Selected-cell highlight while editing (ProseMirror tables add .selectedCell). A translucent overlay via the ::after avoids disturbing the cell border. */ .paper .tiptap .selectedCell { position: relative; } .paper .tiptap .selectedCell::after { content: ''; position: absolute; inset: 0; background: var(--color-primary); opacity: 0.12; pointer-events: none; } /* Column resize (tableColumnResize.ts). The handle is a widget the plugin places at a cell's right edge when the pointer is near an internal column border; the plugin reads the pointer position itself, so the handle ignores pointer events. `.resize-cursor` is toggled on the whole editor while a border is active. */ .paper .tiptap.resize-cursor { cursor: col-resize; } .paper .tiptap .column-resize-handle { position: absolute; right: -2px; top: 0; bottom: 0; width: 4px; z-index: 20; background: var(--color-primary); pointer-events: none; } /* The table's outer left edge: same handle, mirrored to the cell's left side. */ .paper .tiptap .column-resize-handle.left { left: -2px; right: auto; } .paper .tiptap .column-resize-dragging { /* Suppress text-selection flicker while dragging a column border. */ user-select: none; } /* Row resize (tableRowResize.ts). Mirrors the column handle but sits at a cell's lower edge; one handle per cell forms a continuous line across the active row. `.row-resize-cursor` is toggled on the whole editor while a row border is active. Column resize takes precedence at a corner, so the two never fight. */ .paper .tiptap.row-resize-cursor { cursor: row-resize; } .paper .tiptap .row-resize-handle { position: absolute; left: 0; right: 0; bottom: -2px; height: 4px; z-index: 20; background: var(--color-primary); pointer-events: none; } .paper .tiptap p.is-editor-empty:first-child::before { content: attr(data-placeholder); float: left; color: var(--color-text-muted); pointer-events: none; height: 0; } /* Paragraph mark (¶). Reserved in the layout at all times, so the toggle only recolors it and text never reflows. Zero-size inline-block, glyph overflows visibly: width/height/line-height 0 drop both footprint and line-box strut. */ .paper .tiptap p::after, .paper .tiptap h1::after, .paper .tiptap h2::after, .paper .tiptap h3::after, .paper .tiptap h4::after, .paper .tiptap h5::after { content: '\00A0¶'; display: inline-block; width: 0; height: 0; line-height: 0; overflow: visible; white-space: pre; color: transparent; opacity: 0.5; font-family: var(--font-sans); font-weight: 400; user-select: none; pointer-events: none; } .paper.show-formatting-marks .tiptap p::after, .paper.show-formatting-marks .tiptap h1::after, .paper.show-formatting-marks .tiptap h2::after, .paper.show-formatting-marks .tiptap h3::after, .paper.show-formatting-marks .tiptap h4::after, .paper.show-formatting-marks .tiptap h5::after { color: var(--color-primary); } /* After an inline atom ProseMirror's trailing-break BR adds a line the block has not got, so a paragraph holding an image renders one too tall. Kept after a real BR (it carries the caret onto the empty line) and in an empty block (its only line). */ .paper .tiptap :not(br) + br.ProseMirror-trailingBreak { display: none; } /* Empty block fix: a paragraph/heading/li-paragraph holding only the ProseMirror trailing-break BR would render the inline ¶ on a second line, doubling its height. Pull ::after out of flow. Applied always so the reserved ¶ stays zero-height. */ .paper .tiptap p:has(> br.ProseMirror-trailingBreak:only-child), .paper .tiptap h1:has(> br.ProseMirror-trailingBreak:only-child), .paper .tiptap h2:has(> br.ProseMirror-trailingBreak:only-child), .paper .tiptap h3:has(> br.ProseMirror-trailingBreak:only-child), .paper .tiptap h4:has(> br.ProseMirror-trailingBreak:only-child), .paper .tiptap h5:has(> br.ProseMirror-trailingBreak:only-child), .paper .tiptap li > p:has(> br.ProseMirror-trailingBreak:only-child) { position: relative; } .paper .tiptap p:has(> br.ProseMirror-trailingBreak:only-child)::after, .paper .tiptap h1:has(> br.ProseMirror-trailingBreak:only-child)::after, .paper .tiptap h2:has(> br.ProseMirror-trailingBreak:only-child)::after, .paper .tiptap h3:has(> br.ProseMirror-trailingBreak:only-child)::after, .paper .tiptap h4:has(> br.ProseMirror-trailingBreak:only-child)::after, .paper .tiptap h5:has(> br.ProseMirror-trailingBreak:only-child)::after, .paper .tiptap li > p:has(> br.ProseMirror-trailingBreak:only-child)::after { position: absolute; top: 0; left: 0; margin-left: 0; /* Out of flow, so restore the paragraph's line-height (the base rule zeroes it) to seat the glyph on the empty line instead of the collapsed top edge. */ line-height: inherit; } .paper.show-formatting-marks .tiptap .pm-space-mark { position: relative; } .paper.show-formatting-marks .tiptap .pm-space-mark::before { content: '·'; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--color-primary); opacity: 0.55; font-weight: 700; pointer-events: none; user-select: none; } /* Tab marks: a → arrow overlaid at the start of the tab's width (the tab char itself supplies the gap), mirroring Word/LibreOffice. */ .paper.show-formatting-marks .tiptap .pm-tab-mark { position: relative; } .paper.show-formatting-marks .tiptap .pm-tab-mark::before { content: '→'; position: absolute; left: 0.1em; top: 0; color: var(--color-primary); opacity: 0.5; pointer-events: none; user-select: none; } /* Spell-check: red wavy underline under misspelled words (drawn by spellCheck.ts). skip-ink:none keeps the wave continuous under descenders. */ .tiptap .pm-spell-error { text-decoration: red wavy underline; text-decoration-skip-ink: none; } /* Print styles: proper A4 pagination */ @media print { .editor { padding: 0; overflow: visible; } /* Never scale for print — the real page size comes from @page below. */ .paper-scaler { width: auto; height: auto; } .paper { width: 100%; max-width: none; transform: none; } .paper .tiptap { background: white !important; box-shadow: none; border: none; padding: 0; min-height: auto; } /* Squiggles are an editing aid, never printed. */ .tiptap .pm-spell-error { text-decoration: none; } .paper .tiptap > *[data-page-break] { margin-top: 0 !important; page-break-before: always; } @page { size: A4; margin: 25.4mm; } /* Hide the space/tab glyphs when printing (they're out-of-flow ::before, so removing them changes nothing). The ¶ stays reserved but transparent so print pagination matches the screen — removing it would reflow paragraphs it shares a full line with. */ .paper.show-formatting-marks .tiptap .pm-space-mark::before, .paper.show-formatting-marks .tiptap .pm-tab-mark::before { content: none; } .paper.show-formatting-marks .tiptap p::after, .paper.show-formatting-marks .tiptap h1::after, .paper.show-formatting-marks .tiptap h2::after, .paper.show-formatting-marks .tiptap h3::after, .paper.show-formatting-marks .tiptap h4::after, .paper.show-formatting-marks .tiptap h5::after { color: transparent; } } /* Fallback channel: drive color from the --font-color custom property so the visible color survives even if ProseMirror's incremental DOM update leaves a reused span with a stale `style` attribute that no longer contains `color:`. The inline `color:` declaration still wins when present. */ .tiptap [data-color] { color: var(--font-color, currentColor); } /* allBlack theme: force all explicitly-colored text to the page text color. The color is still stored on the textStyle mark, so .odt exports remain faithful. */ [data-theme="allBlack"] .tiptap [data-color] { color: var(--color-page-text) !important; }