.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 {
/* Driven by --user-page-height (pageOrientation.ts); fallback = A4 portrait. The
page geometry lives here, not on .tiptap, so both inherit the same values. */
--page-height: var(--user-page-height, 1123px);
--page-gap: 20px;
--page-cycle: calc(var(--page-height) + var(--page-gap));
/* The widest section's paper, so a landscape page is not cut at the sheet edge. */
width: var(--pb-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;
/* A frame or a formula wider than the text area is cut at the sheet's edge, as it is
in LibreOffice. `clip` rather than `hidden`: it must not become a scroll box. */
overflow-x: clip;
/* The sheets themselves are one box per page (PageSheetLayer), since a section on its
own paper makes them differ in size. They sit at z-index -2, under a header's page
background (.hf-bg-layer, -1) and so under the body text. */
}
/* While a freshly opened document is still finding its pagination (Editor.svelte).
Hidden, not removed: every pass measures the same boxes either way, and the sheet
.paper paints stays up, so the page appears once instead of shuffling into place. */
.paper.settling > * {
visibility: hidden;
}
.paper .tiptap {
/* 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);
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);
/* The page borders ride the sheet boxes (PageSheetLayer). */
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;
}
/* A section whose page setup has side margins of its own insets its top-level blocks by
the difference — .tiptap's padding can only draw one pair (pageBreaks.ts sets the two
properties per block). Everything that writes a block margin adds them in, so an indent
and the inset compose; a descendant clears them, or a nested one would add them twice. */
.paper .tiptap > * {
margin-left: var(--sec-inset-left, 0px);
margin-right: var(--sec-inset-right, 0px);
}
.paper .tiptap > * * {
--sec-inset-left: 0px;
--sec-inset-right: 0px;
}
/* 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;
}
/* One paragraph opting out of the document's automatic hyphenation (pageBreak.ts).
The host carries `hyphens: auto`, so only the paragraph has to say no. */
.paper .tiptap [data-no-hyphenation] {
hyphens: none;
}
/* A page-anchored frame is placed from its page corner and is out of the flow, so the
paragraph the importer hoists it into reserves no space (image.ts). !important: several
other paragraph rules below set their own `position: relative` (line-height shift, the
trailing-break spacer) — any of those would make this paragraph the frame's containing
block instead of the page, offsetting it by a page margin. */
.paper .tiptap p:has(> [data-anchor-page]) {
height: 0;
margin: 0;
position: static !important;
}
/* 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). */
/* The browser's own link colour is off first: a link Word left unstyled is drawn like the
text around it, since there the blue is a character style, not a property of being a
link (link.ts). Every other link — ODF paints them all — takes the visual back. */
.paper .tiptap a {
color: inherit;
text-decoration: none;
cursor: text;
}
.paper .tiptap a:not([data-plain]) {
color: #1a56db;
text-decoration: underline;
}
/* 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);
}
/* A ruby annotation (ruby.ts). The reading sits above its base at half its size,
which is what both word processors print and what `w:hps` says. */
.paper .tiptap ruby {
ruby-position: over;
}
.paper .tiptap ruby rt {
font-size: 0.5em;
line-height: 1;
}
/* The word being offered (wordCompletion.ts): after the caret, in the text's own
font, greyed — it is not in the document until Enter takes it. */
.paper .tiptap .word-completion {
opacity: 0.45;
user-select: none;
}
/* 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;
/* The last one: an entry whose heading carries a hard break is several lines, and the
leader and page number belong beside the last of them. */
align-items: last baseline;
cursor: pointer;
}
.paper .tiptap .toc-entry:hover .toc-text {
text-decoration: underline;
}
/* One level = 0.5cm, the indent LibreOffice's own Contents 1…5 styles carry — the
fallback for an index whose file names no per-level style (that one brings its own). */
.paper .tiptap .toc-level-2:not([data-style]) { padding-left: 0.5cm; }
.paper .tiptap .toc-level-3:not([data-style]) { padding-left: 1cm; }
.paper .tiptap .toc-level-4:not([data-style]) { padding-left: 1.5cm; }
.paper .tiptap .toc-level-5:not([data-style]) { padding-left: 2cm; }
.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;
}
/* Footnotes and endnotes (notes.ts). Sizes and indents are LibreOffice's own Footnote
style; the separator values follow its style:footnote-sep — see
docs/architecture/notes.md, all of them probed. The dialog overrides them on :root. */
/* Registered, so getComputedStyle resolves them to px: pageBreaks.ts reserves the
separator's own band out of the page and an unregistered var reads back as "0.1cm". */
@property --note-sep-above { syntax: ''; inherits: true; initial-value: 0.1cm; }
@property --note-sep-weight { syntax: ''; inherits: true; initial-value: 0.5pt; }
@property --note-sep-below { syntax: ''; inherits: true; initial-value: 0.1cm; }
.paper .tiptap .note-ref {
/* style:text-position="super 58%" on LibreOffice's Footnote anchor style. */
vertical-align: super;
font-size: 58%;
line-height: 0;
cursor: pointer;
}
/* Padding, not a margin, for the hanging indent: the box then still spans the whole
text width, which is what the separator's 25% is measured against. LibreOffice's own
Footnote style, and only for a note whose file names none of its own. */
.paper .tiptap .note {
padding-left: 0.6cm;
text-indent: -0.6cm;
}
.paper .tiptap .note:not([data-style]) {
font-size: 10pt;
}
/* The note's own marker is plain text at the baseline — LibreOffice's Footnote Symbol
style carries nothing, only the anchor is raised. An inline-block the width of the
hanging indent stands in for the tab LibreOffice puts after it. */
.paper .tiptap .note-marker {
display: inline-block;
min-width: 0.6cm;
text-indent: 0;
}
/* A note style declares its indent as a hanging pair (fo:margin-left with a negative
fo:text-indent) and the style model has no first-line half, so its margin alone would
just push the note right. The hanging pair stays LibreOffice's; the font, size and
colour are the file's. */
.paper .tiptap .note[data-style] {
margin-left: 0;
margin-right: 0;
}
/* A footnote is lifted out of the flow to the foot of its anchor's page; pageBreaks.ts
sets `top` in document px and marks the first note of each page. The margin custom
properties are .tiptap's own, so the box spans exactly the text width. */
.paper .tiptap .note[data-note-kind='footnote'] {
position: absolute;
left: var(--page-margin-left);
right: var(--page-margin-right);
}
/* Collected at the end of the document instead (noteSettings.ts): the footnotes rejoin
the flow and open their own page, where the endnote list also starts. */
:root[data-collect-footnotes] .paper .tiptap .note[data-note-kind='footnote'] {
position: static;
}
/* The separator hangs above the first note of a page, inside the band pageBreaks.ts
already reserved for it — so it costs the note's own box no height, which is what
keeps that height out of the reservation that produced it. A gradient, not a
border-top, because the line is only a quarter of the text width. */
.paper .tiptap .note[data-note-page-first]::after {
content: '';
position: absolute;
left: 0;
right: 0;
top: calc(-1 * (var(--note-sep-weight) + var(--note-sep-below)));
/* Drawn at least one device pixel: 0.5pt is 0.66px, which Chromium rounds away to
nothing. The reservation above still uses the true weight, as a hairline border
does — the line is a pixel wider than it measures, not the space it sits in. */
height: max(1px, var(--note-sep-weight));
/* The two non-length values carry their own default: only a registered property
takes one from @property, and the dialog writes all of them onto :root. */
background-image: linear-gradient(var(--note-sep-color, #000), var(--note-sep-color, #000));
background-size: var(--note-sep-rel-width, 25%) 100%;
background-repeat: no-repeat;
background-position: var(--note-sep-align, left) top;
}
/* Formulas (formula.ts). MathML needs a font with an OpenType MATH table or Chromium
drops ∑/ϕ and won't stretch brackets — STIX Two Math is bundled for that. */
/* The family must be set on |