.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 - ): 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;
}
|