Files
pad/web/src/app.css
T
xarmian 5c491b78b5 feat(web): mobile bottom navigation (PLAN-1694) (#692)
* feat(web): mobile bottom navigation + quick-capture (PLAN-1694)

Add a mobile-only persistent bottom navigation bar that coexists with the
existing TopBar. Five slots: Dashboard, Search, Quick-capture (center +),
Activity, More. The "More" slot opens a BottomSheet with the sidebar's
overflow destinations + collections; the center action opens a quick-capture
sheet that creates an item via the existing API.

- New shared nav source (lib/nav/destinations.ts) consumed by both the
  desktop Sidebar and the new BottomNav/More sheet so the two can't drift;
  Sidebar refactored to derive active-state from it (lossless).
- BottomNav mounts in the workspace layout (workspace-only), shows only on
  mobile (uiStore.isMobile, ≤768px), and toggles body.has-bottom-nav so
  app.css reflows .main-content above the fixed bar.
- iOS: app.html viewport-fit=cover + env(safe-area-inset-bottom) padding.
- z-index 40 (above TopBar/sidebar, below sheets).

Header retirement + full "You" (workspace/account) consolidation are
deliberately deferred to a human design checkpoint (DR-3).

Verify: web npm run check (0 errors) + npm run build (✓). Codex review loop
clean (1 P2 fixed: quick-capture re-defaults a stale collection slug).

Refs PLAN-1694 (TASK-1695, TASK-1696, TASK-1697, TASK-1698); parent ROADM-28.

* feat(web): retire mobile header in-workspace, add You sheet + contextual bar (PLAN-1694)

Complete the mobile nav vision on top of the bottom bar: inside a workspace
the global mobile header is gone, replaced by the BottomNav + a "You" sheet +
a contextual back/title bar on detail screens.

- BottomNav 5th slot More → You (👤). New YouSheet composes the workspace
  switcher, the nav overflow, and the full account menu carried over from the
  retired TopBar (Workspaces/Settings/Billing/Admin/theme/Resources/Connect/
  Sign out) so nothing is lost.
- Root layout gates <TopBar mobile /> on !inWorkspace (page.params.workspace),
  keeping it for the non-workspace picker/home which has no BottomNav; the
  .app-layout top offset now applies only via body.has-mobile-topbar.
- New MobileContextBar: fixed back+title bar shown only on detail screens
  (path depth ≥2); body.has-context-bar reflows .main-content top. Root/tab
  screens render full-height (reclaimed space).

Codex consensus + review clean (no P1). P2s fixed: safe in-app-history check
via afterNavigate (deep-link fallback to parent URL), humanized title fallback
for pages that don't wire titleStore, and the You sheet closes on navigation
so a workspace switch dismisses it.

Verify: web npm run check (0 errors) + npm run build (✓).

Refs PLAN-1694 (TASK-1699, TASK-1700); parent ROADM-28.

* feat(web): redesign mobile nav into Workspace + You docked sheets (PLAN-1694)

Address design feedback: the previous "You" sheet reused common/BottomSheet
(covered the nav bar) and embedded the old WorkspaceSwitcher (plain, unadapted).
Replace it with two purpose-built surfaces that dock ABOVE the nav.

- DockedSheet (new): bottom sheet anchored above the bottom nav — backdrop
  stops at the nav's top edge so the bar stays visible + tappable and the
  active slot stays lit. ~2/3 height, grab handle, slide-up, swipe-down /
  tap-out / Escape to dismiss. No more full-screen overlay covering the nav.
- Bottom-nav slot 1 Dashboard → Workspace (icon = current workspace avatar),
  opening WorkspaceSheet: a designed switcher card (inline-expand list) +
  Navigate tile grid + Collections.
- "You" slot → YouSheet, now account-only: profile header, theme toggle,
  account Settings / Workspaces / Billing / Admin / Connect / Resources /
  Sign out. Search / + / Activity unchanged.
- New avatar util (avatarColor/avatarInitial) for workspace + user avatars.

Splits the overstuffed single sheet into two focused surfaces (where-am-I vs
me) and removes the reused WorkspaceSwitcher/BottomSheet. Codex review CLEAN.

Verify: web npm run check (0 errors) + npm run build (✓).

Refs PLAN-1694 (TASK-1701); parent ROADM-28.

* feat(web): toggle Workspace/You sheets on repeat nav tap (PLAN-1694)

Tapping the Workspace or You nav slot a second time now closes its open
sheet (tap to open, tap again to close). Opening one still closes the other.
The docked backdrop stops above the nav, so the slot button stays tappable
while its sheet is open.

Refs PLAN-1694 (TASK-1701).

* feat(web): dock mobile search to match the nav sheets (PLAN-1694)

The CommandPalette was the last mobile surface that didn't match — a
full-screen takeover (square, no handle, hid the nav). Give its mobile
presentation the same docked treatment as the Workspace/You sheets;
desktop ⌘K is untouched.

- Mobile: dock the palette above the bottom nav (backdrop stops at the nav's
  top edge so the bar stays visible), rounded top, grab handle with
  swipe-down-to-dismiss, tap-outside to close, no X. Lift above the nav only
  when it's present (:global(body.has-bottom-nav)); flush to bottom on the
  non-workspace picker. Keyboard handling preserved: input stays at the sheet
  top, .results is the sole scroll area, min-height keeps the input clear of
  the keyboard when empty.
- BottomNav: Search slot toggles (tap to open, tap again to close) and lights
  up while open; the three surfaces (Workspace / You / Search) are now
  mutually exclusive.

Codex review CLEAN. Verify: web npm run check (0 errors) + build (✓).
Note: on-screen-keyboard feel should get an on-device smoke test.

Refs PLAN-1694 (TASK-1701); parent ROADM-28.
2026-06-02 08:41:54 -04:00

986 lines
25 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 {
--bg-primary: #1a1a1a;
--bg-secondary: #242424;
--bg-tertiary: #2e2e2e;
--bg-hover: #353535;
--bg-active: #3a3a3a;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--text-muted: #666666;
--border: #333333;
--border-subtle: #2a2a2a;
--accent-blue: #4a9eff;
--accent-green: #4ade80;
--accent-purple: #a78bfa;
--accent-amber: #fbbf24;
--accent-cyan: #22d3ee;
--accent-orange: #fb923c;
--accent-gray: #9ca3af;
--accent-brown: #a8896c;
--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: 6px;
--radius-sm: 4px;
--radius-lg: 8px;
}
* {
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 */
::-webkit-scrollbar {
width: 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. */
.file-chip {
display: inline-flex;
align-items: center;
gap: 0.4em;
max-width: 100%;
padding: 4px 10px;
margin: 0 1px;
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;
}
.file-chip-icon {
flex-shrink: 0;
font-size: 1.05em;
line-height: 1;
}
.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: #ffffff;
--bg-secondary: #f7f7f8;
--bg-tertiary: #eeeff1;
--bg-hover: #e5e6e8;
--bg-active: #dddee0;
--text-primary: #1a1a1a;
--text-secondary: #555555;
--text-muted: #999999;
--border: #e0e0e0;
--border-subtle: #eaeaea;
--accent-blue: #2563eb;
--accent-green: #16a34a;
--accent-purple: #7c3aed;
--accent-amber: #d97706;
--accent-cyan: #0891b2;
--accent-orange: #ea580c;
--accent-gray: #6b7280;
--accent-brown: #92400e;
--status-draft: #999999;
--status-active: #16a34a;
--status-completed: #2563eb;
--status-archived: #bbbbbb;
}
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
--bg-primary: #ffffff;
--bg-secondary: #f7f7f8;
--bg-tertiary: #eeeff1;
--bg-hover: #e5e6e8;
--bg-active: #dddee0;
--text-primary: #1a1a1a;
--text-secondary: #555555;
--text-muted: #999999;
--border: #e0e0e0;
--border-subtle: #eaeaea;
--accent-blue: #2563eb;
--accent-green: #16a34a;
--accent-purple: #7c3aed;
--accent-amber: #d97706;
--accent-cyan: #0891b2;
--accent-orange: #ea580c;
--accent-gray: #6b7280;
--accent-brown: #92400e;
--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 622–624.
============================================================================ */
@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;
}
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 {
display: none !important;
}
/* Modals / dialogs / tooltips. Most KeyboardShortcuts-style overlays
carry role="dialog" on the backdrop. */
[role="dialog"],
[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));
}
}