Files
edentext/src/styles/editor.css
T
Steffen Becker 7d4a0b256e fix: indent list levels by the step both exporters write
Lists were padded 1.5rem per level on screen while the DOCX and ODT
exports place item text at LIST_LEFT_STEP_CM (1.27cm) per level, so the
editor disagreed with its own output and with Word and LibreOffice by
6.4mm on the first level, doubling with each nesting level.
2026-08-06 20:01:23 +00:00

759 lines
26 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
.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;
/* ODF/LibreOffice "single" spacing = the font's natural line height
(Liberation Serif ≈ 1.15× em), NOT 1.0× the font size like CSS unitless 1.
Keep this in sync with LINE_HEIGHT_RATIO in lineHeight.ts. */
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 <tr>/<td> 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;
}
/* Dotted leader filling the gap; nudged up to sit on the text baseline. */
.paper .tiptap .toc-leader {
flex: 1 1 auto;
min-width: 1em;
margin: 0 4px;
border-bottom: 1px dotted currentColor;
transform: translateY(-0.25em);
}
.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 <img>, 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 (like Word). */
.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;
line-height: 1.15;
}
.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 <ol> 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, ".") ". ";
}
/* 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', 'Liberation 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; export emits them per side
(CELL_PADDING_X/_Y, expanded by expandCellPadding).
Tables are wrapped in <div class="tableWrapper"> 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 <col> 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: 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 (Word's model) 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);
}
/* 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;
}