dorfteich/apps/web/src/styles/base.css
Claude Opus 5 3310ae3926
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 7m19s
CI / Build container images (pull_request) Successful in 1m23s
CI / Auth e2e pack (pull_request) Successful in 8m55s
CI / Import/export fidelity gate (pull_request) Successful in 57s
CD / Build and push images (push) Successful in 14s
CD / Deploy to Test (push) Successful in 16s
CD / Smoke tests against Test (push) Successful in 1m28s
CD / Promote to Int (push) Successful in 13s
CI / Lint, typecheck, test (push) Successful in 6m52s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 9m6s
CI / Import/export fidelity gate (push) Successful in 58s
#305: a full pond archive before deletion and before purge
Deleting a pond already had a strict prompt — typing the pond name, stricter
than a confirm dialog. That was never the gap. The gap is that the person who
deletes it loses access the moment they do: the pond leaves their view, only a
Site Admin can bring it back, and the export is no longer reachable for them.
So the archive is offered INSIDE the deletion flow, before the button.

What it contains, and why it is not the existing export:

- Every page the requester may read, as Markdown, as before.
- **Every attachment of the pond**, not only the embedded ones. An
  attachment nobody put on a page would otherwise vanish unnoticed — which
  is the whole reason this issue exists.
- `manifest.json`: pond settings (EFFECTIVE, defaults filled in — a
  preservation format must not require its reader to know Dorfteich's
  defaults), labels, the page hierarchy and sort keys, comments, and
  attachment metadata including the #199 hash so a reader can verify bytes.
  It extends the #210 manifest rather than adding a second descriptor, and
  carries an explicit `formatVersion`.
- `README.txt`, because the manifest is for machines: whoever unpacks a
  folder of Markdown a year from now must not believe they hold a one-click
  restore.

Decisions worth naming:

- **"Complete" describes the RESULT, not the route.** A pond admin who may
  read every page gets `complete: true`; only an archive that actually
  leaves pages out is incomplete. The Site-Admin route skips the read filter
  (an archive taken before an irreversible purge must not depend on which
  ponds the operator happens to be a member of) — those are two different
  questions and the first version of this conflated them.
- **The omission is named before the download**, with its number, in the UI
  and in the manifest. An archive silently missing content is worse than no
  archive, because it ends the search.
- **Not downloading stays allowed.** A pond of test pages should not require
  one, and the server cannot tell whether a file arrived anyway — so the
  finality is stated in text instead of enforced.
- **A plain link, not fetch-into-a-blob.** The api streams the ZIP; buffering
  a whole pond in the tab to draw a progress bar would trade memory for
  cosmetics. The browser reports progress and completion; what it cannot say
  — that the archive is being BUILT — is announced in a live region.
- Read trail unchanged in kind (ADR 0023): one `export` event per classified
  page before any classified byte enters the stream. Attachments never travel
  without their page, so the same events cover them.
- New audit action `pond.archived` (catalogue v1.7) with page and attachment
  counts, omitted pages, and completeness.

Format documented in `docs/architecture/pond-archive-format.md`, including
what is deliberately NOT in it (history, permissions, trash).

Verified by hand, not only asserted: a real pond's archive downloaded and
unpacked — README, manifest, three page files, the media file; the manifest's
effective settings, per-page classification, the VS-NfD frontmatter and
marking preserved in the classified page's Markdown, and the attachment's
sha256 present. Plus six api tests (including that an unembedded attachment
travels and that a Site Admin gets a complete archive without membership) and
the a11y pack 11/11 in both schemes, which now also scans the pond settings
screen.

Not done, because there is nothing to attach it to: the Site Admin's purge
dialog (#193) exists only as an api endpoint — there is no pond-trash UI in
the web app. The api half is here and tested, so it becomes a link when that
screen is built.
2026-08-01 20:24:35 +02:00

4310 lines
87 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;
}
/* Full-archive offer inside the pond deletion flow (issue #305). Plain
column: the section must reflow at 320px without rules of its own (#301). */
.pond-archive {
border: 1px solid var(--color-border);
border-radius: 6px;
padding: var(--space-3);
margin-bottom: var(--space-4);
min-width: 0;
}
.pond-archive__warning {
border: 1px solid var(--color-border);
border-radius: 6px;
padding: var(--space-2) var(--space-3);
}
.pond-archive__download {
display: inline-block;
text-decoration: none;
}
.pond-archive__finality {
color: var(--color-text-muted);
margin-bottom: 0;
}