Files
pad/web/src/app.css
T
xarmian 74589e98bb fix(attachments): make the live chip a button, not a link (TASK-2424)
From the orchestrator's pass on TASK-2424. The chip opens the options
panel — it does not navigate — but it was still an <a href download> that
only intercepted primary click. Middle-click and aux-click therefore
opened or downloaded the file straight past the panel, which is the exact
accidental download the panel exists to prevent, and screen readers
announced a link for a control that opens a menu.

A button has no URL to activate, so the bypass cannot exist rather than
being intercepted, and it matches the strip's file tile — the same
control, now with the same semantics. renderHTML stays an <a download>:
that is the clipboard / read-only shape, where a link is honest and there
is no panel to open.

Deleted chips now use `disabled` rather than a dropped href: a disabled
button receives no click or keydown at all, where the anchor's handlers
still ran and had to swallow events. It is also blurred explicitly first,
since a chip the user is focused on when another surface deletes the row
would otherwise strand focus on an inert element. The keydown suppression
moved ahead of the deleted bail so a stray Enter can never reach
ProseMirror's keymap and split the paragraph the chip sits in.

.file-chip carries the button reset explicitly (font: inherit), since the
UA's 13.33px Arial would otherwise shrink the live chip away from the
read-only one.
2026-08-04 13:09:07 +00:00

1121 lines
31 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
:root {
/* PLAN-2290 violet refresh — dark is the primary theme. Neutrals are
violet-biased; muted text meets WCAG AA on all bg tokens (TASK-2262 C9). */
--bg-primary: #0b0b10;
--bg-secondary: #14141b;
--bg-tertiary: #1c1c25;
/* Elevated overlay surfaces (menus, popovers). Same as tertiary in
dark; pure white in light where tertiary is a gray input tone. */
--bg-raised: #1c1c25;
--bg-hover: #22222d;
--bg-active: #2a2a36;
--text-primary: #f0f0f4;
--text-secondary: #a9a9b8;
--text-muted: #8b8b9c;
--border: #262633;
--border-subtle: #1d1d27;
--border-strong: #4a4a5e;
/* Card surface family (consumed app-wide from Phase 3 on). */
--card-bg: #16161f;
--card-border: #363648;
--shadow-card: 0 2px 6px rgba(0, 0, 0, 0.35);
/* Tint strength for Chip/badge fills (color-mix percentage). */
--chip-alpha: 16%;
/* Chip TEXT darkening: 100% = raw accent (dark theme); light theme
drops to ~72% mixed toward black so small colored labels hold AA. */
--chip-text-mix: 100%;
/* Brand accent. --accent-blue is aliased to it because ~95% of its 462
call sites mean "brand accent", not "blue" (see PLAN-2290); the
literally-blue categorical sites use --status-blue instead.
#9268f8 (not the mock's #8b5cf6): the one violet that passes AA as
link text on --bg-secondary (4.85:1) while beating the old blue for
white button text (3.78:1 vs 2.75:1). True text-on-fill AA lands
with the Phase 2 Button primitive via --text-on-accent. */
--accent-primary: #9268f8;
--accent-primary-soft: #a78bfa;
/* Filled-control variant: the violet band where white text passes AA
(4.96:1) while the fill stays ≥3:1 against --bg-secondary. Used by
Button primary fills; links/text keep --accent-primary. */
--accent-primary-strong: #7c4ff0;
--accent-blue: var(--accent-primary);
--accent-green: #4ade80;
--accent-purple: #a78bfa;
--accent-amber: #fbbf24;
--accent-cyan: #22d3ee;
--accent-orange: #fb923c;
--accent-gray: #9ca3af;
--accent-brown: #a8896c;
--accent-red: #ef4444;
/* Solid destructive fill — the red where white text passes AA (4.83:1)
in both themes. Button variant="danger-solid" (final confirms). */
--accent-red-strong: #dc2626;
/* Literal blue for categorical uses (status maps, charts, info badges).
Stays blue even if the brand accent changes — see PLAN-2290. */
--status-blue: #4a9eff;
/* Text color on accent-filled surfaces (buttons, filled badges). */
--text-on-accent: #ffffff;
--status-draft: #666666;
--status-active: #4ade80;
--status-completed: #4a9eff;
--status-archived: #444444;
--topbar-height: 44px;
--bottom-nav-height: 56px;
--context-bar-height: 44px;
--sidebar-width: 260px;
--detail-panel-width: 300px;
--content-max-width: 960px;
--font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
--font-content: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
--font-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--radius: 8px;
--radius-sm: 5px;
--radius-lg: 12px;
/* Elevation scale. Values match the long-standing inline fallbacks
(ItemActionsMenu/LaneActionsMenu/BoardView/Modal) so defining them
is behavior-preserving. Theme-invariant for now — PLAN-2290 PR B
may tune per theme. */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15);
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.3);
--modal-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
--scrim: rgba(0, 0, 0, 0.5);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: var(--font-ui);
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
background: var(--bg-primary);
-webkit-font-smoothing: antialiased;
}
a {
color: var(--accent-blue);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button {
font-family: inherit;
font-size: inherit;
cursor: pointer;
border: none;
background: none;
color: inherit;
}
input, textarea, select {
font-family: inherit;
font-size: inherit;
color: var(--text-primary);
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--space-2) var(--space-3);
outline: none;
}
input:focus, textarea:focus {
border-color: var(--accent-blue);
}
/* Scrollbar. `width` styles the VERTICAL scrollbar; `height` styles the
HORIZONTAL one. Both are needed — without `height` a horizontal scrollbar
(e.g. the board-view lane track) falls back to the chunky ~15px browser
default while every vertical scrollbar is a thin 6px, which reads as a
"weirdly large" scrollbar (BUG-2127 follow-up). Keep them equal. */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
/* Prose styles for rendered markdown */
.prose {
font-family: var(--font-content);
font-size: 15px;
line-height: 1.7;
color: var(--text-primary);
max-width: var(--content-max-width);
}
.prose h1 { font-size: 1.8em; margin: 1.2em 0 0.6em; font-weight: 700; }
.prose h2 { font-size: 1.4em; margin: 1em 0 0.5em; font-weight: 600; }
.prose h3 { font-size: 1.15em; margin: 0.8em 0 0.4em; font-weight: 600; }
.prose p { margin: 0.6em 0; }
.prose ul, .prose ol { margin: 0.6em 0; padding-left: 1.5em; }
.prose li { margin: 0.2em 0; }
.prose li input[type="checkbox"] { margin-right: 0.4em; }
.prose pre {
background: var(--bg-tertiary);
padding: var(--space-4);
border-radius: var(--radius);
overflow-x: auto;
margin: 0.8em 0;
font-family: var(--font-mono);
font-size: 0.9em;
line-height: 1.5;
}
.prose code {
background: var(--bg-tertiary);
padding: 0.15em 0.4em;
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: 0.9em;
}
.prose pre code {
background: none;
padding: 0;
}
.prose blockquote {
border-left: 3px solid var(--border);
padding-left: var(--space-4);
margin: 0.8em 0;
color: var(--text-secondary);
}
.prose table {
border-collapse: collapse;
width: 100%;
margin: 0.8em 0;
}
.prose th, .prose td {
border: 1px solid var(--border);
padding: var(--space-2) var(--space-3);
text-align: left;
}
.prose th {
background: var(--bg-secondary);
font-weight: 600;
}
.prose hr {
border: none;
border-top: 1px solid var(--border);
margin: 1.5em 0;
}
.prose img {
max-width: 100%;
border-radius: var(--radius);
}
.prose a.doc-link {
background: var(--bg-tertiary);
padding: 0.1em 0.5em;
border-radius: var(--radius-sm);
color: var(--accent-blue);
font-weight: 500;
}
.prose a.doc-link:hover {
background: var(--bg-hover);
text-decoration: none;
}
.prose .doc-link.broken {
color: var(--accent-orange);
opacity: 0.7;
text-decoration: line-through;
}
/* External links in rendered markdown */
.prose .external-link {
color: var(--accent-blue);
text-decoration: none;
}
.prose .external-link:hover {
text-decoration: underline;
}
.prose .external-icon {
font-size: 0.75em;
opacity: 0.6;
vertical-align: super;
}
/* Editor link styles — autolinked URLs and wiki-links */
.prose a.editor-link {
color: var(--accent-blue);
text-decoration: none;
border-bottom: 1px solid color-mix(in srgb, var(--accent-blue) 30%, transparent);
transition: border-color 0.15s;
}
.prose a.editor-link:hover {
border-bottom-color: var(--accent-blue);
}
/* Attachment images — both inside the editor and in rendered markdown.
Lazy-loaded via the node's renderHTML/NodeView; styles below set a
sane default size so the editor doesn't reflow once thumbnails arrive. */
.attachment-image {
max-width: 100%;
height: auto;
border-radius: var(--radius);
cursor: zoom-in;
display: block;
margin: 0.4em 0;
}
.attachment-image:hover {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-blue) 35%, transparent);
}
/* Wrapper appended by the AttachmentImage NodeView to host the
selection-time rotate toolbar (TASK-879). Position relative so
the toolbar can pin to the top-right corner of the image. */
.attachment-image-wrapper {
position: relative;
display: inline-block;
max-width: 100%;
}
.attachment-image-wrapper.attachment-image-selected .attachment-image {
box-shadow: 0 0 0 2px var(--accent-blue);
}
/* Rotate toolbar — appears when the AttachmentImage node is
selected. Pinned top-right with absolute positioning so it doesn't
shift the image's layout when toggling visibility. */
.attachment-image-toolbar {
position: absolute;
top: 8px;
right: 8px;
display: inline-flex;
gap: 2px;
padding: 4px;
background: rgba(20, 20, 20, 0.85);
border-radius: var(--radius);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
z-index: 5;
-webkit-user-select: none;
user-select: none;
}
.attachment-image-toolbar-hidden {
display: none;
}
.attachment-image-toolbar-btn {
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
border: none;
background: transparent;
color: #fff;
font-size: 18px;
line-height: 1;
border-radius: 4px;
cursor: pointer;
transition: background 0.15s, opacity 0.15s;
font-family: inherit;
}
.attachment-image-toolbar-btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.15);
}
.attachment-image-toolbar-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Attachment file chips — Notion-style chip rendering for non-image
attachments. Used by both the editor's AttachmentChip node and the
read-only markdown render path (TASK-874). Styles target the same
class names from both surfaces so the visual matches edge to edge. */
/* Two elements share this class: the editor NodeView renders a <button> (a
live chip opens the options panel, it does not navigate — PLAN-2392 DR-12),
while the read-only markdown path and the clipboard shape render an <a
download>. So it carries the button reset explicitly: without `font:
inherit` the UA's 13.33px Arial would replace the inherited font and shrink
the chip, and `text-align` keeps the label left-aligned in the button. */
.file-chip {
display: inline-flex;
align-items: center;
gap: 0.4em;
max-width: 100%;
padding: 4px 10px;
margin: 0 1px;
font: inherit;
text-align: start;
background: var(--bg-tertiary);
border: 1px solid var(--border-subtle);
border-radius: 6px;
color: var(--text-primary);
text-decoration: none;
font-size: 0.92em;
line-height: 1.3;
vertical-align: baseline;
transition: background 0.15s, border-color 0.15s;
cursor: pointer;
}
.file-chip:hover {
background: var(--bg-hover);
border-color: color-mix(in srgb, var(--accent-blue) 35%, var(--border));
text-decoration: none;
}
/* The editor NodeView renders an SVG file-type icon here (TASK-2417); the
read-only markdown render path still emits a text glyph, so these rules
have to work for both. currentColor keeps the SVG themed and
forced-colors safe. */
.file-chip-icon {
flex-shrink: 0;
display: flex;
align-items: center;
font-size: 1.05em;
line-height: 1;
}
.file-chip-icon > svg {
color: var(--text-secondary);
}
/* Deleted / missing target. The chip used to signal this by swapping its
glyph to a paperclip; the icon now says what KIND of file it is, so the
dead state gets its own treatment on the chip instead (TASK-2417). The
compound selector keeps this off the markdown renderer's bare
.attachment-missing span, which has its own inline marker. */
.file-chip.attachment-missing {
color: var(--text-muted);
border-style: dashed;
cursor: default;
text-decoration: line-through;
}
/* The NodeView marks a dead chip `disabled`, which is what makes it inert and
unfocusable; keep it looking the same as the read-only path's missing chip
rather than inheriting the UA's disabled-button greying. */
.file-chip:disabled {
color: var(--text-muted);
cursor: default;
opacity: 1;
}
.file-chip.attachment-missing:hover {
background: var(--bg-tertiary);
border-color: var(--border-subtle);
}
.file-chip-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 38ch;
font-weight: 500;
}
.file-chip-size {
flex-shrink: 0;
color: var(--text-muted);
font-size: 0.88em;
}
.file-chip-size:empty {
display: none;
}
/* Editor selection styling — when the chip is the ProseMirror
selection (atom node selected for delete/replace), give it the same
selection treatment ProseMirror gives other inline nodes. */
.ProseMirror-selectednode.file-chip,
.file-chip.ProseMirror-selectednode {
outline: 2px solid var(--accent-blue);
outline-offset: 1px;
}
/* Upload-in-progress placeholder — rendered as a ProseMirror widget
decoration by the AttachmentUpload extension. Dashed border + faded
color so it visually reads as "not yet committed content" while the
network round-trip resolves. */
.attachment-upload-placeholder {
display: inline-flex;
align-items: center;
gap: 0.4em;
padding: 4px 10px;
margin: 0 1px;
background: var(--bg-tertiary);
border: 1px dashed color-mix(in srgb, var(--accent-blue) 50%, var(--border));
border-radius: 6px;
color: var(--text-secondary);
font-size: 0.92em;
line-height: 1.3;
vertical-align: baseline;
}
.attachment-upload-spinner {
width: 12px;
height: 12px;
border: 2px solid color-mix(in srgb, var(--accent-blue) 35%, transparent);
border-top-color: var(--accent-blue);
border-radius: 50%;
animation: attachment-upload-spin 0.7s linear infinite;
flex-shrink: 0;
}
@keyframes attachment-upload-spin {
to {
transform: rotate(360deg);
}
}
.attachment-upload-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 30ch;
}
/* Crop modal (TASK-880). Appended to document.body so styles must
be global. Uses the native <dialog> element with a flex layout
so the image fills available space while header/footer stay fixed. */
dialog.attachment-crop-modal {
border: none;
background: transparent;
padding: 0;
max-width: 100vw;
max-height: 100vh;
color: #fff;
overflow: visible;
}
dialog.attachment-crop-modal::backdrop {
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(2px);
}
.attachment-crop-modal-shell {
display: flex;
flex-direction: column;
width: min(95vw, 1200px);
height: min(92vh, 900px);
background: var(--bg-secondary);
color: var(--text-primary);
border-radius: var(--radius);
overflow: hidden;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
}
.attachment-crop-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--border);
background: var(--bg-secondary);
}
.attachment-crop-modal-title {
font-weight: 600;
font-size: 0.95em;
}
.attachment-crop-modal-aspects {
display: inline-flex;
gap: 4px;
background: var(--bg-tertiary);
padding: 2px;
border-radius: var(--radius);
}
.attachment-crop-modal-aspect-btn {
padding: 4px 12px;
border: none;
background: transparent;
color: var(--text-secondary);
border-radius: 4px;
font-size: 0.85em;
cursor: pointer;
font-family: inherit;
transition: background 0.15s, color 0.15s;
}
.attachment-crop-modal-aspect-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.attachment-crop-modal-aspect-active,
.attachment-crop-modal-aspect-active:hover {
background: var(--accent-blue);
color: #fff;
}
.attachment-crop-modal-stage {
flex: 1;
min-height: 0;
position: relative;
overflow: hidden;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
-webkit-user-select: none;
user-select: none;
}
.attachment-crop-modal-img {
max-width: 100%;
max-height: 100%;
display: block;
-webkit-user-drag: none;
pointer-events: none;
}
.attachment-crop-modal-rect {
position: absolute;
box-sizing: border-box;
border: 1px solid #fff;
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45);
cursor: move;
touch-action: none;
}
.attachment-crop-modal-handle {
position: absolute;
width: 12px;
height: 12px;
background: var(--accent-blue);
border: 2px solid #fff;
border-radius: 50%;
touch-action: none;
}
.attachment-crop-modal-handle-nw {
top: -7px;
left: -7px;
cursor: nwse-resize;
}
.attachment-crop-modal-handle-ne {
top: -7px;
right: -7px;
cursor: nesw-resize;
}
.attachment-crop-modal-handle-sw {
bottom: -7px;
left: -7px;
cursor: nesw-resize;
}
.attachment-crop-modal-handle-se {
bottom: -7px;
right: -7px;
cursor: nwse-resize;
}
.attachment-crop-modal-footer {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
border-top: 1px solid var(--border);
background: var(--bg-secondary);
}
.attachment-crop-modal-cancel,
.attachment-crop-modal-apply {
padding: 6px 16px;
border: 1px solid var(--border);
background: var(--bg-tertiary);
color: var(--text-primary);
border-radius: var(--radius);
font-family: inherit;
font-size: 0.92em;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.attachment-crop-modal-cancel:hover {
background: var(--bg-hover);
}
.attachment-crop-modal-primary {
background: var(--accent-blue);
color: #fff;
border-color: var(--accent-blue);
}
.attachment-crop-modal-primary:hover {
filter: brightness(1.1);
}
/* Lightbox dialog — appended to document.body when an attachment image
is clicked, so styles must be global rather than scoped to Editor.svelte.
Uses the native <dialog> element; the ::backdrop pseudo handles the
overlay shade. The dialog content fills the viewport with a centered
image and a single close button. */
dialog.attachment-image-lightbox {
border: none;
background: transparent;
padding: 0;
max-width: 100vw;
max-height: 100vh;
overflow: visible;
color: #fff;
}
dialog.attachment-image-lightbox::backdrop {
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(2px);
}
dialog.attachment-image-lightbox .attachment-image-lightbox-img {
max-width: 95vw;
max-height: 95vh;
display: block;
margin: 0 auto;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
cursor: zoom-out;
border-radius: var(--radius);
}
dialog.attachment-image-lightbox .attachment-image-lightbox-close {
position: fixed;
top: 16px;
right: 16px;
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 28px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
dialog.attachment-image-lightbox .attachment-image-lightbox-close:hover {
background: rgba(0, 0, 0, 0.85);
}
/* Light mode */
[data-theme="light"] {
--bg-primary: #f5f5f9;
--bg-secondary: #ffffff;
--bg-tertiary: #efeff4;
--bg-raised: #ffffff;
--bg-hover: #ededf3;
--bg-active: #e3e3ec;
--text-primary: #191922;
--text-secondary: #4d4d5c;
--text-muted: #63637a;
--border: #dcdce6;
--border-subtle: #e8e8f0;
--border-strong: #b4b4c6;
--card-bg: #ffffff;
--card-border: #d0d0dc;
--shadow-card: 0 1px 3px rgba(23, 23, 40, 0.1);
--chip-alpha: 12%;
--chip-text-mix: 72%;
--accent-primary: #7c3aed;
--accent-primary-soft: #8b5cf6;
--accent-primary-strong: #7c3aed;
--accent-blue: var(--accent-primary);
--accent-red: #dc2626;
--accent-green: #16a34a;
--accent-purple: #7c3aed;
--accent-amber: #d97706;
--accent-cyan: #0891b2;
--accent-orange: #ea580c;
--accent-gray: #6b7280;
--accent-brown: #92400e;
--accent-red: #ef4444;
--status-blue: #2563eb;
--status-draft: #999999;
--status-active: #16a34a;
--status-completed: #2563eb;
--status-archived: #bbbbbb;
}
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
--bg-primary: #f5f5f9;
--bg-secondary: #ffffff;
--bg-tertiary: #efeff4;
--bg-raised: #ffffff;
--bg-hover: #ededf3;
--bg-active: #e3e3ec;
--text-primary: #191922;
--text-secondary: #4d4d5c;
--text-muted: #63637a;
--border: #dcdce6;
--border-subtle: #e8e8f0;
--border-strong: #b4b4c6;
--card-bg: #ffffff;
--card-border: #d0d0dc;
--shadow-card: 0 1px 3px rgba(23, 23, 40, 0.1);
--chip-alpha: 12%;
--chip-text-mix: 72%;
--accent-primary: #7c3aed;
--accent-primary-soft: #8b5cf6;
--accent-primary-strong: #7c3aed;
--accent-blue: var(--accent-primary);
--accent-red: #dc2626;
--accent-green: #16a34a;
--accent-purple: #7c3aed;
--accent-amber: #d97706;
--accent-cyan: #0891b2;
--accent-orange: #ea580c;
--accent-gray: #6b7280;
--accent-brown: #92400e;
--accent-red: #ef4444;
--status-blue: #2563eb;
--status-draft: #999999;
--status-active: #16a34a;
--status-completed: #2563eb;
--status-archived: #bbbbbb;
}
}
/* Responsive */
@media (max-width: 768px) {
:root {
--sidebar-width: 280px;
}
}
/* ============================================================================
Print styles (PLAN-620 / TASK-621).
Tune Ctrl/Cmd+P output so users can save any Pad page as a clean PDF.
This file handles LAYOUT-LEVEL concerns:
- hide the workspace chrome (sidebar, top bar, modals, floating UI)
- force a light color palette regardless of the active theme
- unlock the fixed 100vh / overflow:hidden app shell so content flows
across pages in the print preview
Item-level formatting (title, properties block, markdown body, header /
footer rules, child-item checklist) is layered on top in tasks 622624.
============================================================================ */
@media print {
/* Force a light color palette for print regardless of the active theme.
We only need to pin the color variables; spacing / radii stay. */
:root,
:root[data-theme="dark"],
:root[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #ffffff;
--bg-tertiary: #f5f5f5;
--bg-hover: transparent;
--bg-active: transparent;
--text-primary: #000000;
--text-secondary: #222222;
--text-muted: #555555;
--border: #cccccc;
--border-subtle: #dddddd;
/* Pin light-theme accents for paper — otherwise printing from the
dark theme puts the bright dark-mode accent on white (PR #1018
Codex finding). */
--accent-primary: #7c3aed;
--accent-blue: var(--accent-primary);
--accent-red: #dc2626;
}
html, body {
height: auto;
background: #ffffff !important;
color: #000000 !important;
font-size: 11pt;
}
/* Unlock the screen shell — it uses height: 100vh / overflow: hidden
to keep the sidebar + main column pinned. In print we need the
content to flow naturally across pages. */
.app-layout,
.app-shell {
display: block !important;
height: auto !important;
max-height: none !important;
overflow: visible !important;
}
.main-content {
flex: none !important;
width: 100% !important;
min-width: 0 !important;
overflow: visible !important;
}
/* Hide the workspace chrome. */
.sidebar,
.topbar,
.sidebar-expand-btn,
.topbar-expand-btn,
.mobile-header,
.hamburger,
.toast-container,
.overlay,
.palette,
.modal,
.modal-backdrop,
dialog[open] {
display: none !important;
}
/* Modals / dialogs / tooltips. Most KeyboardShortcuts-style overlays
carry role="dialog" on the backdrop. The shared Modal primitive uses a
native <dialog> (implicit dialog role, no literal role attribute), so it
is covered by the `dialog[open]` selector above.
`:not(.item-pane)` keeps the detail pane printable: on mobile the pane
carries role="dialog" (TASK-2131) but IS the content being printed, not a
transient overlay to strip. */
[role="dialog"]:not(.item-pane),
[role="tooltip"],
[role="menu"] {
display: none !important;
}
/* Opt-in: any element tagged with data-print-hide disappears in print.
Components can use this to strip interactive affordances (edit buttons,
status pickers, hover cards, etc.) without needing a new CSS rule. */
[data-print-hide] {
display: none !important;
}
/* Strip decorative shadows, gradients, and background images. */
* {
box-shadow: none !important;
text-shadow: none !important;
background-image: none !important;
}
/* Page margins + page number (PLAN-620 / BUG-625).
- Top/bottom margins carve out space for the rendered header /
footer (TASK-623). Previously declared in +page.svelte, but
Svelte scoped-style timing meant the rule didn't always win
over this block, causing the header strip to overlap the
title on page 1. Single source of truth here.
- The page number lives in `@bottom-right` because
`counter(page)` evaluated inside a fixed element's ::after
pseudo captures its initial value (0) and never increments
per page in Chromium. Margin-box counters evaluate per page
correctly. */
@page {
/* Top margin is generous-but-plain (no reserved header strip):
the page-1 document header lives in normal flow via
`.title-row` + `.meta-info`, so subsequent pages start at
this margin with no overlap risk. Bottom margin reserves
space for the fixed footer + the `@bottom-right` page number.
See BUG-626 for why the repeating header was dropped. */
margin: 0.6in 0.6in 1in 0.6in;
@bottom-right {
content: "Page " counter(page);
font-family: var(--font-ui);
font-size: 9pt;
color: #555;
}
}
/* -----------------------------------------------------------------
TASK-622 — rendered markdown (.prose) tuned for print.
----------------------------------------------------------------- */
.prose {
font-size: 11pt;
line-height: 1.55;
color: #000 !important;
max-width: none;
}
.prose h1, .prose h2, .prose h3,
.prose h4, .prose h5, .prose h6 {
color: #000 !important;
page-break-after: avoid;
break-after: avoid-page;
page-break-inside: avoid;
break-inside: avoid-page;
}
.prose h1 { font-size: 18pt; }
.prose h2 { font-size: 14pt; }
.prose h3 { font-size: 12pt; }
.prose p, .prose li {
orphans: 3;
widows: 3;
}
/* Links: keep the colored style AND append the URL inline so a printed
page still carries the reference. Skip internal wiki-links (which
resolve to the printed document's siblings) and fragment-only links.
Note: rich-editor links are rendered via Tiptap's SafeLink extension
using `data-href` instead of `href`, so we target both. */
.prose a[href]::after {
content: " (" attr(href) ")";
font-size: 0.85em;
color: #555;
word-break: break-all;
}
.prose a[data-href]::after {
content: " (" attr(data-href) ")";
font-size: 0.85em;
color: #555;
word-break: break-all;
}
.prose a[href^="#"]::after,
.prose a[data-href^="#"]::after,
.prose a.doc-link::after,
.prose a.editor-link[href^="/"]::after,
.prose a.editor-link[data-href^="/"]::after {
content: "";
}
/* Code blocks: visible, readable in light palette, don't orphan across
pages when we can help it. */
.prose pre {
background: #f5f5f5 !important;
border: 1px solid #ddd !important;
color: #000 !important;
font-size: 9.5pt;
page-break-inside: avoid;
break-inside: avoid;
}
.prose code {
background: #f5f5f5 !important;
color: #000 !important;
}
/* Images: full page width max, don't split across pages. */
.prose img {
max-width: 100% !important;
page-break-inside: avoid;
break-inside: avoid;
}
/* Tables: visible borders, contained to page width. */
.prose table {
page-break-inside: avoid;
break-inside: avoid;
border: 1px solid #ccc !important;
}
.prose th, .prose td {
border: 1px solid #ccc !important;
color: #000 !important;
}
.prose th {
background: #f5f5f5 !important;
}
.prose blockquote {
border-left: 3px solid #666 !important;
color: #333 !important;
}
/* Editor overlays (bubble menu, link popover, slash menu, mobile
toolbar, table toolbar, editor-toolbar) — never print these. */
.bubble-menu,
.link-popover,
.slash-menu,
.mobile-toolbar,
.table-toolbar,
.toolbar,
.editor-toolbar,
.editor-link-popover,
.editor-bubble-menu {
display: none !important;
}
/* Editor wrapper: in print the content is read-only — remove any
interactive cursor, borders, or focus styling. */
.editor-wrapper,
.editor-content {
border: none !important;
background: transparent !important;
padding: 0 !important;
outline: none !important;
}
/* -----------------------------------------------------------------
TASK-622 — Properties panel form widgets in print.
These rules live here (not in the item detail page's scoped style
block) because the widgets are rendered by the FieldEditor child
component — Svelte's scoped selectors don't cross component
boundaries. Strip interactive styling so the selected value reads
as plain text; the `.toggle` checkbox button is exempted and
rendered as a visible outlined box with a check mark when on.
----------------------------------------------------------------- */
.field-value select,
.field-value input,
.field-value textarea,
.field-value button:not(.toggle) {
appearance: none !important;
-webkit-appearance: none !important;
background: transparent !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
color: #000 !important;
font: inherit !important;
cursor: default !important;
pointer-events: none;
box-shadow: none !important;
}
/* Custom select controls (FieldEditor) render a dropdown chevron as
an inline SVG (`.select-chevron`) inside the button — `appearance:
none` on the button doesn't affect SVG children, so the `` caret
leaks into print. Same story for the expanded dropdown panel
(`.select-dropdown`) if one happens to be open at print time.
BUG-625. */
.field-value .select-chevron,
.field-value .select-dropdown,
.select-chevron,
.select-dropdown {
display: none !important;
}
/* Checkbox field (`.toggle`): visible outlined box; on-state gets a
check mark overlay so the on/off signal survives in print. */
.field-value .toggle {
appearance: none !important;
-webkit-appearance: none !important;
display: inline-block !important;
width: 11pt !important;
height: 11pt !important;
padding: 0 !important;
margin: 0 !important;
background: #fff !important;
border: 1px solid #000 !important;
border-radius: 2pt !important;
position: relative;
cursor: default !important;
pointer-events: none;
box-shadow: none !important;
vertical-align: -1pt;
}
.field-value .toggle-knob {
display: none !important;
}
.field-value .toggle.on::after {
content: "✓";
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #000;
font-size: 10pt;
font-weight: 700;
line-height: 1;
}
}
/*
Mobile bottom-nav reflow (PLAN-1694). <BottomNav /> is rendered in the
workspace layout and uses position: fixed; bottom: 0 on mobile. It toggles
`body.has-bottom-nav` while mounted on mobile so the scroll container
(.main-content, defined in the root +layout.svelte) gets bottom padding and
content can't hide under the bar. Gated by the same ≤768px breakpoint
uiStore.isMobile uses, and by the body class so non-workspace shell pages
(which share .main-content but have no bottom nav) aren't padded. The
safe-area inset keeps the last row clear of the iOS home indicator.
*/
@media (max-width: 768px) {
body.has-bottom-nav .main-content {
padding-bottom: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom, 0px));
}
/*
Contextual top bar on detail screens (MobileContextBar, PLAN-1694
Phase 2). It's position: fixed; top: 0 and toggles body.has-context-bar
while shown, so pad the scroll container's top to match. Same body-class
pattern as the bottom-nav reflow above; safe-area inset clears the notch.
*/
body.has-context-bar .main-content {
padding-top: calc(var(--context-bar-height) + env(safe-area-inset-top, 0px));
}
}