Files
edentext/src/styles/global.css
T
Steffen Becker ab8b887889 fix: a page set in Arial is not rendered in the fallback serif
Import maps a document's Arial to Liberation Sans (screenFontName) and the
model keeps it under that name, but only 'Arial' had an @font-face — so on a
machine without Liberation Sans installed every such run fell to the browser's
default serif. The bundled TTFs are now declared under their own family too,
as Liberation Serif already was.
Measured against LibreOffice on a 28-page fixture: its cover lines came out
7-16% narrow, 54 reports to 44.
2026-09-04 19:30:05 +02:00

687 lines
22 KiB
CSS

*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Liberation Serif — bundled so the editor renders the EXACT metrics of the
font the exported .odt uses. odf-kit emits Liberation Serif as the document
default, and LibreOffice ships the identical file; this keeps the on-screen
preview pixel-faithful to the export. (SIL Open Font License — see
public/font-licenses/LICENSE-LiberationFonts.txt.) */
@font-face {
font-family: 'Liberation Serif';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../assets/fonts/LiberationSerif-Regular.ttf') format('truetype');
}
@font-face {
font-family: 'Liberation Serif';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('../assets/fonts/LiberationSerif-Bold.ttf') format('truetype');
}
@font-face {
font-family: 'Liberation Serif';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('../assets/fonts/LiberationSerif-Italic.ttf') format('truetype');
}
@font-face {
font-family: 'Liberation Serif';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('../assets/fonts/LiberationSerif-BoldItalic.ttf') format('truetype');
}
/* Liberation Sans under its own name too: import maps a document's Arial to it
(screenFontName), so a run carries the bare family and a machine without the
font installed would set an Arial page in the fallback serif. */
@font-face {
font-family: 'Liberation Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../assets/fonts/LiberationSans-Regular.ttf') format('truetype');
}
@font-face {
font-family: 'Liberation Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('../assets/fonts/LiberationSans-Bold.ttf') format('truetype');
}
@font-face {
font-family: 'Liberation Sans';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('../assets/fonts/LiberationSans-Italic.ttf') format('truetype');
}
@font-face {
font-family: 'Liberation Sans';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('../assets/fonts/LiberationSans-BoldItalic.ttf') format('truetype');
}
/* Bullet-marker symbols Liberation Serif lacks (❖ ➢ ⇨ ✓ ▸ ◆), subset from DejaVu
Sans so markers render compact and identical on every platform. First in the
::marker/picker stacks; per-glyph fallback keeps all other chars in the text
font. (Bitstream Vera license — see public/font-licenses/LICENSE-DejaVu.txt.) */
@font-face {
font-family: 'EdenText Symbols';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../assets/fonts/EdenSymbols.woff2') format('woff2');
}
/* STIX Two Math — a font with an OpenType MATH table is what makes MathML typeset:
without one Chromium drops ∑ and ϕ entirely and leaves brackets glyph-height next
to a fraction. Word uses Cambria Math for the same job. (OFL — see
public/font-licenses/LICENSE-STIX.txt.) */
@font-face {
font-family: 'STIX Two Math';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('STIX Two Math'), url('../assets/fonts/STIXTwoMath-Regular.woff2') format('woff2');
}
/* Metric-compatible substitutes for the common MS-Office fonts that imported
.odt files reference. Each face is keyed to the MS name but prefers a real
local copy, then a system twin, then the bundled open-source twin — so text
renders with matching metrics (like LibreOffice) instead of dropping to the
browser's default serif. (OFL — see public/font-licenses/LICENSE-*.) */
/* Calibri → Carlito */
@font-face {
font-family: 'Calibri';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Calibri'), local('Carlito'), url('../assets/fonts/Carlito-Regular.ttf') format('truetype');
}
@font-face {
font-family: 'Calibri';
font-style: normal;
font-weight: 700;
font-display: swap;
src: local('Calibri Bold'), local('Carlito Bold'), url('../assets/fonts/Carlito-Bold.ttf') format('truetype');
}
@font-face {
font-family: 'Calibri';
font-style: italic;
font-weight: 400;
font-display: swap;
src: local('Calibri Italic'), local('Carlito Italic'), url('../assets/fonts/Carlito-Italic.ttf') format('truetype');
}
@font-face {
font-family: 'Calibri';
font-style: italic;
font-weight: 700;
font-display: swap;
src: local('Calibri Bold Italic'), local('Carlito Bold Italic'), url('../assets/fonts/Carlito-BoldItalic.ttf') format('truetype');
}
/* Calibri Light → Carlito. Word's default theme heading font; Carlito has no light
cut, so LibreOffice renders it with the regular one too. */
@font-face {
font-family: 'Calibri Light';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Calibri Light'), local('Carlito'), url('../assets/fonts/Carlito-Regular.ttf') format('truetype');
}
@font-face {
font-family: 'Calibri Light';
font-style: normal;
font-weight: 700;
font-display: swap;
src: local('Calibri Light'), local('Carlito Bold'), url('../assets/fonts/Carlito-Bold.ttf') format('truetype');
}
@font-face {
font-family: 'Calibri Light';
font-style: italic;
font-weight: 400;
font-display: swap;
src: local('Calibri Light Italic'), local('Carlito Italic'), url('../assets/fonts/Carlito-Italic.ttf') format('truetype');
}
/* Arial → Liberation Sans */
@font-face {
font-family: 'Arial';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Arial'), local('Liberation Sans'), url('../assets/fonts/LiberationSans-Regular.ttf') format('truetype');
}
@font-face {
font-family: 'Arial';
font-style: normal;
font-weight: 700;
font-display: swap;
src: local('Arial Bold'), local('Liberation Sans Bold'), url('../assets/fonts/LiberationSans-Bold.ttf') format('truetype');
}
@font-face {
font-family: 'Arial';
font-style: italic;
font-weight: 400;
font-display: swap;
src: local('Arial Italic'), local('Liberation Sans Italic'), url('../assets/fonts/LiberationSans-Italic.ttf') format('truetype');
}
@font-face {
font-family: 'Arial';
font-style: italic;
font-weight: 700;
font-display: swap;
src: local('Arial Bold Italic'), local('Liberation Sans Bold Italic'), url('../assets/fonts/LiberationSans-BoldItalic.ttf') format('truetype');
}
/* Cambria → Caladea */
@font-face {
font-family: 'Cambria';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Cambria'), local('Caladea'), url('../assets/fonts/Caladea-Regular.ttf') format('truetype');
}
@font-face {
font-family: 'Cambria';
font-style: normal;
font-weight: 700;
font-display: swap;
src: local('Cambria Bold'), local('Caladea Bold'), url('../assets/fonts/Caladea-Bold.ttf') format('truetype');
}
@font-face {
font-family: 'Cambria';
font-style: italic;
font-weight: 400;
font-display: swap;
src: local('Cambria Italic'), local('Caladea Italic'), url('../assets/fonts/Caladea-Italic.ttf') format('truetype');
}
@font-face {
font-family: 'Cambria';
font-style: italic;
font-weight: 700;
font-display: swap;
src: local('Cambria Bold Italic'), local('Caladea Bold Italic'), url('../assets/fonts/Caladea-BoldItalic.ttf') format('truetype');
}
/* Courier New → Liberation Mono */
@font-face {
font-family: 'Courier New';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Courier New'), local('Liberation Mono'), url('../assets/fonts/LiberationMono-Regular.ttf') format('truetype');
}
@font-face {
font-family: 'Courier New';
font-style: normal;
font-weight: 700;
font-display: swap;
src: local('Courier New Bold'), local('Liberation Mono Bold'), url('../assets/fonts/LiberationMono-Bold.ttf') format('truetype');
}
@font-face {
font-family: 'Courier New';
font-style: italic;
font-weight: 400;
font-display: swap;
src: local('Courier New Italic'), local('Liberation Mono Italic'), url('../assets/fonts/LiberationMono-Italic.ttf') format('truetype');
}
@font-face {
font-family: 'Courier New';
font-style: italic;
font-weight: 700;
font-display: swap;
src: local('Courier New Bold Italic'), local('Liberation Mono Bold Italic'), url('../assets/fonts/LiberationMono-BoldItalic.ttf') format('truetype');
}
/* Times New Roman → Liberation Serif (reuses the bundled Serif files) */
@font-face {
font-family: 'Times New Roman';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Times New Roman'), local('Liberation Serif'), url('../assets/fonts/LiberationSerif-Regular.ttf') format('truetype');
}
@font-face {
font-family: 'Times New Roman';
font-style: normal;
font-weight: 700;
font-display: swap;
src: local('Times New Roman Bold'), local('Liberation Serif Bold'), url('../assets/fonts/LiberationSerif-Bold.ttf') format('truetype');
}
@font-face {
font-family: 'Times New Roman';
font-style: italic;
font-weight: 400;
font-display: swap;
src: local('Times New Roman Italic'), local('Liberation Serif Italic'), url('../assets/fonts/LiberationSerif-Italic.ttf') format('truetype');
}
@font-face {
font-family: 'Times New Roman';
font-style: italic;
font-weight: 700;
font-display: swap;
src: local('Times New Roman Bold Italic'), local('Liberation Serif Bold Italic'), url('../assets/fonts/LiberationSerif-BoldItalic.ttf') format('truetype');
}
:root {
--radius: 6px;
--island-radius: 14px; /* corner radius of the floating toolbar card */
--toolbar-btn-size: 1.7rem; /* height of all toolbar controls (matches the floating toolbars) */
--shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-serif: 'Liberation Serif', 'Times New Roman', Georgia, serif;
/* Headings are sans, like LibreOffice's Heading style ('Arial' is the bundled
Liberation Sans; see HEADING_FONT in export/odt.ts). */
--font-heading: 'Arial', 'Liberation Sans', sans-serif;
/* The logo's two colours (favicon.svg: .slate / .clay), one source of truth for
both chromes. The gradient is the app's signature (island hairline). */
--brand-slate: #4c6c8a;
--brand-clay: #d1734f;
--accent-warm: var(--brand-clay);
--brand-gradient: linear-gradient(90deg, var(--color-primary), var(--accent-warm));
}
/* The ribbon's palette: Word's own greys, our own accent — the logo's slate, which
carries white and clears 4.5:1 on the chrome. Its own token set rather than the
app's, so the two chromes can look nothing alike while sharing a theme. */
:root {
--w-accent: var(--brand-slate);
--w-accent-dark: #3a5570;
--w-accent-fill: var(--brand-slate);
--w-accent-fill-open: #3a5570;
/* The logo's other colour, on the tabs that only exist for the selected object.
Darkened for the label; the bar above it takes the brand tone itself. */
--w-contextual: #a85a3a;
--w-chrome: #ffffff;
--w-surface: #ffffff;
--w-border: #e3e6ea;
--w-border-strong: #d9d9d9;
--w-text: #242424;
--w-text-dim: #605e5c;
--w-text-tertiary: #909499;
--w-hover: #f3f2f1;
--w-pressed: #edebe9;
--w-active: #e2e0de;
--w-accent-soft: #eaeff4;
--w-menu-shadow: 0 8px 24px rgb(0 0 0 / 18%);
--w-font: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
/* Body height: three control rows plus the group-label row, and a big button's
label may take two of them. A tuning knob. */
--w-ribbon-h: 84px;
}
/* Word's dark mode: the Fluent dark neutrals, and both brand tones lightened
because they are unreadable on a dark chrome. Only the colours flip — every
metric above holds, so the two modes are the same ribbon. */
[data-theme='dark'], [data-theme='allBlack'] {
--w-accent: #8fb3cd;
--w-accent-dark: #6e93b0;
--w-accent-fill: #3c586f;
--w-accent-fill-open: #4c6c8a;
--w-contextual: #e2916f;
--w-chrome: #1f1f1f;
--w-surface: #292929;
--w-border: #383838;
--w-border-strong: #4a4a4a;
--w-text: #ffffff;
--w-text-dim: #c8c6c4;
--w-text-tertiary: #8a8886;
--w-hover: #3d3d3d;
--w-pressed: #333333;
--w-active: #4a4a4a;
--w-accent-soft: #2b3b49;
--w-menu-shadow: 0 8px 24px rgb(0 0 0 / 45%);
}
/* AllBlack darkens the canvas past dark, so the ribbon follows with Word's Black
theme rather than sitting a shade lighter than the page behind it. */
[data-theme='allBlack'] {
--w-chrome: #0f0f0f;
--w-surface: #1a1a1a;
--w-border: #2b2b2b;
--w-border-strong: #3d3d3d;
--w-hover: #2e2e2e;
--w-pressed: #262626;
--w-active: #3d3d3d;
--w-accent-soft: #212e39;
}
/* Signature micro-interactions, shared across all toolbar components. */
@keyframes menu-in {
from { opacity: 0; transform: translateY(-5px); }
}
.toolbar-region button:active:not(:disabled) {
transform: scale(0.93);
}
.theme-dropdown, .ol-dropdown, .align-dropdown, .font-dropdown, .size-dropdown,
.color-dropdown, .char-dropdown, .history-dropdown, .layout-dropdown,
.lh-dropdown, .sp-dropdown, .table-dropdown, .bp-dropdown, .ribbon-menu {
animation: menu-in 0.14s ease;
}
/* The ribbon's dropdown panel and its rows. Global because the panel renders its
contents from a caller's snippet, which Svelte scopes to that caller. */
/* `anchored` (menu.svelte.ts) switches this to position: fixed and pins it under
the trigger — the ribbon band clips its own overflow, so a panel has to leave
the flow to be seen. z-index clears the modern island (200) and the editor's
own overlays. */
.ribbon-menu {
position: absolute;
top: calc(100% + 3px);
left: 0;
z-index: 300;
display: flex;
flex-direction: column;
min-width: 180px;
padding: 5px;
background: var(--w-surface);
border: 1px solid var(--w-border-strong);
border-radius: 6px;
box-shadow: var(--w-menu-shadow);
font-family: var(--w-font);
color: var(--w-text);
}
.ribbon-menu.right { left: auto; right: 0; }
.ribbon-menu button {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 7px 12px;
border: none;
background: none;
border-radius: 4px;
color: var(--w-text);
font-family: var(--w-font);
font-size: 13px;
text-align: left;
white-space: nowrap;
cursor: pointer;
}
.ribbon-menu button:hover:not(:disabled) { background: var(--w-hover); }
.ribbon-menu button:disabled { color: var(--w-text-tertiary); cursor: default; }
.ribbon-menu button.selected { background: var(--w-active); }
/* Both trail the row's label: the shortcut hint and the secondary description
Word puts at the right edge of a menu entry. */
.ribbon-menu .menu-key,
.ribbon-menu .menu-sub { margin-left: auto; padding-left: 16px; color: var(--w-text-dim); }
.ribbon-menu .menu-key { font-size: 12px; }
/* A checkable row keeps the slot whether it is checked or not, so the labels line up. */
.ribbon-menu .menu-check {
flex: none;
width: 14px;
color: var(--w-accent);
}
.ribbon-menu .menu-sub { font-size: 11px; }
/* Section label above a run of rows, and the sticky action row below the list. */
.ribbon-menu .rb-menu-label {
padding: 6px 12px 3px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--w-text-tertiary);
user-select: none;
}
.ribbon-menu .rb-menu-foot {
flex-shrink: 0;
margin: 4px -5px -5px;
width: auto;
border-top: 1px solid var(--w-border);
border-radius: 0 0 5px 5px;
font-weight: 600;
color: var(--w-accent);
}
/* Between two ribbon groups, and between two runs of buttons inside one. */
.ribbon-sep {
width: 1px;
flex-shrink: 0;
background: var(--w-border);
margin: 2px 6px;
}
.rb-mini-sep {
width: 1px;
height: 20px;
flex-shrink: 0;
background: var(--w-border);
margin: 0 4px;
}
/* The reused pickers style their trigger for the modern toolbar's tinted island:
a frame on hover, the accent as the open state. Beside a row of ribbon buttons
that reads as a different control, so both become the ribbon's own tints. */
.ribbon .table-trigger:hover,
.ribbon .char-trigger:hover,
.ribbon .dt-trigger:hover,
.ribbon .tsp-trigger:hover,
.ribbon .bp-trigger:hover { border-color: transparent; }
.ribbon .table-trigger[aria-pressed='true'],
.ribbon .char-trigger[aria-pressed='true'],
.ribbon .dt-trigger[aria-pressed='true'],
.ribbon .tsp-trigger[aria-pressed='true'],
.ribbon .bp-trigger[aria-pressed='true'] {
background: var(--w-active);
color: var(--w-text);
}
/* A picker standing alone in a group is a labelled button, so its own compact
trigger grows to the big variant's box and icon size. The stroke is pinned to
the same 1.5px the ribbon's own icons paint at 28px. */
.rb-captioned {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
padding: 2px 6px 3px;
border-radius: 4px;
cursor: pointer;
--toolbar-btn-size: 32px;
}
/* Mirrors .rb-big's box, down to the slot it holds open for a caret, so a picker
and a labelled button in the same row put their glyph and their words on one
line. Their triggers carry their own frame and padding, which is dropped. */
/* A picker root is a block, so its inline-flex trigger would sit on a text
baseline and carry the descender gap with it. */
.rb-captioned > * { display: flex; }
.rb-captioned :is(.table-trigger, .char-trigger, .dt-trigger, .tsp-trigger, .bp-trigger, .lang-picker, .shape-btn) {
height: 32px;
min-height: 32px;
padding: 0;
margin: 0;
}
.rb-captioned::after {
content: '';
display: block;
height: 8px;
margin-top: -3px;
}
/* The trigger lives in the picker's own component, so the tint goes on the wrapper
instead — glyph and caption light up together, as they do on a ribbon button.
`captionClicks` gives the caption the click to match. */
.rb-captioned:hover { background: var(--w-hover); }
.rb-captioned:hover :is(.table-trigger, .char-trigger, .dt-trigger, .tsp-trigger, .bp-trigger, .color-split) {
background: none;
}
/* The first svg is the glyph; a second one is the trigger's own caret and keeps
the size every other caret in the ribbon has. */
.rb-captioned > * > button > svg:first-of-type { width: 28px; height: 28px; }
.rb-captioned > * > button > svg:first-of-type :is(path, rect, line, circle, polyline) { stroke-width: 0.86; }
.rb-captioned .color-bar { width: 24px; }
/* The spell-check picker is chromeless for the status bar; in a group it needs
the combo frame and the same box as the labelled buttons beside it. */
.rb-captioned .lang-picker {
padding: 0 6px;
border: 1px solid var(--w-border-strong);
border-radius: 3px;
background: var(--w-surface);
}
.rb-captioned .lang-picker:hover,
.rb-captioned .lang-picker:focus-within { border-color: var(--w-accent); }
.rb-caption {
font-family: var(--w-font);
font-size: 12px;
line-height: 14px;
color: var(--w-text);
white-space: nowrap;
}
/* Combo box: a borderless input welded to a caret button, as in Word's font and
size fields. */
.rb-combo-wrap { position: relative; }
.rb-combo {
display: flex;
align-items: stretch;
height: 30px;
border: 1px solid var(--w-border-strong);
border-radius: 3px;
background: var(--w-surface);
overflow: hidden;
}
.rb-combo:hover,
.rb-combo:focus-within { border-color: var(--w-accent); }
.rb-combo-input {
flex: 1;
min-width: 0;
border: none;
background: none;
padding: 0 4px;
color: var(--w-text);
font-family: var(--w-font);
font-size: 12px;
}
.rb-combo-input:focus { outline: none; }
.rb-combo-caret {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
border: none;
border-left: 1px solid var(--w-border);
background: none;
color: var(--w-text-dim);
cursor: pointer;
}
.rb-combo-caret:hover { background: var(--w-hover); }
.ribbon-menu hr { border: none; border-top: 1px solid var(--w-border); margin: 4px 2px; }
/* Scrolling body of a popup menu. Only this inner box scrolls; the panel keeps
the background and frame, so the elastic overscroll bounce can't drag the
background off the border and show the page through the gap. */
.menu-scroll {
display: flex;
flex-direction: column;
min-height: 0;
overflow-y: auto;
}
@media (prefers-reduced-motion: reduce) {
.toolbar-region button:active:not(:disabled) { transform: none; }
.theme-dropdown, .ol-dropdown, .align-dropdown, .font-dropdown, .size-dropdown,
.color-dropdown, .char-dropdown, .history-dropdown, .layout-dropdown,
.lh-dropdown, .sp-dropdown, .table-dropdown, .bp-dropdown, .ribbon-menu { animation: none; }
}
[data-theme="light"] {
--color-bg: #e6e9eb;
--color-surface: #ffffff;
--color-border: #e2e8f0;
--color-text: #1e293b;
--color-text-muted: #94a3b8;
--color-primary: var(--brand-slate);
--color-toolbar-bg: #ffffff;
--color-btn-hover: #cbd5e1;
--color-page-bg: #ffffff;
/* The sheet is the document, so its text is the black LibreOffice and Word draw —
the app's own slate would differ from a run the file colours #000000. */
--color-page-text: #000000;
--color-page-border: #e2e8f0;
--scrollbar-thumb: #cbd5e1;
}
[data-theme="dark"] {
--color-bg: #2f2f31;
--color-surface: #161b22;
--color-border: #30363d;
--color-text: #f2e6f3;
--color-text-muted: #7d8590;
--color-primary: var(--brand-slate);
--color-toolbar-bg: #161b22;
--color-btn-hover: #3b4350;
--color-page-bg: #ffffff;
--color-page-text: #000000;
--color-page-border: #e2e8f0;
/* Primary (active-button color) so the toolbar scrollbar stands out on dark. */
--scrollbar-thumb: var(--color-primary);
--scrollbar-thumb-hover: color-mix(in srgb, var(--color-primary) 75%, white);
}
[data-theme="allBlack"] {
--color-bg: #0d1117;
--color-surface: #161b22;
--color-border: #30363d;
--color-text: #e6edf3;
--color-text-muted: #7d8590;
--color-primary: var(--brand-slate);
--color-toolbar-bg: #161b22;
--color-btn-hover: #3b4350;
--color-page-bg: #161b22;
--color-page-text: #e6edf3;
--color-page-border: #30363d;
--scrollbar-thumb: var(--color-primary);
--scrollbar-thumb-hover: color-mix(in srgb, var(--color-primary) 75%, white);
}
html, body {
height: 100%;
font-family: var(--font-sans);
background: var(--color-bg);
color: var(--color-text);
-webkit-font-smoothing: antialiased;
}
#app {
height: 100%;
display: flex;
flex-direction: column;
}