dorfteich/apps/web/src/styles/base.css
Claude Opus 5 76a5e92f2e
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 7m10s
CI / Build container images (pull_request) Successful in 4m12s
CI / Auth e2e pack (pull_request) Successful in 9m33s
CI / Import/export fidelity gate (pull_request) Successful in 1m16s
An instance had no way to look like itself: the top bar said "Dorfteich"
whatever the operator called their instance, `instance.name` was never
rendered in the running app at all, and there was no favicon anywhere —
`index.html` had no `<link rel="icon">` and `public/` held only fonts and
theme-init.js.

Where the line is drawn, and why:

- **The api never decodes an image.** Cropping, scaling and the conversion
  to PNG happen on a canvas in the browser; the api checks the PNG
  signature, reads the IHDR dimensions at their fixed offsets and enforces
  the caps. An image library would put a decoder in front of
  attacker-supplied bytes AND would have to be carried through the
  `--network none` offline build. Reading two big-endian integers is not
  decoding.
- **SVG is refused**, with its own error message rather than a generic
  "not a PNG": it can carry script, and serving it from our own origin
  would be a cross-site-scripting vector. An operator who tried one should
  learn that it is deliberate.
- **The crop is driven by number inputs, not by dragging.** A drag-only
  cropper excludes keyboard and switch users outright; a number input is
  arrow-key operable and screen-reader readable without any custom aria.
  The resulting pixel size is stated in text, not only drawn as a frame.
- **The variant is chosen by CSS, not JavaScript.** `theme-init.js` has
  already resolved `data-theme` before first paint, so the correct logo is
  the one painted rather than the one that appears after a flash. Without a
  dark variant the LIGHT logo carries both themes — the operator's own
  asset shown unchanged beats one they did not choose (the rule #307
  extends to ponds). The settings screen warns; it never blocks.
- **The favicon link is static, its resource dynamic.** index.html stays a
  static file and the api answers with the uploaded icon or a shipped
  default — that route must never 404, or the browser keeps its generic
  icon for good. The default is generated by a script from Node's own zlib
  (`gen-default-favicon.mjs`), for the same offline-build reason.
- Both favicon sizes are uploaded together: one source, one crop, so the
  tab icon and the home-screen icon can never disagree.
- Branding is served WITHOUT a session, because the login screen carries it
  and the browser fetches the favicon before anyone signs in. The admin
  screen says so — an operator may not expect their logo to be public.
- The metadata is not writable through the settings endpoint: it describes
  bytes on disk, and hand-writing it would claim an asset that is not
  there.

`./data/branding` follows the three-step rule #303 paid for: env default +
`data-dirs.ts` entry, compose volume (repo AND the stages on ONE), and the
`mkdir`/`chown` line in the api Dockerfile. `data-dirs.test.ts` is new and
closes the hole that made #303's variant invisible: the nightly archive
skips a missing directory WORDLESSLY, so the fence now demands that every
`*_DIR` the backup env declares actually travels in the archive. Verified
against the real defect — removing the line fails it by name.

Audit catalogue v1.7 (`branding.changed`), carrying `scope` from the start
so #307 is the same event with a different scope, not a second id.

Verified: api suite 103 files green (a lone `public-api` ECONNRESET under
local parallel load, green in isolation — the documented local flake);
branding suite 12 tests against a real directory; crop arithmetic unit
tests; a11y pack 11/11 in both schemes; /admin measured at 320px with the
new section (overflow 0); and the whole flow walked in the browser: upload
→ crop 780×180 → stored as 512×118 → logo in the sidebar linking home with
the instance name as its accessible name → topbar wordmark following
`instance.name` → light logo still shown under `data-theme="dark"`.
2026-08-01 19:28:11 +02:00

4284 lines
86 KiB
CSS

*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body,
#root {
height: 100%;
margin: 0;
}
body {
font-family: var(--font-body);
font-weight: var(--font-weight-body);
color: var(--color-text);
background: var(--color-bg);
line-height: 1.6;
}
h1,
h2,
h3,
h4 {
font-family: var(--font-heading);
font-weight: var(--font-weight-heading);
line-height: 1.25;
}
code,
pre {
font-family: var(--font-mono);
font-weight: var(--font-weight-mono);
}
/* A pond's content root (ADR 0016): sets --font-* (via PondFontScope) so its
* body text uses the pond's body font; headings and code re-resolve the same
* variables through the global rules above. */
.pond-font-scope {
font-family: var(--font-body);
font-weight: var(--font-weight-body);
}
a {
color: var(--color-accent);
}
button {
font: inherit;
}
/* Layout skeleton */
.app {
display: grid;
/* minmax: the topbar row may grow when its controls wrap on narrow
viewports (issue #165, WCAG 1.4.10). */
grid-template-rows: minmax(var(--topbar-height), auto) 1fr;
height: 100%;
}
.app-body {
display: flex;
min-height: 0;
/* Grid items default to a min-content minimum; without this the row can
force the whole app wider than the viewport (issue #100). */
min-width: 0;
}
.topbar {
display: flex;
align-items: center;
gap: var(--space-4);
padding: 0 var(--space-4);
/* Same #100 pattern as .app-body: without this the topbar's min-content
width sizes the shared grid column and forces page-level horizontal
scrolling on narrow viewports (issue #165, WCAG 1.4.10). */
min-width: 0;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg);
/* Own stacking context above the scrolling `.main` content: the nav comes
before `.app-body` in the DOM, so without this the sticky editor toolbar
(z-index 20 inside `.main`) paints over the nav's overflow "…" menu, hiding
its entries. 30 > the toolbar's 20; modals (1100+) still win. */
position: relative;
z-index: 30;
}
.topbar__brand {
font-family: var(--font-heading);
font-size: 1.1rem;
color: var(--color-text);
text-decoration: none;
}
.topbar__spacer {
flex: 1;
}
/* Reduced motion (#170): collapse the few CSS transitions to instant. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}
/* Skip link (#166, WCAG 2.4.1): first tab stop, visible only on focus. */
.skip-link {
position: absolute;
left: -9999px;
top: 0;
z-index: 1200;
padding: var(--space-2) var(--space-4);
background: var(--color-accent);
color: var(--color-accent-contrast);
border-radius: 0 0 var(--radius) 0;
text-decoration: none;
}
.skip-link:focus-visible {
left: 0;
}
/* Narrow viewports (issue #165, WCAG 1.4.10): the topbar wraps onto a
second row instead of pushing the page wider than the viewport; every
control stays visible and reachable. The search button drops to its
icon (the aria-label carries the name). The expanded sidebar becomes
an overlay above the content — 256 px beside a 320 px viewport would
leave the content a sliver and force page-level horizontal scrolling. */
@media (max-width: 40rem) {
.topbar {
flex-wrap: wrap;
row-gap: 0;
padding: var(--space-1) var(--space-2);
}
.topbar__search-text {
display: none;
}
.app-body {
position: relative;
}
.sidebar:not(.sidebar--collapsed) {
position: absolute;
inset: 0 auto 0 0;
z-index: 25;
box-shadow: 2px 0 8px rgb(0 0 0 / 0.15);
}
.sidebar-resizer {
display: none;
}
}
.sidebar {
width: var(--sidebar-width);
flex-shrink: 0;
/* Anchor for the absolutely positioned resize handle (#166). */
position: relative;
/* Column flex so the trash footer can pin to the bottom (margin-top: auto). */
display: flex;
flex-direction: column;
border-right: 1px solid var(--color-border);
background: var(--color-bg-subtle);
padding: var(--space-4);
overflow-y: auto;
transition:
margin-left 0.18s ease-out,
opacity 0.15s ease-out;
}
.sidebar--collapsed {
margin-left: calc(-1 * var(--sidebar-width));
opacity: 0;
}
/* Drag handle on the sidebar's right edge (issue #99). */
.sidebar-resizer {
/* Lives inside the nav landmark (#166); straddles the sidebar edge. */
position: absolute;
top: 0;
bottom: 0;
right: -3px;
width: 6px;
cursor: col-resize;
touch-action: none;
background: transparent;
z-index: 5;
}
.sidebar-resizer:hover,
.sidebar-resizer:focus-visible {
background: var(--color-accent);
outline: none;
}
.sidebar__hint {
color: var(--color-text-muted);
font-size: 0.9rem;
}
.sidebar__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
margin-bottom: var(--space-3);
}
.sidebar__pond-name {
font-size: 1rem;
margin: 0;
}
.sidebar__sort {
font: inherit;
font-size: 0.85rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-bg);
padding: var(--space-1) var(--space-2);
}
.sidebar__pages {
list-style: none;
margin: 0 0 var(--space-3);
padding: 0;
}
/* Folder view (issue #108): the page tree with collapsible nodes. */
.sidebar__view-toggle {
display: flex;
gap: var(--space-1);
margin-bottom: var(--space-2);
}
.sidebar__view-btn {
font: inherit;
font-size: 0.8rem;
padding: var(--space-1) var(--space-2);
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-bg);
color: var(--color-text-muted);
cursor: pointer;
}
.sidebar__view-btn--active {
color: var(--color-text);
font-weight: 600;
background: var(--color-surface);
}
/* Favorites (issue #132): the latching filter button carries a small star;
golden treatment for the TopBar toggle and favorite tree icons. */
.sidebar__view-btn--favorites {
display: inline-flex;
align-items: center;
gap: var(--space-1);
}
.sidebar__view-btn--favorites svg {
width: 0.85rem;
height: 0.85rem;
color: var(--color-favorite);
}
.icon-button--favorite {
color: var(--color-favorite);
}
.sidebar__tree-row {
display: flex;
align-items: center;
gap: var(--space-1);
min-width: 0;
}
.sidebar__tree-row .sidebar__page {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* In the tree, the nested children <ul> is a SECOND flex item of the
.sidebar__page-item <li>. Stack it below the parent row (#122) — the
base rule's flex row centered the parent vertically BESIDE its children. */
.sidebar__pages--tree .sidebar__page-item {
flex-direction: column;
align-items: stretch;
gap: 0;
}
.sidebar__tree-children {
/* Per-level indent: about two characters is plenty (#122). */
margin: 0 0 0 1.25em;
}
.sidebar__caret {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1rem;
border: 0;
background: none;
padding: 0;
cursor: pointer;
color: var(--color-text-muted);
transform: rotate(90deg);
transition: transform 0.12s ease;
}
.sidebar__caret svg {
width: 0.9rem;
height: 0.9rem;
}
.sidebar__caret--collapsed {
transform: rotate(0deg);
}
.sidebar__caret--leaf {
cursor: default;
}
/* Folder pages vs. leaf pages read differently at a glance (#122). */
.sidebar__page-icon {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
color: var(--color-text-muted);
}
/* Favorites carry a golden icon in the tree (issue #132) — after the base
rule, so the equal-specificity color override wins. */
.sidebar__page-icon--favorite {
color: var(--color-favorite);
}
.sidebar__page-icon svg {
width: 0.875rem;
height: 0.875rem;
}
/* Label view (issue #108): pages grouped under the label tree. */
.sidebar__label-group {
margin-bottom: var(--space-2);
}
.sidebar__label-heading {
display: flex;
align-items: center;
gap: var(--space-1);
margin: 0 0 var(--space-1);
font-size: 0.8rem;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.03em;
}
.sidebar__label-group .sidebar__pages {
margin-bottom: 0;
}
.sidebar__new-page-parent {
margin: 0 0 var(--space-2);
font-size: 0.8rem;
color: var(--color-text-muted);
}
/* Drop-into cue while dragging a page onto another (reparent, issue #109). */
.sidebar__page-item--drop-into > .sidebar__tree-row {
outline: 2px solid var(--color-accent);
outline-offset: -2px;
border-radius: var(--radius);
}
/* Minimal modal (issue #109: "Move to…" and the delete-choice dialog). */
.modal-overlay {
position: fixed;
inset: 0;
z-index: 1100;
background: rgb(0 0 0 / 40%);
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-4);
}
.modal {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: var(--space-4);
max-width: 28rem;
width: 100%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 8px 24px rgb(0 0 0 / 20%);
}
.modal__title {
margin: 0 0 var(--space-3);
font-size: 1.1rem;
}
.modal__actions {
display: flex;
align-items: center;
gap: var(--space-3);
margin-top: var(--space-3);
}
.modal__actions--stacked {
flex-direction: column;
align-items: stretch;
gap: var(--space-2);
}
.move-dialog__options {
list-style: none;
margin: 0;
padding: 0;
max-height: 40vh;
overflow-y: auto;
}
.move-dialog__option {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-1) 0;
cursor: pointer;
}
.move-dialog__option:has(input:disabled) {
color: var(--color-text-muted);
cursor: not-allowed;
}
.sidebar__page {
display: block;
/* Compact rows (#122): deep imported trees need to fit on one screen. */
padding: 0.125rem var(--space-2);
border-radius: var(--radius);
color: var(--color-text);
text-decoration: none;
font-size: 0.875rem;
}
.sidebar__page:hover {
background: var(--color-bg);
}
.sidebar__page--active {
background: var(--color-bg);
font-weight: 600;
}
.sidebar__new-page-form {
margin-top: var(--space-2);
}
.sidebar__new-page-form .field {
margin-bottom: var(--space-2);
}
.sidebar__new-page-actions {
display: flex;
align-items: center;
gap: var(--space-3);
}
/* The import control sits in the pinned footer icon row (#124); its
progress list floats ABOVE the row so the icons stay put. */
.sidebar__import {
position: relative;
display: inline-flex;
}
.sidebar__import-list {
list-style: none;
position: absolute;
bottom: calc(100% + var(--space-1));
left: 0;
width: 14rem;
margin: 0;
padding: var(--space-2);
display: flex;
flex-direction: column;
gap: var(--space-2);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
z-index: 30;
}
.sidebar__import-item {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: var(--space-2);
font-size: 0.85rem;
}
.sidebar__import-name {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar__import-status {
color: var(--color-text-muted);
}
.sidebar__import-item--failed .sidebar__import-status {
color: var(--color-danger);
}
.sidebar__import-actions {
display: flex;
gap: var(--space-2);
}
.pond-home {
max-width: 36rem;
}
.main-column {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.main {
flex: 1;
min-height: 0;
min-width: 0;
overflow-y: auto;
padding: var(--space-6) var(--space-8);
}
.icon-button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border: 1px solid transparent;
border-radius: var(--radius);
background: transparent;
cursor: pointer;
color: var(--color-text);
}
.icon-button svg {
width: 1.15rem;
height: 1.15rem;
}
.icon-button--active {
color: var(--color-accent);
background: var(--color-bg-subtle);
}
.icon-button:disabled {
opacity: 0.5;
cursor: default;
}
/* Page actions in the TopBar (issue #101). */
.topbar__page-actions {
display: flex;
align-items: center;
}
/* Live presence next to the page actions (issue #102). */
.topbar__presence {
display: flex;
align-items: center;
}
.page-actions {
display: flex;
align-items: center;
gap: var(--space-1);
}
.page-actions__more {
position: relative;
}
.page-actions__menu {
position: absolute;
right: 0;
top: calc(100% + 4px);
min-width: 14rem;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: 0 4px 12px rgb(0 0 0 / 0.08);
display: flex;
flex-direction: column;
z-index: 10;
}
.page-actions__menu a,
.page-actions__menu button {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
text-align: left;
background: none;
border: none;
color: var(--color-text);
text-decoration: none;
cursor: pointer;
font: inherit;
}
.page-actions__menu svg {
width: 1rem;
height: 1rem;
flex-shrink: 0;
}
.page-actions__menu a:hover,
.page-actions__menu button:hover:not(:disabled) {
background: var(--color-bg-subtle);
}
.page-actions__menu button:disabled {
color: var(--color-text-muted);
cursor: default;
}
.page-actions__menu button.page-actions__menu-danger {
color: var(--color-danger);
}
.icon-button:hover {
background: var(--color-bg-subtle);
border-color: var(--color-border);
}
.icon-button:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 1px;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-1) var(--space-3);
border-radius: 999px;
border: 1px solid var(--color-border);
font-size: 0.9rem;
}
.status-pill--ok {
color: var(--color-ok);
}
.status-pill--error {
color: var(--color-danger);
}
/* Forms */
.field {
display: block;
margin-bottom: var(--space-4);
}
.field__label {
display: block;
margin-bottom: var(--space-1);
font-size: 0.9rem;
color: var(--color-text-muted);
}
.field input,
.field select {
width: 100%;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border-input);
border-radius: var(--radius);
font: inherit;
background: var(--color-bg);
}
.field input:focus-visible,
.field select:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 1px;
}
.field__hint,
.field__error {
display: block;
margin-top: var(--space-1);
font-size: 0.85rem;
}
.field__hint {
color: var(--color-text-muted);
}
.field__error {
color: var(--color-danger);
}
/* VS-NfD deviation marking (issue #244): icon + text carry the meaning,
the colour only reinforces it (ADR 0017 — never colour alone). */
.vs-nfd-mark {
display: block;
margin-top: var(--space-1);
font-size: 0.85rem;
font-weight: 600;
color: var(--color-danger);
}
.vs-nfd-summary {
font-weight: 600;
}
.button {
display: inline-block;
padding: var(--space-2) var(--space-4);
border: none;
border-radius: var(--radius);
background: var(--color-accent);
color: var(--color-accent-contrast);
cursor: pointer;
text-decoration: none;
font: inherit;
}
.button:disabled {
opacity: 0.6;
cursor: default;
}
.linklike {
background: none;
border: none;
padding: 0;
color: var(--color-accent);
text-decoration: underline;
cursor: pointer;
font: inherit;
}
.form-banner {
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius);
margin-bottom: var(--space-4);
font-size: 0.95rem;
}
.form-banner--error {
border-color: var(--color-danger);
color: var(--color-danger);
}
.form-banner--ok {
border-color: var(--color-ok);
color: var(--color-ok);
}
/* Auth pages */
.auth-card {
max-width: 24rem;
margin: var(--space-8) auto;
}
.auth-card__links {
margin-top: var(--space-4);
font-size: 0.95rem;
}
/* OIDC sign-in (issue #214): the SSO button is a full-width link styled as
a button; the separator sits between it and the local form. */
.button--block {
display: block;
width: 100%;
text-align: center;
text-decoration: none;
box-sizing: border-box;
}
.auth-card__separator {
margin: var(--space-4) 0;
text-align: center;
color: var(--color-text-muted);
font-size: 0.9rem;
}
/* Legal pages + footer (issue #82) */
.app-footer {
display: flex;
align-items: center;
gap: var(--space-2);
flex-shrink: 0;
/* Wrap instead of widening the page on narrow viewports (#165). */
flex-wrap: wrap;
min-width: 0;
padding: var(--space-2) var(--space-8);
border-top: 1px solid var(--color-border);
background: var(--color-bg);
color: var(--color-text-muted);
font-size: 0.85rem;
}
.app-footer__spacer {
flex: 1;
}
.app-footer__status {
display: flex;
align-items: center;
}
/* Instance version next to the connection status (#126). */
.app-footer__version {
font-size: 0.8rem;
white-space: nowrap;
}
.legal-page {
max-width: 44rem;
}
.legal-page__placeholder {
color: var(--color-text-muted);
}
.legal-editor,
.markdown-field {
margin-bottom: var(--space-4);
}
.legal-editor textarea,
.markdown-field textarea {
width: 100%;
font-family: var(--font-mono);
font-size: 0.9rem;
}
.legal-editor__preview,
.markdown-field__preview {
margin-top: var(--space-3);
padding: var(--space-3);
border: 1px dashed var(--color-border);
border-radius: var(--radius);
}
/* First-run setup wizard (issue #81) */
.setup-shell {
min-height: 100vh;
padding: var(--space-4);
}
.setup-wizard {
max-width: 28rem;
}
.setup-progress {
margin-bottom: var(--space-1);
color: var(--color-text-muted);
font-size: 0.9rem;
}
.setup-progress-steps {
display: flex;
flex-wrap: wrap;
gap: var(--space-1) var(--space-3);
margin: 0 0 var(--space-6);
padding: 0;
list-style: none;
color: var(--color-text-muted);
font-size: 0.85rem;
}
.setup-progress-steps li[aria-current='step'] {
color: inherit;
font-weight: 600;
}
.setup-progress-steps__done {
color: var(--color-accent);
}
.setup-language-buttons {
display: flex;
gap: var(--space-3);
}
.button--outline {
background: transparent;
color: var(--color-accent);
box-shadow: inset 0 0 0 1px var(--color-accent);
}
.setup-actions {
display: flex;
align-items: center;
gap: var(--space-3);
margin-top: var(--space-4);
}
.setup-actions__spacer {
flex: 1;
}
.setup-choice {
display: flex;
align-items: flex-start;
gap: var(--space-2);
margin-bottom: var(--space-3);
}
.setup-choice__hint {
display: block;
}
.setup-smtp-detail {
color: var(--color-danger);
font-family: var(--font-mono);
font-size: 0.85rem;
overflow-wrap: anywhere;
}
.setup-summary dt {
margin-top: var(--space-3);
color: var(--color-text-muted);
font-size: 0.9rem;
}
.setup-summary dd {
margin: 0;
}
/* Settings */
.settings-section {
max-width: 36rem;
margin-bottom: var(--space-8);
padding-bottom: var(--space-6);
border-bottom: 1px solid var(--color-border);
}
/* Wrapper for tables that can outgrow a narrow viewport (issue #301). The
table keeps its own scrollbar; `tabindex` makes that scroll area reachable
by keyboard, which a bare overflow container is not. */
.table-scroll {
overflow-x: auto;
max-width: 100%;
/* Establishes the containing block for the table's absolutely positioned
descendants — the `.visually-hidden` column headings. Without it their
containing block is `.app-body`, so they escape this scroller's clipping,
keep their static position out at the table's right edge, and push the
PAGE (issue #301). Exactly 23px locally, 417px in CI, where different
font metrics make the table wider — one cause, two numbers. */
position: relative;
}
.table {
width: 100%;
border-collapse: collapse;
margin-bottom: var(--space-4);
}
.table th,
.table td {
text-align: left;
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--color-border);
font-size: 0.95rem;
}
.table th {
color: var(--color-text-muted);
font-weight: 400;
}
.badge {
display: inline-block;
margin-left: var(--space-2);
padding: 0 var(--space-2);
border-radius: 999px;
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
font-size: 0.8rem;
color: var(--color-text-muted);
}
/* User menu */
.user-menu {
position: relative;
}
.user-menu__trigger {
background: none;
border: 1px solid transparent;
border-radius: var(--radius);
padding: var(--space-1) var(--space-3);
cursor: pointer;
}
.user-menu__trigger:hover {
border-color: var(--color-border);
}
.user-menu__list {
position: absolute;
right: 0;
top: calc(100% + 4px);
min-width: 12rem;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: 0 4px 12px rgb(0 0 0 / 0.08);
display: flex;
flex-direction: column;
z-index: 10;
}
.user-menu__list a,
.user-menu__list button {
padding: var(--space-2) var(--space-3);
text-align: left;
background: none;
border: none;
color: var(--color-text);
text-decoration: none;
cursor: pointer;
font: inherit;
}
.user-menu__list a:hover,
.user-menu__list button:hover {
background: var(--color-bg-subtle);
}
.topbar__nav {
display: flex;
gap: var(--space-4);
}
/* Page editor (issue #25). Fills the content area — no width cap (issue
#100); the public read view keeps its own narrow reading layout. */
.editor-page {
min-width: 0;
}
.editor-page__header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-4);
margin-bottom: var(--space-4);
}
.editor-page__title {
flex: 1;
/* An input's intrinsic size-attribute width must not set the page's
minimum width on narrow viewports (#165). */
min-width: 0;
border: none;
background: none;
font-family: var(--font-heading);
font-weight: var(--font-weight-heading);
font-size: 1.6rem;
padding: var(--space-1) 0;
}
.editor-page__title:disabled {
color: var(--color-text);
opacity: 1;
}
.editor-page__title:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
/* Status line between page header and article (#134). Middot-separated,
muted; shared by the authenticated read view and the public view. */
.page-statusbar {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin-top: calc(-1 * var(--space-2));
margin-bottom: var(--space-4);
color: var(--color-text-muted);
font-size: 0.8125rem;
}
.page-statusbar__item {
display: inline-flex;
align-items: center;
}
.page-statusbar__item:not(:first-child)::before {
content: '·';
margin-right: var(--space-2);
color: var(--color-border);
}
/* The export entries flow inline with the other overflow-menu items. */
.editor-page__export {
display: contents;
}
.pond-export__hint {
color: var(--color-text-muted);
margin-bottom: var(--space-2);
}
/* Pond appearance (fonts) settings — issue #66 */
.appearance__intro {
color: var(--color-text-muted);
}
.appearance__slot {
display: flex;
flex-wrap: wrap;
align-items: end;
gap: var(--space-3);
padding: var(--space-3) 0;
border-top: 1px solid var(--color-border);
}
.appearance__slot-label {
flex-basis: 100%;
font-weight: 600;
}
.appearance__field {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.appearance__preview {
flex: 1 1 16rem;
margin: 0;
font-size: 1.1rem;
}
.appearance__actions {
display: flex;
align-items: center;
gap: var(--space-4);
margin-top: var(--space-3);
}
.font-catalog__table {
border-collapse: collapse;
width: 100%;
}
.font-catalog__table th,
.font-catalog__table td {
text-align: left;
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--color-border);
}
.editor-shell {
border: 1px solid var(--color-border);
border-radius: var(--radius);
}
/* Read mode drops the frame and its inner padding — the rendered page sits
directly in the content area (M10 follow-up). */
.editor-shell--reading {
border: none;
border-radius: 0;
}
.editor-shell--reading > .editor-content {
padding: 0;
}
.editor-toolbar {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--color-border);
background: var(--color-bg-subtle);
/* Long articles: the toolbar pins to the top of the scrolling content area
instead of scrolling away. The scroll container `.main` has a top padding
(`--space-6`); a plain `top: 0` would stick the toolbar at that padding
edge, leaving a visible gap below the nav. Offset by `-1 * --space-6` so it
pins flush against the navigation, with no scrolling page showing through. */
position: sticky;
top: calc(-1 * var(--space-6));
z-index: 20;
}
.editor-toolbar__group {
display: flex;
gap: var(--space-1);
padding-right: var(--space-3);
border-right: 1px solid var(--color-border);
}
.editor-toolbar__group:last-child {
border-right: none;
padding-right: 0;
}
.editor-toolbar__section-select {
height: 1.75rem;
max-width: 12rem;
padding: 0 var(--space-1);
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-bg);
color: var(--color-text);
font-size: 0.85rem;
}
/* A section whose plugin is disabled/removed is an invisible wrapper (its
* dt-style-… class matches no stylesheet). While writing, a faint dashed
* hint keeps every section findable, so "unwrap" has a visible target. */
.editor-content .dt-section {
border-radius: var(--radius);
outline: 1px dashed transparent;
outline-offset: 2px;
}
.editor-content:focus-within .dt-section {
outline-color: var(--color-border);
}
/* The page-tools panel (issue #77): active pageTool plugin surfaces behind
* disclosures; each iframe mounts lazily on first open. */
.page-tools {
margin: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius);
}
.page-tools__tool + .page-tools__tool {
border-top: 1px solid var(--color-border);
}
.page-tools__toggle {
display: block;
width: 100%;
padding: var(--space-2) var(--space-3);
border: none;
background: var(--color-bg-subtle);
color: var(--color-text);
text-align: left;
font-size: 0.9rem;
cursor: pointer;
}
.page-tools__tool .plugin-frame-host {
padding: var(--space-2) var(--space-3);
}
/* A plugin-owned block (issue #76): a framed island in the content column.
* The sandbox iframe sizes itself via ui.resize; the bar carries the plugin
* name and the edit affordance. */
.plugin-block {
margin: 0.75rem 0;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-bg);
}
.plugin-block--selected {
outline: 2px solid var(--color-accent);
outline-offset: 1px;
}
.plugin-block__bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
padding: var(--space-1) var(--space-2);
border-bottom: 1px solid var(--color-border);
background: var(--color-bg-subtle);
font-size: 0.8rem;
color: var(--color-text-muted);
}
.plugin-block__mount .plugin-frame {
display: block;
width: 100%;
border: none;
}
/* Read mode integrates plugin output like normal content: no frame, no
name bar — same principle as the frameless reading shell (M10). */
.editor-shell--reading .plugin-block {
border: none;
border-radius: 0;
background: transparent;
/* No selection outline either — reading is not editing. */
outline: none;
}
.editor-shell--reading .plugin-block__bar {
display: none;
}
.plugin-block__fallback {
padding: var(--space-2) var(--space-3);
}
.plugin-block__fallback-text {
margin: 0;
}
.plugin-block__fallback-note {
margin: var(--space-1) 0 0;
font-size: 0.8rem;
color: var(--color-text-muted);
}
.toolbar-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.75rem;
height: 1.75rem;
padding: 0 var(--space-1);
border: 1px solid transparent;
border-radius: var(--radius);
background: transparent;
cursor: pointer;
color: var(--color-text);
font-size: 0.9rem;
}
.toolbar-button:hover:not(:disabled) {
background: var(--color-bg);
border-color: var(--color-border);
}
.toolbar-button:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 1px;
}
.toolbar-button--active {
background: var(--color-accent);
color: var(--color-accent-contrast);
}
.toolbar-button:disabled {
opacity: 0.4;
cursor: default;
}
.editor-link-form {
display: flex;
gap: var(--space-1);
align-items: center;
}
.editor-link-form input {
padding: var(--space-1) var(--space-2);
border: 1px solid var(--color-border);
border-radius: var(--radius);
font: inherit;
font-size: 0.9rem;
}
.editor-link-form__error {
flex-basis: 100%;
font-size: 0.8rem;
color: var(--color-danger);
}
.editor-link-menu {
padding: var(--space-2);
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-bg);
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
}
.editor-link-menu__actions {
display: flex;
gap: var(--space-1);
}
/* The connection-status icon in the content footer (left half). */
.editor-connection {
display: inline-flex;
align-items: center;
color: var(--color-text-muted);
}
.editor-connection svg {
width: 1rem;
height: 1rem;
}
.editor-connection[data-status='connected'] {
color: var(--color-accent);
}
.editor-connection[data-status='offline'] {
color: var(--color-danger);
}
.presence-strip {
display: flex;
gap: calc(-1 * var(--space-1));
align-items: center;
padding: var(--space-1) var(--space-3);
}
.presence-avatar {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
margin-left: -0.4rem;
border: 2px solid var(--color-bg);
border-radius: 50%;
/* Deliberately NOT a theme token: USER_COLORS backgrounds are chosen so
white text clears 4.5:1 (user-color.test.ts) independent of the theme. */
color: #ffffff;
font-size: 0.7rem;
font-weight: 600;
}
.presence-avatar:first-child {
margin-left: 0;
}
.presence-avatar--overflow {
background: var(--color-text-muted);
}
.presence-avatar__ro {
position: absolute;
right: -0.15rem;
bottom: -0.15rem;
font-size: 0.75rem;
line-height: 1;
}
/* Remote collaboration carets and selections (issue #37). */
.collab-caret {
position: relative;
margin-left: -1px;
margin-right: -1px;
border-left: 1px solid;
border-right: 1px solid;
word-break: normal;
pointer-events: none;
}
.collab-caret--hidden {
display: none;
}
.collab-caret__label {
position: absolute;
top: -1.4em;
left: -1px;
padding: 0.05rem 0.3rem;
border-radius: 3px 3px 3px 0;
/* White on USER_COLORS, theme-independent — see .presence-avatar. */
color: #ffffff;
font-size: 0.7rem;
font-weight: 600;
white-space: nowrap;
user-select: none;
}
.collab-selection {
border-radius: 2px;
}
.editor-banner {
margin: var(--space-1) var(--space-3);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-sm, 4px);
font-size: 0.9rem;
}
.editor-banner--info {
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
color: var(--color-text-muted);
}
.editor-banner--error {
background: var(--color-bg-subtle);
border: 1px solid var(--color-danger);
color: var(--color-danger);
}
/* VS-NfD classification marking (issue #206, ADR 0022): rendered at the top
and bottom of every page representation. Deliberately built from the
normal text token only — full contrast in both themes and under every
accent, with no new color pair for the contrast fence. */
.classification-banner {
margin: var(--space-2) 0;
padding: var(--space-1) var(--space-2);
border-top: 2px solid currentColor;
border-bottom: 2px solid currentColor;
color: var(--color-text);
font-weight: 700;
letter-spacing: 0.08em;
text-align: center;
font-size: 0.9rem;
}
/* The print frame (issue #207) is a REAL table so browsers repeat its
thead/tfoot (the classification banners) on every printed page. On
screen the whole chain renders as plain blocks — layout and visual
order are exactly as without the table. */
.print-frame,
.print-frame > thead,
.print-frame > tbody,
.print-frame > tfoot,
.print-frame > * > tr,
.print-frame > * > tr > td {
display: block;
width: 100%;
border: none;
padding: 0;
margin: 0;
}
.editor-page__body {
display: flex;
gap: var(--space-4);
align-items: flex-start;
}
.editor-page__body > .editor-shell {
flex: 1 1 auto;
min-width: 0;
}
/* Column holding the open side panels next to the editor; each panel takes
the column's full width and they stack vertically (M10 follow-up). */
.editor-page__panels {
display: flex;
flex-direction: column;
gap: var(--space-4);
flex: 0 0 22rem;
max-width: 22rem;
}
.editor-page__panels > .label-picker,
.editor-page__panels > .history-panel {
flex: none;
max-width: none;
}
.history-panel {
flex: 0 0 22rem;
max-width: 22rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm, 4px);
padding: var(--space-3);
max-height: 80vh;
overflow-y: auto;
}
.history-panel__header {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-2);
}
.history-panel__header h2 {
font-size: 1rem;
margin: 0;
}
.history-panel__list {
list-style: none;
margin: var(--space-2) 0;
padding: 0;
}
.history-panel__item {
display: flex;
flex-direction: column;
width: 100%;
text-align: left;
gap: 2px;
padding: var(--space-2);
border: 1px solid transparent;
border-radius: var(--radius-sm, 4px);
background: none;
cursor: pointer;
color: inherit;
}
.history-panel__item:hover {
background: var(--color-bg-subtle);
}
.history-panel__item[aria-current='true'] {
border-color: var(--color-border);
background: var(--color-bg-subtle);
}
/* Owner-facing quick label management inside the picker (M10 follow-up). */
.label-picker__manage {
margin-top: var(--space-3);
padding-top: var(--space-3);
border-top: 1px solid var(--color-border);
font-size: 0.85rem;
}
.label-picker__create {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-2);
}
.label-picker__create input {
flex: 1;
min-width: 0;
}
.label-picker__error {
color: var(--color-danger);
font-size: 0.85rem;
}
/* Contributor names under each version entry (M10 follow-up). */
.history-panel__contributors {
display: block;
padding: 0 var(--space-2) var(--space-1);
font-size: 0.8rem;
color: var(--color-text-muted);
}
.history-panel__contributors-more {
font: inherit;
}
.history-panel__when {
font-weight: 600;
font-size: 0.9rem;
}
.history-panel__meta {
font-size: 0.8rem;
color: var(--color-text-muted);
}
.history-panel__error {
color: var(--color-danger);
}
.history-panel__diff {
white-space: pre-wrap;
word-break: break-word;
font-size: 0.85rem;
background: var(--color-bg-subtle);
padding: var(--space-2);
border-radius: var(--radius-sm, 4px);
}
.diff--added {
background: color-mix(in srgb, green 22%, transparent);
text-decoration: none;
}
.diff--removed {
background: color-mix(in srgb, red 22%, transparent);
text-decoration: line-through;
}
.history-panel__preview {
border-top: 1px solid var(--color-border);
padding-top: var(--space-2);
}
.access-revoked__title {
font-weight: 600;
margin: 0 0 var(--space-1);
}
.access-revoked__description {
margin: 0 0 var(--space-2);
}
.access-revoked__actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.editor-content {
padding: var(--space-4) var(--space-6);
min-height: 12rem;
}
.editor-content .ProseMirror {
outline: none;
}
.editor-content h1,
.editor-content h2,
.editor-content h3,
.editor-content h4 {
margin-top: var(--space-6);
margin-bottom: var(--space-2);
}
.editor-content blockquote {
margin: var(--space-4) 0;
padding-left: var(--space-4);
border-left: 3px solid var(--color-border);
color: var(--color-text-muted);
}
.editor-content pre {
padding: var(--space-3);
border-radius: var(--radius);
background: var(--color-bg-subtle);
overflow-x: auto;
}
.editor-content code {
background: var(--color-bg-subtle);
border-radius: 3px;
padding: 0 0.25em;
}
.editor-content pre code {
background: none;
padding: 0;
}
/* Task lists (checkbox lists). Styled globally via the unique
`data-type='task_list'` attribute — produced only by docToHtml and the
editor nodeview — so checkbox and text line up in the editor
(.editor-content) AND every read-mode container that injects docToHtml
output (.public-page__body, .comment__body, .legal-page__body, .home-page,
.history-panel__preview). Previously these rules were scoped to
.editor-content and never reached the read views. Issue #137. */
ul[data-type='task_list'] {
list-style: none;
padding-left: var(--space-2);
}
ul[data-type='task_list'] li {
display: flex;
align-items: flex-start;
gap: var(--space-2);
}
/* The checkbox is the top flex child; nudge it onto the first text line and
drop the leading/trailing margins of the item's paragraphs so the text meets
the checkbox instead of dropping a line. DESCENDANT selectors on purpose
(issue #137, second regression): the DOM depth differs per surface —
public/docToHtml renders `li > input` + `li > p`, but the editor/auth read
view (TipTap ReactNodeView) nests the paragraph TWO wrappers deep:
`li > div[data-node-view-content] > div > p`. Child-combinator chains keep
missing one of the shapes; matching any `p` inside the task item is the
robust form. `:first-of-type`/`:last-of-type` (NOT `:first-child`) because
the `<input>`/`<label>` precedes the paragraph in the read-mode markup. */
ul[data-type='task_list'] li > input[type='checkbox'],
ul[data-type='task_list'] li > label,
ul[data-type='task_list'] li > [data-node-view-wrapper] > label {
flex: none;
margin-top: 0.25em;
}
/* Editor/auth NodeView only (the bare-input shape has no label): its line
metrics sit the checkbox ~3px lower than in the public view, so pull the
label up by that much — tuned to Stefan's eye on the live stage (#137).
Since #169 the nodeview's label sits one display:contents wrapper deep
(`li > [data-node-view-wrapper] > label`); the extra selector keeps the
read/public shape (`li > label` never occurs there) untouched. */
ul[data-type='task_list'] li > [data-node-view-wrapper] > label {
margin-top: calc(0.25em - 3px);
}
ul[data-type='task_list'] li p:first-of-type {
margin-top: 0;
}
ul[data-type='task_list'] li p:last-of-type {
margin-bottom: 0;
}
.editor-content table {
border-collapse: collapse;
margin: var(--space-4) 0;
width: 100%;
}
.editor-content table td,
.editor-content table th {
border: 1px solid var(--color-border);
padding: var(--space-2) var(--space-3);
}
.editor-content table th {
background: var(--color-bg-subtle);
text-align: left;
}
.editor-content hr {
border: none;
border-top: 1px solid var(--color-border);
margin: var(--space-6) 0;
}
.editor-image {
position: relative;
display: inline-block;
max-width: 100%;
vertical-align: bottom;
}
.editor-image img {
max-width: 100%;
border-radius: var(--radius);
}
.editor-image__controls {
display: flex;
flex-direction: column;
gap: var(--space-2);
position: absolute;
top: 100%;
left: 0;
z-index: 1;
margin-top: var(--space-1);
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-bg);
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
white-space: nowrap;
}
.editor-image__alt {
display: flex;
flex-direction: column;
gap: var(--space-1);
font-size: 0.85rem;
color: var(--color-text-muted);
}
.editor-image__alt input {
padding: var(--space-1) var(--space-2);
border: 1px solid var(--color-border);
border-radius: var(--radius);
font: inherit;
font-size: 0.9rem;
}
.editor-image__widths {
display: flex;
gap: var(--space-1);
}
.editor-image-upload {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 3rem;
min-height: 2rem;
padding: var(--space-1) var(--space-2);
border: 1px dashed var(--color-border);
border-radius: var(--radius);
background: var(--color-bg-subtle);
color: var(--color-text-muted);
font-size: 0.85rem;
}
.editor-image-upload--error {
border-color: var(--color-danger);
color: var(--color-danger);
}
/* Pond trash (issue #31) */
.trash-page {
max-width: 48rem;
margin: 0 auto;
}
/* Multi-select toolbar above the list (#128). */
.trash-page__bulk {
display: flex;
align-items: center;
gap: var(--space-3);
margin-top: var(--space-4);
padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border);
}
.trash-page__select-all {
display: inline-flex;
align-items: center;
gap: var(--space-2);
margin-right: auto;
cursor: pointer;
}
.trash-page__bulk-error {
color: var(--color-danger);
}
.trash-page__list {
list-style: none;
margin: var(--space-4) 0 0;
padding: 0;
}
.trash-page__item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-2) 0;
border-bottom: 1px solid var(--color-border);
}
.trash-page__title {
flex: 1;
font-weight: var(--font-weight-heading);
}
.trash-page__deleted-at {
color: var(--color-text-muted);
font-size: 0.85rem;
}
/* Labels (issue #44) --------------------------------------------------- */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Colored chips on sidebar page entries. */
.label-chips {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
margin: var(--space-1) 0 0 var(--space-2);
}
.label-chip {
display: inline-block;
padding: 0 var(--space-2);
border-radius: 999px;
font-size: 0.72rem;
line-height: 1.5;
white-space: nowrap;
/* Invisible on light; separates arbitrary user label colors from the
dark canvas (issue #180). */
border: 1px solid var(--color-chip-outline);
}
.label-chip__swatch {
display: inline-block;
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
border: 1px solid var(--color-border);
flex: 0 0 auto;
}
/* Sidebar label filter. */
.sidebar__filter {
margin-bottom: var(--space-3);
font-size: 0.9rem;
}
.sidebar__filter > summary {
cursor: pointer;
color: var(--color-text-muted);
padding: var(--space-1) 0;
}
.sidebar__filter-list {
list-style: none;
margin: var(--space-1) 0;
padding: 0;
max-height: 14rem;
overflow-y: auto;
}
.sidebar__filter-option {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-1) 0;
cursor: pointer;
}
.sidebar__footer {
/* #129: .sidebar is the scroll container, so margin-top: auto alone only
pins the row to the END of the content — behind the fold on long page
lists. Sticky keeps it visible; the negative bottom/side margins undo
the sidebar padding so the bar sits flush and covers the scrolling
list across the full width. */
position: sticky;
bottom: calc(-1 * var(--space-4));
margin: auto calc(-1 * var(--space-4)) calc(-1 * var(--space-4));
padding: var(--space-2) var(--space-4);
background: var(--color-bg-subtle);
border-top: 1px solid var(--color-border);
display: flex;
align-items: center;
gap: var(--space-1);
}
/* Knowledge graph (issue #112) + local graph panel (#113). */
/* #131: the page fills the main column as a flex column, so the canvas can
take exactly the remaining viewport height — the page itself must never
grow a scrollbar (wheel-zoom would scroll along with it). */
.graph-page {
height: 100%;
display: flex;
flex-direction: column;
}
.graph-page__header {
display: flex;
align-items: baseline;
gap: var(--space-3);
flex-wrap: wrap;
}
.graph-page__counts {
margin: 0;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.graph-page__hint {
color: var(--color-text-muted);
font-size: 0.9rem;
}
.graph-page__canvas {
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
overflow: hidden;
flex: 1;
min-height: 0;
}
/* The pond graph SVG gets its measured container size as viewBox (#131), so
it may fill the canvas exactly; the local panel keeps width-scaled sizing. */
.graph-page__canvas .force-graph {
height: 100%;
}
/* Physics/rendering sliders (#123). */
.graph-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-2);
font-size: 0.85rem;
color: var(--color-text-muted);
}
.graph-controls__field {
display: inline-flex;
align-items: center;
gap: var(--space-1);
}
.graph-controls__field input[type='range'] {
width: 7rem;
}
.force-graph {
display: block;
width: 100%;
height: auto;
touch-action: none;
cursor: grab;
}
.force-graph__edge {
stroke: var(--color-border);
stroke-width: 1.2;
}
.force-graph__node {
cursor: pointer;
}
.force-graph__dot {
stroke-width: 2;
}
.force-graph__node--phantom .force-graph__dot {
stroke-dasharray: 3 3;
}
.force-graph__node--phantom .force-graph__label {
fill: var(--color-text-muted);
font-style: italic;
}
.force-graph__ring {
fill: none;
stroke: var(--color-accent);
stroke-width: 2;
}
.force-graph__label {
font-size: 11px;
fill: var(--color-text);
pointer-events: none;
user-select: none;
}
.graph-legend {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
margin-top: var(--space-3);
font-size: 0.85rem;
color: var(--color-text-muted);
}
.graph-legend__entry {
display: inline-flex;
align-items: center;
gap: var(--space-1);
}
.graph-legend__phantom-swatch {
width: 0.8rem;
height: 0.8rem;
border-radius: 50%;
border: 2px dashed var(--color-text-muted);
}
/* Obsidian vault import dialog (issue #118). */
.vault-import__hint {
color: var(--color-text-muted);
font-size: 0.9rem;
}
.vault-import-dialog__field {
display: block;
border: 0;
padding: 0;
margin: 0 0 var(--space-3);
}
.vault-import-dialog__field > legend,
.vault-import-dialog__field > span {
display: block;
font-weight: 600;
font-size: 0.9rem;
margin-bottom: var(--space-1);
}
.vault-import-dialog__field .move-dialog__options {
max-height: 12rem;
}
.vault-import-dialog__labels-empty {
color: var(--color-text-muted);
font-size: 0.9rem;
margin: 0 0 var(--space-1);
}
/* Inline label creation (#121): input + button side by side. */
.vault-import-dialog__label-create {
display: flex;
gap: var(--space-2);
margin-top: var(--space-1);
}
.vault-import-dialog__label-create input {
flex: 1;
min-width: 0;
}
/* "Create this page" on the not-found screen (issue #115). */
.create-missing-page {
margin-top: var(--space-3);
}
.create-missing-page__hint {
color: var(--color-text-muted);
font-size: 0.9rem;
margin: 0 0 var(--space-2);
}
/* Local neighborhood graph below the page content (issue #113). */
.local-graph {
margin-top: var(--space-4);
}
.local-graph > summary {
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
}
.local-graph__controls {
display: flex;
gap: var(--space-1);
margin: var(--space-2) 0;
}
/* Hop-depth slider (#123): replaces the two fixed depth buttons. */
.local-graph__hops {
display: inline-flex;
align-items: center;
gap: var(--space-2);
font-size: 0.85rem;
color: var(--color-text-muted);
}
.local-graph__hops input[type='range'] {
width: 7rem;
}
.local-graph__canvas {
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
max-width: 36rem;
overflow: hidden;
}
/* Pond settings page + label manager. */
.pond-settings-page {
max-width: 48rem;
}
/* Member management (issue #54) */
.member-manager__intro,
.member-manager__note,
.member-manager__empty {
color: var(--color-text-muted);
margin-bottom: var(--space-3);
}
.member-seats {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin: 0 0 var(--space-3);
padding: 0;
}
.member-seats__item {
background: var(--color-surface-muted);
border-radius: var(--radius-sm, 0.375rem);
padding: var(--space-1) var(--space-2);
font-size: 0.875rem;
}
.member-add {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: var(--space-2);
margin-bottom: var(--space-3);
}
.member-add__field {
display: flex;
flex-direction: column;
gap: var(--space-1);
font-size: 0.875rem;
}
.member-add__quota-full {
flex-basis: 100%;
color: var(--color-danger);
margin: 0;
font-size: 0.875rem;
}
.member-search {
width: 100%;
margin-bottom: var(--space-3);
}
.member-group {
margin-bottom: var(--space-4);
}
.member-group__title {
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-text-muted);
margin: 0 0 var(--space-2);
}
.member-group__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.member-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
padding: var(--space-2);
border: 1px solid var(--color-border, rgba(127, 127, 127, 0.25));
border-radius: var(--radius-sm, 0.375rem);
}
.member-row__username,
.member-row__owner-badge {
color: var(--color-text-muted);
}
.member-row__actions {
display: flex;
align-items: center;
gap: var(--space-2);
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.label-manager__description {
color: var(--color-text-muted);
margin-bottom: var(--space-3);
}
.label-manager__new-root {
display: flex;
gap: var(--space-2);
margin-bottom: var(--space-3);
}
.label-manager__error {
color: var(--color-danger);
margin-bottom: var(--space-3);
}
.label-manager__empty {
color: var(--color-text-muted);
}
.label-tree {
list-style: none;
margin: 0;
padding: 0;
}
.label-tree .label-tree {
margin-left: var(--space-4);
border-left: 1px solid var(--color-border);
padding-left: var(--space-3);
}
.label-node {
display: flex;
align-items: center;
gap: var(--space-2);
flex-wrap: wrap;
padding: var(--space-1) 0;
}
.label-node__swatch {
display: inline-block;
width: 0.9rem;
height: 0.9rem;
border-radius: 50%;
border: 1px solid var(--color-border);
flex: 0 0 auto;
}
.label-node__name {
font-weight: 600;
}
.label-node__actions {
display: flex;
align-items: center;
gap: var(--space-2);
margin-left: auto;
}
.label-node__color {
width: 1.75rem;
height: 1.5rem;
padding: 0;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: none;
cursor: pointer;
}
.label-node__delete {
color: var(--color-danger);
}
.label-tree__child-form {
display: flex;
gap: var(--space-2);
margin: var(--space-1) 0 var(--space-2) var(--space-4);
}
/* Page label picker panel. */
.label-picker {
flex: 0 0 20rem;
max-width: 20rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm, 4px);
padding: var(--space-3);
max-height: 80vh;
overflow-y: auto;
}
.label-picker__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-2);
}
.label-picker__search {
width: 100%;
margin-bottom: var(--space-2);
}
.label-picker__list {
list-style: none;
margin: 0;
padding: 0;
}
.label-picker__option {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-1) 0;
cursor: pointer;
}
.label-picker__empty {
color: var(--color-text-muted);
}
/* Manual page reordering (issue #45) ----------------------------------- */
.sidebar__page-item {
display: flex;
align-items: center;
gap: var(--space-1);
}
.sidebar__page-item .sidebar__page {
flex: 1 1 auto;
min-width: 0;
}
.sidebar__page-item--draggable {
cursor: grab;
}
.sidebar__reorder {
display: inline-flex;
gap: 2px;
flex: 0 0 auto;
}
.sidebar__reorder-btn {
border: 1px solid var(--color-border);
background: var(--color-bg);
border-radius: var(--radius);
color: var(--color-text-muted);
width: 1.4rem;
height: 1.4rem;
line-height: 1;
cursor: pointer;
padding: 0;
}
.sidebar__reorder-btn:disabled {
opacity: 0.35;
cursor: default;
}
.sidebar__reorder-btn:not(:disabled):hover {
background: var(--color-bg-subtle);
color: var(--color-text);
}
/* Wikilinks (issue #46) ------------------------------------------------- */
.wikilink-nodeview {
display: inline;
}
.wikilink {
color: var(--color-accent);
text-decoration: none;
/* Permanently visible: color alone must not be the only link cue
(issue #167, WCAG 1.4.1 — accent vs. text is only ~2.5:1). */
border-bottom: 1px solid currentColor;
cursor: pointer;
}
.wikilink:hover {
border-bottom-width: 2px;
}
/* A link to a page that does not exist yet. */
.wikilink--phantom {
color: var(--color-text-muted);
border-bottom: 1px dashed currentColor;
}
/* Page embed / transclusion (issue #135). Placeholder card shown in the editor
and while loading; the expanded embed wraps the target's rendered content. */
.dt-transclusion-card {
display: flex;
align-items: center;
gap: var(--space-2);
margin: var(--space-3) 0;
padding: var(--space-2) var(--space-3);
border: 1px dashed var(--color-border);
border-radius: 8px;
background: var(--color-bg-subtle);
color: var(--color-text-muted);
font-size: 0.9375rem;
}
.dt-transclusion-card__icon {
font-size: 1.1em;
}
.dt-transclusion-card__label {
flex: 1;
min-width: 0;
}
.dt-transclusion-card__open {
color: var(--color-accent);
text-decoration: none;
white-space: nowrap;
}
.dt-embed {
margin: var(--space-3) 0;
padding: var(--space-3);
border: 1px solid var(--color-border);
border-left: 3px solid var(--color-accent);
border-radius: 8px;
background: var(--color-surface, #fff);
}
.dt-embed__title {
margin-bottom: var(--space-2);
font-weight: var(--font-weight-heading);
}
.dt-embed__body > :first-child {
margin-top: 0;
}
.dt-embed__body > :last-child {
margin-bottom: 0;
}
.wikilink-suggest {
list-style: none;
margin: 0;
padding: var(--space-1);
min-width: 14rem;
max-width: 22rem;
max-height: 16rem;
overflow-y: auto;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: 0 4px 16px rgb(0 0 0 / 12%);
z-index: 30;
}
.wikilink-suggest__item {
display: block;
width: 100%;
text-align: left;
padding: var(--space-1) var(--space-2);
border: 0;
border-radius: var(--radius);
background: none;
color: var(--color-text);
cursor: pointer;
font-size: 0.95rem;
}
.wikilink-suggest__item.is-active,
.wikilink-suggest__item:hover {
background: var(--color-bg-subtle);
}
/* Backlinks + missing pages (issue #48) --------------------------------- */
.backlinks {
margin-top: var(--space-6);
border-top: 1px solid var(--color-border);
padding-top: var(--space-3);
}
.backlinks > summary {
cursor: pointer;
font-weight: 600;
color: var(--color-text-muted);
}
.backlinks__list {
list-style: none;
margin: var(--space-2) 0 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.backlinks__link {
font-weight: 600;
}
.backlinks__snippet {
margin: 2px 0 0;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.phantom-pages__description {
color: var(--color-text-muted);
margin-bottom: var(--space-3);
}
.phantom-pages__empty {
color: var(--color-text-muted);
}
.phantom-pages__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.phantom-pages__head {
display: flex;
align-items: center;
gap: var(--space-3);
}
.phantom-pages__slug {
font-family: var(--font-mono);
background: var(--color-bg-subtle);
padding: 2px var(--space-2);
border-radius: var(--radius);
}
.phantom-pages__referrers {
margin: var(--space-1) 0 0;
color: var(--color-text-muted);
font-size: 0.9rem;
}
/* Search palette (issue #50) -------------------------------------------- */
.topbar__search {
display: inline-flex;
align-items: center;
gap: var(--space-1);
border: 1px solid var(--color-border);
background: var(--color-bg-subtle);
border-radius: var(--radius);
padding: var(--space-1) var(--space-3);
color: var(--color-text-muted);
cursor: pointer;
font-size: 0.9rem;
}
.topbar__search:hover {
background: var(--color-bg);
color: var(--color-text);
}
.search-overlay {
position: fixed;
inset: 0;
z-index: 100;
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: 10vh;
}
.search-backdrop {
position: absolute;
inset: 0;
background: rgb(0 0 0 / 35%);
}
.search-palette {
position: relative;
width: min(40rem, 92vw);
max-height: 75vh;
overflow-y: auto;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: 0 12px 40px rgb(0 0 0 / 25%);
padding: var(--space-3);
}
.search-palette__input {
width: 100%;
font-size: 1.1rem;
padding: var(--space-2);
}
.search-palette__scope {
display: flex;
align-items: center;
gap: var(--space-4);
margin: var(--space-2) 0;
font-size: 0.9rem;
color: var(--color-text-muted);
}
.search-palette__labels ul {
list-style: none;
margin: var(--space-1) 0 0;
padding: 0;
max-height: 10rem;
overflow-y: auto;
}
.search-palette__labels label {
display: flex;
align-items: center;
gap: var(--space-2);
padding: 2px 0;
}
.search-palette__hint {
color: var(--color-text-muted);
padding: var(--space-3);
text-align: center;
}
.search-palette__recent ul {
list-style: none;
margin: 0;
padding: 0;
}
.search-palette__recent li {
padding: var(--space-1) var(--space-2);
}
.search-results {
list-style: none;
margin: 0;
padding: 0;
}
.search-result {
display: block;
width: 100%;
text-align: left;
border: 0;
background: none;
border-radius: var(--radius);
padding: var(--space-2);
cursor: pointer;
color: var(--color-text);
}
.search-result--active,
.search-result:hover {
background: var(--color-bg-subtle);
}
.search-result__title {
font-weight: 600;
margin-right: var(--space-2);
}
.search-result__pond {
color: var(--color-text-muted);
font-size: 0.85rem;
}
/* VS-NfD marking on a search hit (issue #211, ADR 0022): compact form of the
banner — text token only, full contrast in both themes. */
.search-result__classification {
display: block;
color: var(--color-text);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
}
.search-result__snippet {
display: block;
margin-top: 2px;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.search-result__snippet mark {
background: var(--color-accent);
color: var(--color-accent-contrast);
border-radius: 2px;
padding: 0 2px;
}
/* Access rules (issue #55) */
.access-rules__intro,
.access-rules__empty {
color: var(--color-text-muted);
margin-bottom: var(--space-3);
}
.rule-add {
border: 1px solid var(--color-border, rgba(127, 127, 127, 0.25));
border-radius: var(--radius-sm, 0.375rem);
padding: var(--space-3);
margin-bottom: var(--space-4);
}
.rule-add__title {
margin: 0 0 var(--space-2);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-text-muted);
}
.rule-add__row {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin-bottom: var(--space-2);
}
.rule-add__field {
display: flex;
flex-direction: column;
gap: var(--space-1);
font-size: 0.875rem;
}
.rule-add__public-warning {
display: flex;
align-items: center;
gap: var(--space-2);
}
.rule-groups,
.rule-group__list {
list-style: none;
margin: 0;
padding: 0;
}
.rule-group {
margin-bottom: var(--space-3);
}
.rule-group__subject {
margin: 0 0 var(--space-1);
font-size: 0.9375rem;
}
.rule-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
padding: var(--space-1) 0;
}
/* Public read-only page view (issue #56) */
.public-page {
max-width: 48rem;
margin: 0 auto;
}
.public-page__badge {
display: inline-block;
padding: var(--space-1) var(--space-2);
border-radius: 999px;
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
font-size: 0.8rem;
color: var(--color-text-muted);
margin-bottom: var(--space-3);
}
.public-page__pond {
color: var(--color-text-muted);
font-size: 0.9rem;
margin: 0;
}
.public-page__body img {
max-width: 100%;
height: auto;
}
/* Plugin-fallback SVGs (e.g. Excalidraw sketches inlined by the api's
fallback renderer) carry fixed pixel width/height attributes; without this
they overflow narrow viewports — the img rule above does not match inline
<svg> elements. */
.dt-plugin-fallback svg {
max-width: 100%;
height: auto;
}
/* Effective-permissions inspector (issue #57) */
.inspector__intro {
color: var(--color-text-muted);
margin-bottom: var(--space-3);
}
.inspector__controls {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
margin-bottom: var(--space-3);
}
.inspector__field {
display: flex;
flex-direction: column;
gap: var(--space-1);
font-size: 0.875rem;
}
.inspector__result {
margin: 0;
}
.inspector__outcome {
display: flex;
gap: var(--space-2);
padding: var(--space-2);
border-left: 3px solid var(--color-border);
margin-bottom: var(--space-2);
}
.inspector__outcome dt {
min-width: 5rem;
font-weight: 600;
}
.inspector__outcome dd {
margin: 0;
}
.inspector__outcome--allow {
border-left-color: var(--color-ok);
}
.inspector__outcome--deny {
border-left-color: var(--color-danger);
}
.inspector__reason {
color: var(--color-text-muted);
}
/* Quota override manager (issue #58) */
.quota-manager__lookup {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin-bottom: var(--space-3);
}
.quota-manager__subject {
font-weight: 600;
margin-bottom: var(--space-2);
}
.quota-row__override {
display: flex;
align-items: center;
gap: var(--space-2);
}
.quota-row__input {
width: 8rem;
}
.quota-row--over .quota-row__usage {
color: var(--color-danger);
}
.quota-row__flag {
color: var(--color-danger);
}
/* User management (issue #59) */
.user-manager__search {
width: 100%;
margin-bottom: var(--space-3);
}
.user-row__username,
.user-row__email {
color: var(--color-text-muted);
font-size: 0.85rem;
}
.user-row__actions-inner {
display: flex;
align-items: center;
gap: var(--space-2);
}
.user-row__delete-confirm {
color: var(--color-danger);
font-weight: 600;
}
.user-manager__pager {
display: flex;
align-items: center;
gap: var(--space-3);
margin-top: var(--space-3);
}
/* Attachments: page section + pond file manager (issue #61) */
.attachments-panel {
border: 1px solid var(--color-border);
border-radius: var(--radius-sm, 0.375rem);
padding: var(--space-3);
margin-bottom: var(--space-3);
background: var(--color-bg-subtle);
}
.attachments-panel__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
margin-bottom: var(--space-2);
}
.attachments-panel__header h3 {
margin: 0;
}
/* Upload warning on classified pages (issue #213, ADR 0022) — a persistent
note, not a dismissable toast: the consequence applies to every upload. */
.attachments-panel__warning {
margin: 0 0 var(--space-2);
padding: var(--space-2);
border: 1px solid var(--color-border);
border-left: 3px solid currentColor;
font-size: 0.85rem;
color: var(--color-text);
}
.attachments-panel__upload {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-3);
}
.attachments-panel__input {
display: none;
}
.attachments-panel__empty,
.pond-file-manager__empty {
color: var(--color-text-muted);
}
.attachments-panel__list,
.pond-file-manager__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.attachments-item {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-1) 0;
flex-wrap: wrap;
}
.attachments-item__glyph {
font-size: 1.1rem;
}
.attachments-item__name {
font-weight: 600;
}
.attachments-item__meta {
color: var(--color-text-muted);
font-size: 0.875rem;
}
.attachments-item__orphan {
color: var(--color-danger, #b91c1c);
font-style: italic;
}
.attachments-item__actions {
margin-left: auto;
display: flex;
gap: var(--space-2);
}
.pond-file-manager__usage {
color: var(--color-text-muted);
margin-bottom: var(--space-2);
}
/* --- Plugin sandbox frames (issue #73) ------------------------------------ */
.plugin-frame-host__mount .plugin-frame {
display: block;
width: 100%;
height: 120px;
border: none;
background: transparent;
}
.plugin-frame-host[data-state='failed'] .plugin-frame-host__mount {
display: none;
}
.plugin-frame-host__status {
color: var(--color-text-muted);
font-size: 0.875rem;
}
.plugin-frame-host__status--failed {
color: var(--color-danger, #b91c1c);
border: 1px dashed var(--color-danger, #b91c1c);
border-radius: 4px;
padding: var(--space-2);
}
.plugin-preview__version {
color: var(--color-text-muted);
font-size: 1rem;
font-weight: 400;
}
.plugin-preview__surface {
margin-top: var(--space-4);
}
/* --- Plugin administration (issue #72) ------------------------------------ */
.plugin-manager__upload-input {
display: none;
}
.plugin-manager__list {
list-style: none;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.plugin-manager__item {
border: 1px solid var(--color-border);
border-radius: 6px;
padding: var(--space-3);
}
.plugin-manager__head {
display: flex;
align-items: baseline;
gap: var(--space-2);
}
.plugin-manager__name {
font-weight: 600;
}
.plugin-manager__version,
.plugin-manager__kind {
color: var(--color-text-muted);
font-size: 0.875rem;
}
.plugin-manager__permissions {
margin: var(--space-2) 0;
font-size: 0.875rem;
}
.plugin-manager__permission {
margin-right: var(--space-1);
}
.plugin-manager__controls {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
}
.plugin-manager__mode-hint {
color: var(--color-text-muted);
font-size: 0.8125rem;
}
.pond-plugins__list {
list-style: none;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.pond-plugins__version {
color: var(--color-text-muted);
font-size: 0.875rem;
}
/* Site-Admin system panel (issue #86) */
.system-badge {
display: inline-block;
padding: 0.1rem 0.5rem;
border-radius: 999px;
font-size: 0.8125rem;
background: var(--color-surface-muted);
margin-left: var(--space-1);
}
.system-badge--ok {
background: var(--color-badge-ok-bg);
color: var(--color-badge-ok-text);
}
.system-badge--error {
background: var(--color-badge-error-bg);
color: var(--color-badge-error-text);
}
.system-badge--warn {
background: var(--color-badge-warn-bg);
color: var(--color-badge-warn-text);
}
.system-jobs__notice {
color: var(--color-text-muted);
}
.system-backup__facts dt {
font-weight: 600;
margin-top: var(--space-2);
}
.system-backup__facts dd {
margin: 0;
}
.system-backup__retention,
.system-backup__unavailable {
color: var(--color-text-muted);
}
/* Backup settings + in-app restore (issue #103) */
.system-backup__settings,
.system-backup__restore {
margin-top: var(--space-4);
padding-top: var(--space-3);
border-top: 1px solid var(--color-border, #cbd5e1);
}
.system-backup__settings label,
.system-backup__confirm label {
display: block;
margin-top: var(--space-2);
font-weight: 600;
}
.system-backup__settings input[type='text'],
.system-backup__settings input[type='url'],
.system-backup__settings input[type='password'],
.system-backup__settings input[type='number'],
.system-backup__settings select,
.system-backup__confirm input[type='text'] {
display: block;
margin-top: var(--space-1);
width: min(28rem, 100%);
}
.system-backup__checkbox,
.system-backup__settings .system-backup__checkbox {
font-weight: 600;
}
.system-backup__checkbox input {
margin-right: var(--space-1);
}
.system-backup__nextcloud {
border: none;
padding: 0;
margin: 0;
}
.system-backup__nextcloud:disabled {
opacity: 0.5;
}
.system-backup__hint {
color: var(--color-text-muted);
font-size: 0.875rem;
margin: var(--space-1) 0;
}
.system-backup__notice--error {
color: var(--color-danger);
}
.system-backup__warning {
color: var(--color-danger);
font-weight: 600;
}
.system-backup__sets {
list-style: none;
padding: 0;
margin: var(--space-1) 0;
}
.system-backup__save,
.system-backup__test {
margin-top: var(--space-3);
}
.system-backup__restore-status--failed {
color: var(--color-danger);
}
.button--danger {
border-color: var(--color-danger);
background: var(--color-danger);
color: var(--color-danger-contrast);
}
.button--danger:hover:not(:disabled) {
background: var(--color-danger-strong);
}
/* Pond creation form in the switcher + pond danger zone */
.pond-switcher__create {
display: block;
width: 100%;
text-align: left;
border: none;
border-top: 1px solid var(--color-border);
background: none;
padding: var(--space-2) var(--space-3);
cursor: pointer;
font: inherit;
color: var(--color-accent);
}
.pond-switcher__form {
padding: var(--space-3);
min-width: 18rem;
}
.pond-switcher__form label {
display: block;
margin-top: var(--space-2);
font-weight: 600;
}
.pond-switcher__form input {
display: block;
width: 100%;
margin-top: var(--space-1);
box-sizing: border-box;
}
.pond-switcher__hint {
color: var(--color-text-muted);
font-size: 0.85rem;
}
.pond-switcher__actions {
display: flex;
gap: var(--space-2);
margin-top: var(--space-2);
}
.pond-delete {
border-top: 1px solid var(--color-border);
margin-top: var(--space-6);
padding-top: var(--space-4);
}
.pond-delete__hint {
color: var(--color-text-muted);
}
.pond-delete label {
display: block;
font-weight: 600;
margin: var(--space-2) 0;
}
.pond-delete input {
display: block;
margin-top: var(--space-1);
width: min(24rem, 100%);
}
.pond-delete__submit {
margin-top: var(--space-2);
}
/* API tokens (issue #104) */
.api-tokens__create label {
display: block;
margin-top: var(--space-2);
font-weight: 600;
}
.api-tokens__create input[type='text'],
.api-tokens__create input[type='date'],
.api-tokens__create select {
display: block;
margin-top: var(--space-1);
width: min(24rem, 100%);
}
.api-tokens__ponds {
border: none;
padding: 0;
margin: var(--space-2) 0;
}
.api-tokens__ponds legend {
font-weight: 600;
padding: 0;
}
.api-tokens__pond {
display: block;
font-weight: 400 !important;
margin-top: var(--space-1) !important;
}
.api-tokens__pond input {
margin-right: var(--space-1);
}
.api-tokens__create .button {
margin-top: var(--space-2);
}
.api-tokens__created {
margin-top: var(--space-3);
padding: var(--space-3);
border: 1px solid var(--color-border, #cbd5e1);
border-radius: 8px;
}
.api-tokens__secret {
display: block;
margin: var(--space-2) 0;
word-break: break-all;
user-select: all;
}
.api-tokens__intro,
.api-tokens__hint,
.api-opt-in__hint {
color: var(--color-text-muted);
}
.api-tokens__table {
margin-top: var(--space-3);
}
.api-opt-in__label input {
margin-right: var(--space-1);
}
/* Fullscreen plugin surface (ui.enterFullscreen — drawio-class editors):
covers the whole viewport, above every app layer, until the plugin exits
or the surface is destroyed. */
.plugin-block__mount .plugin-frame.plugin-frame--fullscreen,
.plugin-frame.plugin-frame--fullscreen {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh !important;
z-index: 1000;
border: none;
border-radius: 0;
background: var(--color-surface, #fff);
}
/* Maintenance screen during an in-app restore (issue #103) */
.maintenance-page {
max-width: 32rem;
margin: 15vh auto 0;
padding: var(--space-4);
text-align: center;
}
.maintenance-page__error {
color: var(--color-danger);
}
.system-audit__filters {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
margin-bottom: var(--space-3);
}
.system-audit__table code,
.system-storage__table code {
font-size: 0.8125rem;
word-break: break-all;
}
.system-audit__pager {
display: flex;
align-items: center;
gap: var(--space-3);
margin-top: var(--space-3);
}
/* Inline comments section, read view (issues #92, #133) */
.comments-section {
border: 1px solid var(--color-border, #cbd5e1);
border-radius: 8px;
padding: var(--space-3);
margin: var(--space-3) 0;
background: var(--color-surface, #fff);
}
.comments-section__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-2);
}
.comments-section__header h2 {
margin: 0;
font-size: 1.1rem;
}
.comments-section__count {
margin-left: var(--space-2);
font-size: 0.8125rem;
background: var(--color-surface-muted);
border-radius: 999px;
padding: 0.05rem 0.5rem;
}
.comments-section__policy-hint,
.comments-section__empty {
color: var(--color-text-muted);
}
.comments-section__threads {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.comments-section__resolved {
margin-top: var(--space-3);
}
.comments-section__resolved > summary {
cursor: pointer;
color: var(--color-text-muted);
}
.comments-section__resolved > .comments-section__threads {
margin-top: var(--space-2);
}
.comments-thread {
border-top: 1px solid var(--color-border, #e2e8f0);
padding-top: var(--space-2);
}
.comments-thread--resolved {
opacity: 0.75;
}
.comments-thread__replies {
list-style: none;
margin: var(--space-2) 0 0;
padding-left: var(--space-4);
border-left: 2px solid var(--color-border, #e2e8f0);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.comment__meta {
display: flex;
gap: var(--space-2);
align-items: baseline;
font-size: 0.8125rem;
color: var(--color-text-muted);
}
.comment__author {
font-weight: 600;
color: var(--color-text, inherit);
}
.comment__body p {
margin: var(--space-1) 0;
}
.comment__actions {
display: flex;
gap: var(--space-3);
}
.comments-link-button {
background: none;
border: none;
padding: 0;
color: var(--color-accent);
cursor: pointer;
font-size: 0.8125rem;
text-decoration: underline;
}
.comments-composer textarea {
width: 100%;
resize: vertical;
}
.comments-composer__footer {
display: flex;
align-items: center;
gap: var(--space-3);
margin-top: var(--space-1);
}
.comments-composer__hint {
flex: 1;
font-size: 0.75rem;
color: var(--color-text-muted);
}
.comment-policy__label {
display: flex;
align-items: center;
gap: var(--space-2);
}
.comment-policy__hint {
color: var(--color-text-muted);
font-size: 0.8125rem;
}
/* Watches (issue #93) */
.watch-toggle--active {
background: var(--color-accent);
color: var(--color-accent-contrast);
}
.watches-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.watches-list li {
display: flex;
align-items: center;
gap: var(--space-3);
}
.watches-list__type {
color: var(--color-text-muted);
font-size: 0.8125rem;
}
.settings-checkbox {
display: flex;
align-items: center;
gap: var(--space-2);
margin: var(--space-2) 0;
/* Radio + label + accent swatches must be allowed to break onto a second
line at 320px (issue #301, WCAG 1.4.10) — the swatches have a fixed size
and cannot shrink, so without this the row sets a floor for the page. */
flex-wrap: wrap;
}
/* Radio groups in settings sections (issue #180). */
.settings-fieldset {
border: none;
margin: 0;
padding: 0;
}
.settings-fieldset legend {
padding: 0;
font-weight: 600;
}
/* Accent picker (issue #184): the two dots preview the DERIVED accent per
mode; their ring (inline box-shadow) shows each mode's canonical
background, so the auto-adjustment is visible before choosing. */
.accent-swatches {
display: inline-flex;
gap: var(--space-3);
margin-left: var(--space-2);
}
.accent-swatch {
width: 0.875rem;
height: 0.875rem;
border-radius: 50%;
border: 1px solid var(--color-chip-outline);
}
.accent-color-input {
inline-size: 2.25rem;
block-size: 1.5rem;
padding: 0;
border: 1px solid var(--color-border-input);
border-radius: var(--radius);
background: none;
}
.pond-settings-page__header {
display: flex;
align-items: center;
gap: var(--space-3);
}
/* Notification center (issue #94) */
.notifications-bell {
position: relative;
}
/* The bell is an `.icon-button` like search and the theme toggle (issue
#300) — it used to carry its own rules, which lacked the flex centring
and the icon size, so the glyph sat on the text baseline and rendered at
lucide's 24px default. The badge stays positioned against that button;
`.icon-button` is `position: relative` for exactly this. */
.notifications-bell__badge {
position: absolute;
top: -1px;
right: -3px;
background: var(--color-danger);
color: var(--color-danger-contrast);
border-radius: 999px;
font-size: 0.7rem;
padding: 0 0.35rem;
line-height: 1.2rem;
}
.notifications-bell__dropdown {
position: absolute;
right: 0;
top: 100%;
z-index: 30;
width: 22rem;
max-height: 24rem;
overflow-y: auto;
background: var(--color-surface, #fff);
border: 1px solid var(--color-border, #cbd5e1);
border-radius: 8px;
box-shadow: 0 6px 24px rgb(0 0 0 / 0.12);
padding: var(--space-2);
}
.notifications-bell__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-2);
font-weight: 600;
}
.notifications-bell__empty {
color: var(--color-text-muted);
margin: var(--space-2) 0;
}
.notifications-bell__list {
list-style: none;
margin: 0;
padding: 0;
}
.notifications-bell__entry {
display: flex;
flex-direction: column;
gap: 2px;
width: 100%;
text-align: left;
background: none;
border: none;
border-top: 1px solid var(--color-border, #e2e8f0);
padding: var(--space-2);
cursor: pointer;
}
.notifications-bell__entry--unread {
font-weight: 600;
}
.notifications-bell__entry time {
color: var(--color-text-muted);
font-size: 0.75rem;
font-weight: 400;
}
/* Toast notifications (issue #130): bottom-center stack above the footer,
over everything including modal overlays (z 1100). */
.toast-stack {
position: fixed;
bottom: var(--space-8);
left: 50%;
transform: translateX(-50%);
z-index: 1200;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
pointer-events: none;
}
.toast {
pointer-events: auto;
cursor: pointer;
max-width: 28rem;
padding: var(--space-2) var(--space-4);
border-radius: var(--radius);
background: var(--color-text);
color: var(--color-bg);
font-size: 0.9rem;
box-shadow: 0 4px 12px rgb(0 0 0 / 20%);
animation: toast-in 0.15s ease-out;
}
.toast--error {
background: var(--color-danger);
color: var(--color-accent-contrast);
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateY(0.5rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Settings section jump navigation (issue #145). Wide viewports: sticky rail
beside the content; narrow ones: horizontal chip bar above it. */
.settings-layout {
display: flex;
flex-direction: row-reverse;
gap: var(--space-6);
align-items: flex-start;
}
.settings-layout__content {
flex: 1;
min-width: 0;
}
.settings-layout section[id] {
scroll-margin-top: var(--space-4);
}
.settings-nav {
position: sticky;
top: var(--space-2);
width: 13rem;
flex-shrink: 0;
}
.settings-nav ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.settings-nav__link {
display: block;
width: 100%;
text-align: left;
padding: var(--space-1) var(--space-2);
border: none;
border-left: 2px solid var(--color-border);
border-radius: 0 6px 6px 0;
background: none;
color: var(--color-text-muted);
font: inherit;
font-size: 0.9375rem;
cursor: pointer;
}
.settings-nav__link:hover {
color: var(--color-text);
background: var(--color-bg-subtle);
}
.settings-nav__link--active {
color: var(--color-accent);
border-left-color: var(--color-accent);
font-weight: var(--font-weight-heading);
}
@media (max-width: 60rem) {
.settings-layout {
flex-direction: column;
/* Override the row layout's flex-start: stacked, the content column
must stretch to the viewport instead of its widest control (#165). */
align-items: stretch;
}
.settings-nav {
position: static;
width: 100%;
overflow-x: auto;
/* THE reflow fix (issue #301). As a flex child the nav defaults to
`min-width: auto`, i.e. the min-content width of the whole jump
strip — so it pushed the column wider than the viewport and its own
`overflow-x: auto` never got the chance to scroll anything. Measured
at 320px: 417px of page-level overflow, all of it these links. */
min-width: 0;
}
.settings-nav ul {
flex-direction: row;
gap: var(--space-2);
}
.settings-nav__link {
white-space: nowrap;
border-left: none;
border-bottom: 2px solid var(--color-border);
border-radius: 6px 6px 0 0;
}
.settings-nav__link--active {
border-bottom-color: var(--color-accent);
}
}
/* `@username` mention chip (issue #150). */
.dt-mention {
display: inline-block;
padding: 0 0.35em;
border-radius: 999px;
background: var(--color-bg-subtle);
color: var(--color-accent);
font-weight: 500;
white-space: nowrap;
}
.dt-mention--dead {
color: var(--color-text-muted);
text-decoration: line-through;
}
/* Date marker chip (issue #152): » = due date, « = start date. */
.dt-date {
display: inline-block;
padding: 0 0.35em;
border-radius: 6px;
background: var(--color-bg-subtle);
color: var(--color-text-muted);
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.dt-date--due {
color: var(--color-accent);
}
.dt-date--overdue {
color: var(--color-danger);
}
/* Task overview block (issue #154). */
.dt-task-overview-table {
width: 100%;
border-collapse: collapse;
margin: var(--space-3) 0;
font-size: 0.9375rem;
}
.dt-task-overview-table th,
.dt-task-overview-table td {
padding: var(--space-1) var(--space-2);
border-bottom: 1px solid var(--color-border);
text-align: left;
vertical-align: top;
}
.dt-task-overview-table th {
color: var(--color-text-muted);
font-weight: var(--font-weight-heading);
}
.dt-task-overview-empty {
margin: var(--space-3) 0;
padding: var(--space-2) var(--space-3);
border: 1px dashed var(--color-border);
border-radius: 8px;
color: var(--color-text-muted);
}
/* Site-Admin font management (issue #304). The layout stays a plain column so
the section reflows at 320px without its own rules (#301). */
.custom-fonts__list {
list-style: none;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.custom-fonts__item {
border: 1px solid var(--color-border);
border-radius: 6px;
padding: var(--space-3);
}
.custom-fonts__name {
font-size: 1.25rem;
margin: 0;
}
.custom-fonts__meta {
color: var(--color-text-muted);
margin: var(--space-1) 0 var(--space-2);
}
.custom-fonts__actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
}
.custom-fonts__weights {
border: 1px solid var(--color-border);
border-radius: 6px;
padding: var(--space-3);
margin: var(--space-3) 0;
min-width: 0;
}
.custom-fonts__weight {
border-top: 1px solid var(--color-border);
padding-top: var(--space-2);
margin-top: var(--space-2);
}
.custom-fonts__weight:first-of-type {
border-top: none;
padding-top: 0;
margin-top: 0;
}
.custom-fonts__confirm,
.custom-fonts__add-weight {
border-top: 1px solid var(--color-border);
margin-top: var(--space-3);
padding-top: var(--space-3);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-2);
}
/* A file input is as wide as its filename; without this it pushes the page
at 320px (issue #301's lesson, applied ahead of the fact). */
.custom-fonts input[type='file'] {
max-width: 100%;
}
/* Instance branding (issue #306). The logo sits above the pond name in the
sidebar; the two variants are both rendered and one is hidden here rather
than in JavaScript, so the right one is the one PAINTED — theme-init.js has
already resolved data-theme when this applies. */
.brand-logo {
display: block;
padding: var(--space-2) 0;
color: inherit;
text-decoration: none;
}
.brand-logo__img {
display: block;
max-width: 100%;
height: auto;
/* A tall logo must not push the pond name out of view. */
max-height: 3rem;
}
.brand-logo__name {
font-weight: var(--font-weight-heading);
}
/* Without a dark variant the light logo carries both themes (#306/#307:
variants are never mixed across levels). */
.brand-logo__img--dark {
display: none;
}
:root[data-theme='dark'] .brand-logo__img--light:not(.brand-logo__img--both) {
display: none;
}
:root[data-theme='dark'] .brand-logo__img--dark {
display: block;
}
.branding__slot {
border-top: 1px solid var(--color-border);
margin-top: var(--space-4);
padding-top: var(--space-3);
min-width: 0;
}
.branding__preview {
max-width: 100%;
max-height: 6rem;
height: auto;
}
/* The dark logo is meant for a dark surface — previewing it on the light
admin background would misrepresent it. */
.branding__preview--dark {
background: #10161d;
padding: var(--space-2);
border-radius: 6px;
}
.branding__warning {
border: 1px solid var(--color-border);
border-radius: 6px;
padding: var(--space-2) var(--space-3);
}
.crop-field {
min-width: 0;
}
.crop-field input[type='file'] {
max-width: 100%;
}
.crop-field__controls {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
align-items: flex-end;
}
.crop-field__controls .field {
max-width: 12rem;
}
.crop-field__canvas {
max-width: 100%;
height: auto;
border: 1px solid var(--color-border);
border-radius: 6px;
/* A transparent PNG on a transparent page shows nothing — the checkerboard
is how an operator sees that the background really is transparent. */
background-color: var(--color-surface);
background-image:
linear-gradient(45deg, var(--color-surface-muted) 25%, transparent 25%),
linear-gradient(-45deg, var(--color-surface-muted) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--color-surface-muted) 75%),
linear-gradient(-45deg, transparent 75%, var(--color-surface-muted) 75%);
background-size: 16px 16px;
background-position:
0 0,
0 8px,
8px -8px,
-8px 0;
}