Files
datascape/assets/style.css
T
2026-07-20 08:54:12 +02:00

962 lines
33 KiB
CSS

/* === Fonts === */
@font-face {
font-family: "Iosevka Etoile";
src: url("/_/fonts/IosevkaEtoile.woff2") format("woff2");
font-display: swap;
}
@font-face {
font-family: "Iosevka Slab";
src: url("/_/fonts/IosevkaSlab.woff2") format("woff2");
font-display: swap;
}
/* === Theme === */
:root {
--bg: #2e2e2e;
--bg-panel: #434343;
--bg-panel-hover: #585858;
--text: #e6e6e6;
--text-muted: #cfcfcf;
--primary: #87458a;
--primary-hover: #d64d95;
--secondary: #c48401;
--link: #01b6c4;
--link-hover: #d6d24d;
--danger: #c40141;
--danger-hover: #d03467;
/* Translucent scrim for badges/overlays sitting on top of media. */
--overlay: rgba(0, 0, 0, 0.55);
--border: 1px solid var(--secondary);
--border-dashed: 1px dashed var(--secondary);
/* Subtle vertical rail for tree-nesting depth — quieter than --border so
it reads as a guide, not a divider. */
--border-guide: 1px solid var(--bg-panel-hover);
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.5rem;
--font-xs: 0.75rem;
--font-sm: 0.85rem;
/* Width of the persistent left folder-tree rail (desktop). */
--tree-width: 15rem;
/* Comfortable max reading width for the center column's content + footer.
The shell itself is edge-to-edge; this keeps prose lines readable on
wide monitors (content stays centered within the center column). */
--reading-width: 60rem;
}
/* Anchor / TOC jumps are handled by the .center scroll container's own
scroll-padding-top (the header no longer overlaps content in the app-shell),
so no html-level scroll padding is needed. */
/* === Base === */
/* App-shell: the body is a full-viewport, non-scrolling grid of two rows —
the fixed-height header and a 1fr shell that owns all scrolling internally.
100dvh (not 100vh) so the mobile browser's dynamic toolbar doesn't clip the
bottom row; overflow:hidden pins the header and lets each shell column scroll
on its own. */
body {
background: var(--bg);
color: var(--text);
height: 100dvh;
margin: 0;
padding: 0;
overflow: hidden;
font: 1rem "Iosevka Etoile", monospace;
display: grid;
grid-template-rows: auto 1fr;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
a { color: var(--text); text-decoration: none; }
a:hover { color: var(--primary-hover); }
.content a { color: var(--link); }
.content a:hover { color: var(--link-hover); }
.content a.broken { color: var(--primary-hover); text-decoration: line-through; }
.content a.broken:hover { color: var(--link-hover); }
hr { border: none; border-top: var(--border-dashed); margin: var(--space-4) 0; }
/* === Layout primitives ===
.row and .col are shared flex recipes; gap-* modifiers cover the cases
where the default rhythm doesn't fit. */
.row { display: flex; align-items: center; gap: var(--space-2); }
.col { display: flex; flex-direction: column; gap: var(--space-3); }
.gap-1 { gap: var(--space-1); }
.gap-2 { gap: var(--space-2); }
.gap-3 { gap: var(--space-3); }
.gap-4 { gap: var(--space-4); }
.space-between { justify-content: space-between; }
.divider-dashed { border-bottom: var(--border-dashed); }
/* === Page layout (app-shell) ===
The shell is a full-height flex row below the header: left rail | center |
right rail. Each column owns its own vertical scroll (overflow-y:auto +
min-height:0), so nothing scrolls the page as a whole. Edge-to-edge: no
centered max-width container. The right rail collapses out of the flex flow
when empty (:empty { display:none }), letting the center reclaim the width. */
.shell {
display: flex;
min-height: 0;
overflow: hidden;
}
/* Center column: main content plus the footer beneath it, scrolling together.
Content and footer are held to a comfortable reading width and centered
within the (edge-to-edge) column. */
.center {
flex: 1 1 auto;
min-width: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
align-items: center;
scroll-padding-top: var(--space-4);
}
main {
width: 100%;
max-width: var(--reading-width);
padding: var(--space-5) var(--space-4);
flex: 1 0 auto;
min-width: 0;
}
/* === Header / footer ===
Header is the app-shell's fixed top row (grid row 1 of <body>): full width,
never scrolls. Three-column grid (breadcrumbs left, search centre, actions
right) so the centre stays reserved even when search is hidden in editor
mode; the far-right actions column hosts the [ACTIONS] dropdown. position +
z-index keep the header (and its open dropdown) painting above the shell.
Mobile (≤1100px) collapses to a compact three-column layout — see responsive
block below. */
header {
position: relative;
z-index: 40;
background: var(--bg);
padding: var(--space-3) var(--space-4);
border-bottom: var(--border-dashed);
display: grid;
grid-template-columns: 1fr minmax(0, 60rem) 1fr;
grid-template-areas: "crumbs search actions";
align-items: center;
gap: var(--space-2);
}
/* Footer rides at the end of the center column's content (scrolls with it, not
pinned to the viewport). Held to the same reading width as main so its top
border delimits the content, not the whole shell. */
footer {
width: 100%;
max-width: var(--reading-width);
padding: var(--space-3) var(--space-4);
border-top: var(--border-dashed);
display: flex;
align-items: center;
gap: var(--space-2);
flex-wrap: wrap;
}
.breadcrumb { grid-area: crumbs; gap: var(--space-1); min-width: 0; }
.header-actions { grid-area: actions; justify-content: flex-end; flex-wrap: wrap; }
.logo { width: 1.1em; height: 1.1em; vertical-align: center; }
/* === Panel ===
Bordered container recipe shared by listings, sidebar widgets, the tree
picker, the floating TOC. .panel-floating raises the background for menus
and modals that sit over content. */
.panel { border: var(--border); background: var(--bg); }
.panel-floating { background: var(--bg-panel); }
.panel-header {
font-size: var(--font-xs);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
border-bottom: var(--border-dashed);
padding-bottom: var(--space-1);
margin-bottom: 0.4rem;
}
.panel-body {
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-3);
word-break: break-word;
}
.panel-footer {
padding: 0.6rem var(--space-4);
border-top: var(--border-dashed);
display: flex;
justify-content: space-between;
gap: var(--space-2);
}
/* === Buttons === */
.btn {
background: none;
border: none;
color: var(--text);
font: inherit;
cursor: pointer;
padding: 0;
text-decoration: none;
display: inline-block;
white-space: nowrap;
}
.btn::before { content: "["; color: var(--secondary); }
.btn::after { content: "]"; color: var(--secondary); }
.btn:hover { color: var(--primary-hover); }
.btn-small { font-size: 0.8rem; font-weight: normal; vertical-align: middle; }
.btn-tool { padding: 0 0.15rem; }
.btn-block {
display: flex;
justify-content: space-between;
align-items: baseline;
width: 100%;
border: none;
padding: 0.3rem var(--space-3);
white-space: nowrap;
}
.btn-fab {
background: var(--bg-panel);
border: var(--border);
width: 3rem;
height: 3rem;
font-size: 1.5rem;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-fab::before, .btn-fab::after { content: none; }
.btn-fab:hover { background: var(--bg-panel-hover); color: var(--primary-hover); }
.danger { color: var(--danger); }
.danger:hover { color: var(--danger-hover); }
/* Selected segmented-toggle button (view-settings modal). */
.btn.is-active { color: var(--primary-hover); }
/* === Form controls ===
.input baseline is shared by search-input, modal inputs, and the editor
textarea. Hosts layer their own background / font-family / sizing on top. */
.input {
width: 100%;
background: var(--bg-panel);
border: var(--border);
color: var(--text);
font: inherit;
padding: 0.3rem var(--space-2);
outline: none;
}
.input:focus { border-color: var(--primary-hover); }
/* === Typography utilities === */
.muted { color: var(--text-muted); font-size: var(--font-sm); }
.small { font-size: var(--font-sm); }
.caption {
font-size: var(--font-xs);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
}
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* === Markdown content === */
.content { margin-bottom: 2rem; }
.content h1, .content h2, .content h3,
.content h4, .content h5, .content h6,
main > h2 {
color: var(--text);
margin: 1.25rem 0 var(--space-2);
line-height: 1.3;
}
.content h1 {
font-size: 1.75rem;
border-bottom: var(--border-dashed);
padding-bottom: var(--space-1);
}
.content h2, main > h2 { font-size: 1.4rem; }
.content h3 { font-size: 1.15rem; }
.content p { margin: var(--space-3) 0; }
.content ul, .content ol { margin: var(--space-3) 0 var(--space-3) var(--space-5); }
.content li { margin: var(--space-1) 0; }
.content blockquote {
border-left: 3px solid var(--secondary);
padding: var(--space-1) var(--space-4);
color: var(--text-muted);
margin: var(--space-3) 0;
}
.content code {
font-family: "Iosevka Etoile", monospace;
font-size: 0.875em;
background: var(--bg-panel);
padding: 0.1em 0.35em;
}
.content pre {
background: var(--bg-panel);
border: var(--border);
padding: var(--space-4);
overflow-x: auto;
margin: var(--space-3) 0;
}
.content pre code { background: none; padding: 0; }
.content hr { margin: var(--space-5) 0; }
.content img { max-width: 100%; }
.content li:has(> input.task-checkbox:checked) {
color: var(--text-muted);
text-decoration: line-through;
}
a.heading-anchor {
color: var(--text-muted);
margin-right: 0.4em;
font-weight: normal;
text-decoration: none;
}
a.heading-anchor:hover { color: var(--primary-hover); }
/* === Data tables ===
Shared style for the file listing, search-suggestion dropdown, and
markdown content tables. .data-table-grid adds per-cell borders + a
header band for content (markdown) tables. */
.data-table { width: 100%; border-collapse: collapse; }
.data-table th,
.data-table td { padding: 0.4rem var(--space-3); text-align: left; }
.data-table:not(.data-table-grid) tbody tr + tr { border-top: var(--border); }
.data-table tr.is-active { background: var(--bg-panel-hover); }
/* Hover-highlight only on devices with a real pointer. On touch, a tap or
drag-scroll leaves an emulated :hover stuck on the last-touched row until
the next tap — distracting while scrolling a listing. */
@media (hover: hover) {
.data-table:not(.data-table-grid) tbody tr:hover { background: var(--bg-panel-hover); }
}
.data-table tbody tr.is-empty,
.data-table tbody tr.is-empty:hover {
color: var(--text-muted);
background: none;
cursor: default;
}
.data-table-grid th,
.data-table-grid td { border: var(--border); }
.data-table-grid th { background: var(--bg-panel); color: var(--text); }
.content .data-table-grid { margin: var(--space-3) 0; font-size: 0.9rem; }
/* File listing rows */
.list-item { font-size: 0.95rem; }
.list-item > td { padding: 0.6rem var(--space-4); }
.list-item td.icon { width: 1.25rem; text-align: center; }
.list-item td.name { overflow-wrap: anywhere; }
.list-item td.name a { color: inherit; display: block; }
.list-item td.meta {
color: var(--text-muted);
font-size: 0.8rem;
white-space: nowrap;
text-align: right;
}
/* === Dropdown menu === */
.dropdown { position: relative; }
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 100;
min-width: 9rem;
display: none;
background: var(--bg-panel);
border: var(--border);
}
.dropdown-menu.align-right { left: auto; right: 0; }
.dropdown-menu.open-up { top: auto; bottom: 100%; margin-bottom: 0.4rem; }
.dropdown-menu.is-open { display: block; }
.dropdown-menu.scrollable { max-height: 23rem; overflow-y: auto; }
/* === Suggestion dropdown (header search + editor link picker) ===
Anchored to a position:relative host. Mirrors .dropdown-menu visuals with
a dashed border and bg-panel-hover for the active row. */
.suggest-host { position: relative; }
.suggest-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 200;
background: var(--bg-panel);
border: var(--border-dashed);
border-top: none;
display: none;
}
.suggest-dropdown.is-open { display: block; }
.suggest-row { cursor: pointer; }
.suggest-row > td { padding: 0.4rem 0.6rem; }
.suggest-name, .suggest-path { display: block; }
.suggest-name { color: var(--text); }
.suggest-path { color: var(--text-muted); font-size: 0.8rem; margin-top: 0.1rem; }
.suggest-footer > td { color: var(--link); font-size: var(--font-sm); }
/* === Editor toolbar ===
Single non-wrapping row that scrolls horizontally (swipe on mobile) rather
than breaking into stacked rows. A horizontal-scroll container also clips
overflow-y, so open dropdown menus are pinned to the viewport via JS
(editor/main.js pinMenu) to escape the clip. */
.editor-toolbar {
display: flex;
flex-wrap: nowrap;
gap: var(--space-1);
border: var(--border);
border-bottom: none;
padding: 0.4rem 0.6rem;
background: var(--bg-panel-hover);
overflow-x: auto;
scrollbar-width: none;
}
.editor-toolbar::-webkit-scrollbar { display: none; }
.editor-toolbar > * { flex-shrink: 0; }
.toolbar-sep {
width: 1px;
background: var(--secondary);
margin: 0 0.2rem;
align-self: stretch;
}
/* === Edit form === */
.edit-form { display: flex; flex-direction: column; }
/* No rails are rendered while editing, so the editor uses the full center
column: drop the reading-width cap on main (and the footer) so the toolbar
and CodeMirror mount span the whole width. */
body.edit-mode main,
body.edit-mode footer { max-width: none; }
/* CodeMirror mount. The .cm-editor visual treatment (border, bg, font, padding)
lives in the CM theme (editor-build/entry.js), keyed off the same :root
variables; this only sizes the container. */
.editor-cm { min-height: 60vh; }
.editor-cm .cm-editor { height: 100%; }
/* === Search === */
.search-form {
grid-area: search;
display: flex;
gap: var(--space-1);
position: relative;
justify-self: center;
width: 100%;
max-width: 40rem;
}
.search-input { font-size: 0.9rem; }
.search-card {
display: flex;
flex-direction: column;
gap: var(--space-1);
margin-bottom: var(--space-4);
word-break: break-word;
}
.search-card a { color: var(--link); font-size: 1.1rem; }
.search-card a:hover { color: var(--link-hover); }
.search-section {
padding-bottom: var(--space-2);
border-bottom: var(--border-dashed);
}
/* === Floating action button ===
Standalone FAB buttons (the tree rail, the right rail) are mobile-only and
stack bottom-right: the tree FAB anchors the bottom, the right-rail FAB sits
above it. Wrapped FABs (the search actions dropdown) stay visible on desktop
and, on mobile, sit in the upper slot so they never overlap the tree FAB. */
.fab { position: fixed; bottom: var(--space-4); right: var(--space-4); z-index: 50; }
button.fab { display: none; }
.fab-rail { bottom: calc(var(--space-4) + 3rem + var(--space-2)); }
/* Todo FAB: third slot, above the tree and right-rail FABs. Only created when
todo.txt exists (todo-rail.js). */
.fab-todo { bottom: calc(var(--space-4) + 2 * (3rem + var(--space-2))); }
/* === Companion status === */
.companion-status { margin-left: auto; }
.companion-icon { font-size: 0.9rem; line-height: 1; padding: 0.1rem 0.3rem; }
.companion-on { color: var(--link); }
.companion-off { color: var(--text-muted); }
.companion-flyout { min-width: 14rem; padding: 0.4rem; }
.companion-line { padding: var(--space-1) var(--space-2); font-size: var(--font-sm); }
/* === Photo grid === */
.photo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.4rem;
margin-top: var(--space-3);
}
.photo-grid a { display: block; line-height: 0; }
.photo-grid img {
width: 100%;
height: 140px;
object-fit: cover;
display: block;
background: var(--bg-panel) url("/_/icons/thumb-placeholder.svg") center/2rem no-repeat;
}
/* === Thumbnail listing grid ===
File-listing variant of .photo-grid: responsive tiles that pair a thumbnail
(or a file/folder icon for non-thumbnailable entries) with a truncated
name label beneath. */
.thumb-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: var(--space-3);
margin-top: var(--space-3);
}
.thumb-tile {
display: flex;
flex-direction: column;
gap: var(--space-1);
color: var(--text);
border: var(--border);
background: var(--bg-panel);
padding: var(--space-2);
}
.thumb-tile:hover { background: var(--bg-panel-hover); color: var(--primary-hover); }
.thumb-img {
width: 100%;
height: 150px;
object-fit: cover;
display: block;
background: var(--bg) url("/_/icons/thumb-placeholder.svg") center/2rem no-repeat;
}
/* Wrapper so a video tile can overlay a play badge on its extracted frame. */
.thumb-media { position: relative; display: block; }
.thumb-play {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding-left: 0.15rem;
border-radius: 50%;
background: var(--overlay);
color: var(--text);
pointer-events: none;
}
.thumb-icon {
height: 150px;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: var(--secondary);
}
.thumb-label { font-size: var(--font-sm); }
.empty { padding: var(--space-4); text-align: center; }
/* === Scrollbars === */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #111; }
::-webkit-scrollbar-thumb { background: var(--primary); }
::-webkit-scrollbar-thumb:hover { background: var(--primary-hover); }
/* === Sidebar (right rail) ===
Full-height column in the app-shell that scrolls its own overflow (TOC on
top, page widget(s) below). Collapses out of the flex row when it has no
content so the center column widens. A dashed separator to main mirrors the
left tree rail; widgets within drop their panel outlines (see
.panel-sidebar). */
.sidebar {
width: 14rem;
flex-shrink: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--space-4);
padding: var(--space-4) 0 var(--space-4) var(--space-2);
border-left: var(--border-dashed);
}
aside.sidebar:empty { display: none; }
/* Density modifier for panels in the sidebar (smaller font, tighter padding).
Drops the .panel outline so the rail reads as a clean column separated from
main by the .sidebar dashed border, matching the left tree rail. Inside the
mobile Overlay the widgets render as plain full-width content. */
.panel-sidebar {
padding: var(--space-2) var(--space-3);
font-size: var(--font-sm);
border: none;
}
/* === Table of contents ===
Rendered as a .panel-sidebar block in the right rail (padding + font-size
come from that density class); toc.js always mounts it there, on both
desktop and inside the mobile Overlay. */
.toc ul { list-style: none; margin: 0; padding: 0; }
.toc li { margin: 0.15rem 0; }
.toc a {
color: var(--link);
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.toc a:hover { color: var(--link-hover); }
.toc-h3 { padding-left: 0.8rem; }
.toc-h4 { padding-left: 1.6rem; }
/* === Modal === */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: flex-start;
justify-content: center;
z-index: 1000;
padding: var(--space-4);
}
.modal {
width: 100%;
max-width: 500px;
display: flex;
flex-direction: column;
margin-top: 6rem;
position: relative;
}
.modal.is-dragged { margin: 0; }
/* Modal headers use larger font + bright color + drag handle; everything
else comes from .panel-header. */
.modal .panel-header {
font-size: var(--font-sm);
color: var(--text);
padding: 0.6rem var(--space-4);
margin-bottom: 0;
cursor: move;
user-select: none;
}
/* Modal-scoped input variant: dark bg + slab font + slightly more padding. */
.modal .input {
background: var(--bg);
font-family: "Iosevka Slab", monospace;
padding: 0.4rem 0.6rem;
}
/* === Overlay (full-viewport rail host, mobile) ===
A distinct component from .modal: it hosts a whole rail (the folder tree or
the right rail's TOC/widgets) at full screen with its own scroll and a
top-right close control. overlay.js MOVES the rail node into .overlay-body,
so the rules below strip the rail's column chrome (fixed width, borders, the
mobile display:none) and let it render as plain full-width content. */
.overlay {
position: fixed;
inset: 0;
z-index: 200;
background: var(--bg);
display: flex;
flex-direction: column;
overflow: hidden;
}
.overlay-bar {
display: flex;
justify-content: flex-end;
padding: var(--space-2);
border-bottom: var(--border-dashed);
flex-shrink: 0;
}
.overlay-close { width: 2.5rem; height: 2.5rem; font-size: 1.5rem; }
.overlay-body {
flex: 1;
overflow-y: auto;
padding: var(--space-3) var(--space-4);
-webkit-overflow-scrolling: touch;
}
.overlay-body .tree-scroll,
.overlay-body .sidebar {
display: block;
width: auto;
max-height: none;
border: none;
padding: 0;
overflow: visible;
}
/* The todo widget fills the overlay: drop the rail divider and lift the
internal height cap so the whole file is editable on mobile. */
.overlay-body .todo-rail { border-bottom: none; padding-bottom: 0; }
.overlay-body .todo-rail .todo-rail-view,
.overlay-body .todo-rail .cm-editor { max-height: none; }
/* Right rail keeps its column spacing (TOC above widgets) inside the overlay;
the base .sidebar already sets flex-direction + gap. */
.overlay-body .sidebar { display: flex; }
/* === Tree picker === */
/* Rows are click targets (navigate / toggle), not prose — suppress the
text-selection highlight that double/drag clicks would otherwise leave,
matching the persistent .tree-sidebar rail. */
.tree-picker { max-height: 60vh; overflow-y: auto; user-select: none; }
.tree-row {
display: flex;
align-items: center;
gap: var(--space-1);
padding: 0.25rem var(--space-1);
cursor: pointer;
}
.tree-row:hover, .tree-row.is-selected { background: var(--bg-panel-hover); }
.tree-row.is-selected {
border-left: 3px solid var(--primary);
padding-left: calc(var(--space-2) - 3px);
}
.tree-row.is-disabled { color: var(--text-muted); cursor: default; }
.tree-row.is-disabled:hover { background: none; }
.tree-chevron { text-align: center; flex-shrink: 0; }
.tree-chevron { width: 1.25rem; color: var(--secondary); }
.tree-chevron.is-leaf { visibility: hidden; }
.tree-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Each nesting level gets a vertical guide rail down its left edge so depth
stays scannable in the compact view (cf. VS Code / file-explorer trees). */
.tree-children {
margin-left: var(--space-2);
padding-left: var(--space-2);
border-left: var(--border-guide);
}
.tree-selected-path {
font-size: var(--font-sm);
padding: var(--space-1) 0;
word-break: break-all;
}
/* Current-page row in the navigation rail. Mirrors .tree-row.is-selected so the
two tree surfaces stay visually consistent. */
.tree-row.is-active {
background: var(--bg-panel-hover);
border-left: 3px solid var(--primary);
padding-left: calc(var(--space-2) - 3px);
}
.tree-row.is-active > .tree-name { color: var(--primary-hover); }
/* === Tree sidebar (persistent left navigation rail) ===
Reuses the .tree-row / .tree-children / .tree-name / .tree-chevron modules.
Desktop: a full-height flex column in the app-shell (composes with .col)
holding the todo widget pinned on top and the tree's own scroll region
below. Mobile: not laid out inline — its content is surfaced through the
Overlay via the stacked FABs (see responsive). */
/* No right padding on the aside: the scrolling children below reach the
border-right so their scrollbars sit flush against the divider line (they
pad their own content off the scrollbar instead). */
.tree-sidebar {
width: var(--tree-width);
flex-shrink: 0;
min-height: 0;
padding: var(--space-4) 0 var(--space-4) var(--space-4);
border-right: var(--border-dashed);
}
/* The tree's scroll region: only the tree overflows, the todo widget above
keeps its own internal scroll. Also the node the tree FAB moves into the
Overlay, so font sizing lives here rather than on the aside. */
.tree-scroll {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding-right: var(--space-2);
font-size: var(--font-sm);
/* Rows are click targets (navigate / toggle), not prose — suppress the
text-selection highlight that double/drag clicks would otherwise leave. */
user-select: none;
}
/* === Todo rail (root todo.txt widget) ===
Pinned above the folder tree (todo-rail.js). Hidden via the [hidden]
attribute until GET /_todo confirms the file exists. A long file scrolls
internally rather than pushing the tree off screen; the height cap is
lifted inside the Overlay. */
.todo-rail {
flex-shrink: 0;
padding-bottom: var(--space-3);
border-bottom: var(--border-dashed);
}
.todo-rail-head {
font-size: var(--font-xs);
margin-bottom: var(--space-2);
padding-right: var(--space-2);
}
/* Compact action toolbar in the widget header. Buttons are the app's standard
bracketed .btn/.btn-tool; the row just holds them together and lets them wrap
in the narrow rail rather than overflow. */
.todo-toolbar { flex-wrap: wrap; }
/* Roomier tap targets when the widget is surfaced full-screen on mobile. */
.overlay-body .todo-toolbar .btn {
font-size: var(--font-sm);
padding: var(--space-1) var(--space-2);
}
.todo-unsaved { color: var(--primary-hover); font-size: var(--font-xs); }
/* The scrolling children run edge-to-edge (the aside has no right padding) so
their scrollbars are flush with the divider; padding-right keeps their text
clear of the scrollbar. */
.todo-rail-view {
font-family: "Iosevka Slab", monospace;
font-size: var(--font-xs);
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
max-height: 40vh;
overflow-y: auto;
padding-right: var(--space-2);
cursor: text;
}
.todo-rail .cm-editor { max-height: 40vh; }
.todo-rail .cm-content { padding-right: var(--space-2); }
/* Hanging indent per task. Each todo (one logical line) starts flush-left and
its wrapped continuation lines are indented, so a new task is distinguishable
from a wrapped line even when it has no leading priority/date/x marker. The
indent is character-relative (ch) because it aligns to the monospace text,
not to the layout's spacing scale. */
.todo-rail .cm-line {
padding-left: 2ch;
text-indent: -2ch;
}
.todo-rail .todo-fallback {
width: 100%;
min-height: 10rem;
font-family: "Iosevka Slab", monospace;
font-size: var(--font-xs);
}
/* === Movie info box === */
.movie-info { margin: var(--space-3) 0; }
.movie-info::after { content: ""; display: block; clear: both; }
.movie-info .movie-poster {
float: right;
max-width: 200px;
margin: 0 0 var(--space-3) var(--space-4);
}
.movie-info table { width: auto; }
@media (max-width: 600px) {
.movie-info .movie-poster {
float: none;
display: block;
margin: 0 auto var(--space-3);
}
}
/* === Diary calendar === */
/* Header row: "Chronological" on the left, the [year ▾] dropdown pushed to the
right. Composes with .row (flex + centering); space-between splits the two. */
.diary-cal .panel-header { justify-content: space-between; }
.diary-cal-drop .dropdown-menu { min-width: 0; }
/* Per-month caption above each grid, linking to that month's anchor. */
.diary-cal-month {
font-size: var(--font-sm);
margin: var(--space-3) 0 0.2rem;
}
.diary-cal-month a { color: var(--link); }
.diary-cal-month a:hover { color: var(--link-hover); }
.diary-cal-grid {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
margin-bottom: 0.4rem;
}
.diary-cal-grid th, .diary-cal-grid td {
text-align: center;
padding: 0.1rem 0.15rem;
font-weight: normal;
color: var(--text-muted);
}
.diary-cal-grid td a { color: var(--link); display: block; }
.diary-cal-grid td a:hover { color: var(--link-hover); }
.diary-cal-grid td.cal-empty a { color: var(--text-muted); }
.diary-cal-grid td.cal-empty a:hover { color: var(--link-hover); }
.diary-cal-grid td.cal-today { background: var(--bg-panel); }
.diary-cal-grid td.cal-current,
.diary-cal-grid td.cal-current a { color: var(--primary-hover); }
.btn-block.cal-current { color: var(--primary-hover); }
/* === Fitness dashboard ===
Server-rendered inline SVG charts. Geometry comes precomputed from Go;
colors and strokes are applied here via classes so the inline SVG follows
the theme palette. */
.fitness-chart { padding: var(--space-3); }
.fitness-chart-header { margin-bottom: var(--space-2); }
.fitness-range { width: auto; font-size: var(--font-sm); }
.fitness-empty {
border: var(--border-dashed);
color: var(--text-muted);
text-align: center;
padding: var(--space-5);
}
.fitness-svg { display: block; width: 100%; height: auto; }
.fitness-svg .chart-grid { stroke: var(--bg-panel-hover); }
.fitness-svg .chart-axis { stroke: var(--text-muted); }
.fitness-svg .chart-label { fill: var(--text-muted); font-size: var(--font-xs); }
.fitness-svg .chart-line { fill: none; stroke: var(--link); stroke-width: 1.5; }
.fitness-svg .chart-dot { fill: var(--link); }
.fitness-svg .chart-goal { stroke: var(--primary-hover); stroke-dasharray: 4 3; }
.fitness-svg .chart-goal-label { fill: var(--primary-hover); font-size: var(--font-xs); }
/* === Responsive === */
@media (max-width: 1100px) {
/* Rails are not laid out inline on mobile — their content is surfaced
through the Overlay via the stacked FABs. The <aside> elements stay in
the DOM (their JS renders into them and the Overlay moves them in/out);
we only pull them out of the shell flow here. */
.tree-sidebar, .sidebar { display: none; }
/* Compact three-column header: logo left, search stretches the middle,
actions right. */
header { grid-template-columns: auto 1fr auto; }
.search-form { width: 100%; max-width: none; justify-self: stretch; }
/* Reveal the mobile FAB stack (tree at the bottom, right rail above it).
The search actions dropdown FAB moves to the upper slot too so it never
overlaps the tree FAB. */
button.fab { display: inline-flex; }
.fab.dropdown { bottom: calc(var(--space-4) + 3rem + var(--space-2)); }
}
@media (max-width: 600px) {
header, footer { padding: var(--space-2) var(--space-3); }
main { padding: var(--space-4) var(--space-3); }
.app-name {display: none;}
.editor-cm { min-height: 50vh; }
/* Editing on mobile is full-bleed: drop the main inset so the toolbar and
editor use the entire viewport width. */
body.edit-mode main { padding: 0; }
/* Fingers, not cursors: give every toolbar control a ~44px tap target. */
.editor-toolbar { gap: var(--space-2); padding: var(--space-2); }
.editor-toolbar .btn-tool {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2rem;
min-height: 2rem;
padding: 0 var(--space-1);
}
/* Pin the toolbar above the on-screen keyboard rather than at the top, which
is out of thumb reach while typing. interactive-widget=resizes-content
(layout.html viewport) shrinks the viewport on keyboard open so bottom: 0
sits directly above it. cm-content reserves matching scroll space so the
last lines aren't hidden behind the bar. */
body.edit-mode .editor-toolbar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 50;
border: none;
border-top: var(--border);
padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom));
}
body.edit-mode .cm-content {
padding-bottom: calc(5rem + env(safe-area-inset-bottom));
}
.modal-backdrop { padding: var(--space-2); align-items: flex-start; }
.modal { max-width: none; margin-top: var(--space-4); }
.modal .panel-header { cursor: default; }
/* On mobile, switch .list-item from a table row to a CSS grid so the
meta cell wraps to its own line indented under the name. */
.list-item {
display: grid;
grid-template-columns: 1.25rem 1fr;
gap: 0 var(--space-3);
padding: 0.6rem var(--space-4);
}
.list-item > td { padding: 0; }
.list-item td.icon { grid-row: 1; grid-column: 1; }
.list-item td.name { grid-row: 1; grid-column: 2; }
.list-item td.meta { grid-row: 2; grid-column: 2; text-align: left; }
}