eternalibre-notes/static/style.css

2048 lines
41 KiB
CSS

/* EternaLibre Notes — Apple Elegance meets Classic Desktop GUI */
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Inter", "Segoe UI", Roboto, sans-serif;
background-color: var(--app-bg);
color: var(--app-fg);
transition: background-color 0.15s ease, color 0.15s ease;
}
body {
display: flex;
flex-direction: column;
height: 100dvh;
overflow: hidden;
}
a {
color: var(--ansi-4);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button, input, select, textarea {
font: inherit;
color: inherit;
}
/* Menu Bar */
.menu-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 32px;
padding: 0 12px;
background-color: var(--app-bg);
border-bottom: 1px solid var(--ui-border);
user-select: none;
flex-shrink: 0;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.menu-list {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 4px;
}
.menu-item {
position: relative;
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
}
/* Menus are opened by click (see `openMenu` in app.js), not CSS hover, so
that only one can be open at a time. `.open` both highlights the trigger
and reveals its dropdown. */
.menu-item:hover,
.menu-item:focus,
.menu-item.open {
background-color: var(--ui-hover);
outline: none;
}
.menu-item.open .menu-dropdown {
display: block;
}
.menu-dropdown {
display: none;
position: absolute;
top: calc(100% - 4px);
left: 0;
min-width: 180px;
/* The Theme menu has 40 entries; cap the height so it cannot run off
the bottom of the viewport (body has overflow: hidden). */
max-height: calc(100dvh - 48px);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
background-color: var(--app-bg);
border: 1px solid var(--ui-border);
border-radius: 6px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
list-style: none;
margin: 0;
padding: 12px 6px 6px;
z-index: 100;
}
/* The File menu is short, so let its flyout escape horizontally instead of
letting the generic scroll container clip it. The nested flyout handles its
own vertical scrolling near the bottom edge of the viewport. */
.menu-dropdown.file-menu-dropdown {
max-height: none;
overflow: visible;
}
.menu-dropdown li {
padding: 0;
}
.menu-dropdown button,
.menu-dropdown a {
display: block;
width: 100%;
padding: 6px 10px;
background: none;
border: none;
text-align: left;
color: var(--app-fg);
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.menu-dropdown button:hover,
.menu-dropdown a:hover {
background-color: var(--ui-hover);
text-decoration: none;
}
/* Marks a View menu item whose pane is currently collapsed, so the state is
readable at a glance instead of only via the Hide/Show label. */
.menu-dropdown button.active-view::before {
content: "\2713\00a0";
opacity: 0.7;
}
.menu-dropdown hr {
border: none;
border-top: 1px solid var(--ui-border);
margin: 6px 0;
}
.menu-submenu-trigger {
display: flex !important;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.menu-submenu-trigger:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.menu-dropdown .menu-submenu-trigger:disabled:hover {
background: none;
}
.submenu-chevron {
font-size: 18px;
line-height: 1;
opacity: 0.65;
}
.menu-submenu {
position: relative;
}
.menu-submenu-list {
display: none;
position: absolute;
top: 0;
left: calc(100% - 2px);
z-index: 101;
min-width: 180px;
/* This flyout holds user-supplied folder names (up to 100 characters) and
`body` has `overflow: hidden`, so without a cap a long name widens the
list past the viewport and that destination becomes unreachable. */
max-width: calc(100vw - 24px);
max-height: calc(100dvh - 48px);
overflow-y: auto;
overflow-wrap: anywhere;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
background-color: var(--app-bg);
border: 1px solid var(--ui-border);
border-radius: 6px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
list-style: none;
margin: 0;
padding: 6px;
}
.menu-submenu-list.open {
display: block;
}
.menu-dropdown .menu-submenu-list a {
padding-left: 10px;
font-size: 12px;
}
.app-title {
font-size: 12px;
font-weight: 600;
opacity: 0.7;
}
/* Toolbar */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 12px;
background-color: var(--app-bg);
border-bottom: 1px solid var(--ui-border);
gap: 12px;
flex-shrink: 0;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.toolbar-group {
display: flex;
align-items: center;
gap: 8px;
}
.btn {
padding: 5px 12px;
background-color: var(--ui-hover);
border: 1px solid var(--ui-border);
border-radius: 6px;
color: var(--app-fg);
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.btn:hover {
background-color: var(--ansi-12);
border-color: var(--app-fg);
color: var(--ansi-0);
}
.btn-primary {
background-color: var(--btn-primary-bg);
color: var(--btn-primary-fg);
border-color: var(--btn-primary-bg);
}
.btn-primary:hover {
background-color: var(--btn-primary-bg);
box-shadow: inset 0 0 0 999px rgba(255, 255, 255, 0.12);
}
.btn-danger {
background-color: var(--btn-danger-bg);
color: var(--btn-danger-fg);
border-color: var(--btn-danger-bg);
}
.btn-danger:hover {
background-color: var(--btn-danger-bg);
box-shadow: inset 0 0 0 999px rgba(255, 255, 255, 0.12);
}
.search-form input,
.theme-form select {
padding: 5px 10px;
background-color: var(--app-bg);
border: 1px solid var(--ui-border);
border-radius: 6px;
font-size: 13px;
outline: none;
}
.search-form input:focus,
.theme-form select:focus {
border-color: var(--ansi-4);
box-shadow: 0 0 0 2px var(--ui-hover);
}
.theme-form {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
/* Flash Messages */
.flash-message {
padding: 8px 12px;
background-color: var(--ansi-1);
color: var(--app-bg);
font-size: 13px;
font-weight: 500;
flex-shrink: 0;
}
/* App Body */
.app-body {
display: grid;
grid-template-columns: 280px 1fr 1fr;
flex: 1;
min-height: 0;
overflow: hidden;
}
/* The welcome state renders no preview pane, so the editor takes the full
remaining width instead of sitting in a half-width track. */
.app-body.no-preview {
grid-template-columns: 280px 1fr;
}
/* When a pane is collapsed via the View menu, the remaining pane reclaims the
space. Each state sets its own column count so a lone pane always spans the
full remaining width instead of sitting in a half-width track.
Scoped to desktop only: on mobile the tab bar already decides which single
pane is shown, and these rules would outrank `.app-body > .mobile-active`
on specificity, leaving the Preview tab permanently empty. */
@media (min-width: 901px) {
.app-body.hide-preview,
.app-body.hide-editor,
.app-body.no-preview {
grid-template-columns: 280px 1fr;
}
.app-body.hide-editor.hide-preview,
.app-body.no-preview.hide-editor {
grid-template-columns: 280px;
}
}
@media (min-width: 601px) and (max-width: 900px) {
.app-body.hide-preview,
.app-body.hide-editor,
.app-body.no-preview {
grid-template-columns: 240px 1fr;
}
.app-body.hide-editor.hide-preview,
.app-body.no-preview.hide-editor {
grid-template-columns: 240px;
}
}
@media (min-width: 601px) {
.app-body.hide-editor .editor-pane {
display: none;
}
.app-body.hide-preview .preview-pane {
display: none;
}
/* Desktop's entry point for re-filing a note is File > Move to Folder…;
the bar is the mobile one. Paired with the `max-width: 600px` rules for
the mobile flyout below — keep the two breakpoints in sync.
Note the bar must NOT also carry `.desktop-only` in the markup. That
class hides things at `max-width: 600px`, so the two rules together would
hide it at every width and mobile would lose re-filing entirely.
`move_entry_points_are_not_desktop_only` pins this. */
.move-folder-bar {
display: none;
}
}
/* Sidebar */
.sidebar {
background-color: var(--app-bg);
border-right: 1px solid var(--ui-border);
overflow-y: auto;
display: flex;
flex-direction: column;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.sidebar-header {
padding: 12px 16px;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
opacity: 0.6;
border-bottom: 1px solid var(--ui-border);
}
.note-list {
list-style: none;
margin: 0;
padding: 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
.note-item {
border-radius: 8px;
transition: background-color 0.15s ease;
/* Suppress iOS Safari's long-press callout (Copy / Look Up / Translate).
On a touch screen the re-file gesture IS a long press, and that menu
competes with it for the same gesture. `user-select` is deliberately left
alone: preview text stays selectable and copyable.
Not `touch-action: none`, which is the usual advice for drag — on a
scrollable sidebar it would break scrolling the note list. */
-webkit-touch-callout: none;
}
.note-item:hover {
background-color: var(--ui-hover);
}
.note-item.active {
background-color: var(--ui-hover);
}
.note-item a {
display: block;
padding: 10px 12px;
color: inherit;
}
.note-item a:hover {
text-decoration: none;
}
.note-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.note-meta {
font-size: 11px;
opacity: 0.6;
margin-bottom: 4px;
}
.note-preview {
font-size: 12px;
opacity: 0.7;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.note-item.empty {
padding: 20px;
text-align: center;
opacity: 0.5;
font-size: 13px;
}
/* New Folder Form */
.new-folder-form {
display: flex;
gap: 6px;
padding: 8px 12px;
border-bottom: 1px solid var(--ui-border);
}
.new-folder-form input {
flex: 1;
min-width: 0;
padding: 6px 10px;
background-color: var(--app-bg);
border: 1px solid var(--ui-border);
border-radius: 6px;
color: var(--app-fg);
font-size: 13px;
outline: none;
}
.new-folder-form input:focus {
border-color: var(--ansi-4);
}
.new-folder-form button {
padding: 6px 10px;
background-color: var(--ui-hover);
border: 1px solid var(--ui-border);
border-radius: 6px;
color: var(--app-fg);
font-size: 12px;
cursor: pointer;
white-space: nowrap;
transition: background-color 0.15s ease;
}
.new-folder-form button:hover {
background-color: var(--ansi-12);
color: var(--ansi-0);
}
/* Folder Navigation */
/* Sticky so the folders stay visible, and droppable, however long the note
list grows — `.sidebar` is the scroll container and the nav sits above the
list inside it. Nothing auto-scrolls a nested overflow container mid-drag,
so without this the drag targets simply scroll out of reach.
Needs its own background because it now overlays the scrolling note items.
The z-index is scoped to the sidebar and safe under the stacking-context
rule in AGENTS.md §7: no menu dropdowns live in here to trap. */
.folder-nav-container {
position: sticky;
top: 0;
z-index: 2;
background-color: var(--app-bg);
/* `.sidebar` is a flex column, so without this the nav can be squeezed by
a long note list — and a squeezed sticky bar clips its own folder names,
which are the drop target labels. */
flex-shrink: 0;
}
.folder-nav {
list-style: none;
margin: 0;
padding: 8px;
display: flex;
flex-direction: column;
gap: 2px;
border-bottom: 1px solid var(--ui-border);
}
/* Pull-tab toggle — hidden on desktop, shown on mobile */
.folder-nav-toggle {
display: none;
width: 100%;
min-height: 44px;
align-items: center;
justify-content: center;
background: var(--app-bg);
border: none;
border-bottom: 1px solid var(--ui-border);
color: var(--app-fg);
opacity: 0.6;
cursor: pointer;
flex-shrink: 0;
transition: opacity 0.15s ease;
}
.folder-nav-toggle:hover {
opacity: 0.8;
}
.folder-nav-toggle-icon {
font-size: 18px;
line-height: 1;
transition: transform 0.15s ease;
}
/* Collapsed state — hides the folder list but keeps the toggle visible */
.folder-nav-container.collapsed .folder-nav {
display: none;
}
.folder-nav-container.collapsed .folder-nav-toggle-icon {
transform: rotate(180deg);
}
.nav-item a {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
border-radius: 6px;
color: var(--app-fg);
font-size: 13px;
transition: background-color 0.15s ease;
}
.nav-item a:hover {
background-color: var(--ui-hover);
text-decoration: none;
}
.nav-item.active a {
background-color: var(--ui-hover);
font-weight: 600;
}
.nav-count {
font-size: 11px;
opacity: 0.6;
background-color: var(--ui-hover);
padding: 2px 6px;
border-radius: 10px;
}
.nav-item.active .nav-count {
opacity: 0.8;
}
/* Folder Group Headers */
.folder-header {
padding: 8px 12px 4px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
opacity: 0.5;
}
/* Note Folder Display */
/* Sits inside .move-folder-bar, which owns the border. */
.note-folder {
font-size: 12px;
opacity: 0.6;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Move-to-folder control. A sibling of #note-form rather than a child: a form
inside a form is invalid and browsers discard the inner one. */
.move-folder-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 8px 16px;
border-top: 1px solid var(--ui-border);
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.move-folder-form {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.move-folder-form select {
max-width: 160px;
padding: 6px 10px;
background-color: var(--app-bg);
border: 1px solid var(--ui-border);
border-radius: 6px;
color: var(--app-fg);
font-size: 13px;
font-family: inherit;
outline: none;
}
.move-folder-form select:focus {
border-color: var(--ansi-4);
}
.move-label {
font-size: 12px;
opacity: 0.6;
white-space: nowrap;
}
/* Drag-and-drop re-filing: the drag source is the sidebar note item, the drop
targets are the folder nav items.
No `opacity` here, deliberately. Any opacity below 1 promotes the row to its
own compositing layer, and when iOS rasterizes that layer to build the drag
image the backdrop is not composited in, so a transparent-background row
comes out solid black. That reported as the whole row turning black while
held for a long press, and it was far more obvious against light themes —
the artifact is a fixed dark region whose visibility merely tracks contrast
with the sidebar behind it.
The background must be *opaque*, and this is the part that took two rounds
to get right. `--ui-hover` is `{fg}1a` — about 10% alpha — so it looks like
a subtle tint when the browser composites it over the sidebar, but iOS
captures the row with that transparency and paints the see-through area
black. `.note-item.active` uses `--ui-hover` and is fine only because it
is never dragged. So the dragging state uses `--app-bg` directly: the same
colour the sidebar already paints, which reads as "this row is lifted out
of the list" and gives the drag image an opaque backdrop to rasterize.
`transition: none` because `.note-item` transitions `background-color` over
150ms: an animating background can transiently promote a layer, so a drag
image captured mid-transition would go black all over again. It also makes
the pickup instant, which suits a drag cue better than a fade-in.
Same `--ansi-4` as the drop targets, so source and target read as one
system. Inset rather than outline, since outline draws outside the border
box and would collide with the sidebar's own borders. */
.note-item.dragging {
background-color: var(--app-bg);
box-shadow: inset 0 0 0 1px var(--ansi-4);
transition: none;
}
.nav-item.drop-target > a {
background-color: var(--ui-hover);
outline: 1px dashed var(--ansi-4);
outline-offset: -1px;
}
/* Editor Pane */
.editor-pane {
display: flex;
flex-direction: column;
background-color: var(--app-bg);
border-right: 1px solid var(--ui-border);
overflow: hidden;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
/* `flex: 1` + `min-height: 0` rather than `height: 100%`, because the pane
also holds `.move-folder-bar`. A percentage height on a flex item would
resolve against the pane's full height and push the bar off-screen; these
let the form take the leftover space and keep the textarea's own `flex: 1`
growth meaningful. `min-height: 0` is what stops the textarea's intrinsic
height from refusing to shrink below its content. The bar is
`display: none` above 600px, so this only matters on mobile. */
.note-form {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.note-title-input {
padding: 14px 16px;
font-size: 20px;
font-weight: 700;
background: transparent;
border: none;
border-bottom: 1px solid var(--ui-border);
outline: none;
color: var(--app-fg);
}
.note-title-input:focus {
border-bottom-color: var(--ansi-4);
}
.note-content-input {
flex: 1;
width: 100%;
padding: 16px;
font-family: "SF Mono", "Monaco", "Inconsolata", "Fira Code", monospace;
font-size: 14px;
line-height: 1.6;
background: transparent;
border: none;
outline: none;
resize: none;
color: var(--app-fg);
tab-size: 2;
}
.form-actions {
padding: 10px 16px;
border-top: 1px solid var(--ui-border);
display: flex;
justify-content: flex-end;
}
.welcome-state {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: clamp(24px, 5vw, 52px);
scrollbar-gutter: stable;
}
.welcome-heading {
max-width: 760px;
margin: 0 auto 28px;
}
.welcome-eyebrow {
margin: 0 0 10px;
color: var(--ansi-7);
font-size: 11px;
font-weight: 700;
letter-spacing: 1.2px;
}
.welcome-heading h1 {
margin: 0;
font-size: clamp(24px, 3vw, 36px);
line-height: 1.15;
letter-spacing: -0.6px;
text-wrap: balance;
}
.welcome-tagline {
margin: 12px 0 0;
max-width: 560px;
font-size: 15px;
line-height: 1.6;
opacity: 0.72;
}
.welcome-columns {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 16px;
max-width: 760px;
margin: 0 auto;
}
.welcome-card {
min-width: 0;
padding: 18px;
border: 1px solid var(--ui-border);
border-radius: 10px;
background: color-mix(in srgb, var(--app-fg) 3%, var(--app-bg));
box-shadow: 0 4px 18px color-mix(in srgb, var(--app-fg) 5%, transparent);
}
.welcome-card h2 {
margin: 0 0 14px;
font-size: 14px;
font-weight: 650;
}
.welcome-muted {
margin: 0;
font-size: 13px;
line-height: 1.55;
opacity: 0.68;
}
.welcome-recent-list {
list-style: none;
margin: 0;
padding: 0;
}
.welcome-recent-list li + li {
border-top: 1px solid var(--ui-border);
}
.welcome-recent-list a {
display: flex;
flex-direction: column;
gap: 3px;
padding: 10px 8px;
margin: 0 -8px;
border-radius: 6px;
color: var(--app-fg);
transition: background-color 0.15s ease, color 0.15s ease;
}
.welcome-recent-list a:hover {
background-color: var(--ui-hover);
text-decoration: none;
}
.welcome-note-title {
overflow-wrap: anywhere;
font-size: 14px;
font-weight: 600;
}
.welcome-note-meta,
.welcome-note-preview,
.weather-location,
.weather-privacy {
font-size: 12px;
line-height: 1.45;
opacity: 0.65;
}
.welcome-note-preview {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.weather-summary {
margin: 0 0 6px;
font-size: 20px;
font-weight: 600;
line-height: 1.35;
overflow-wrap: anywhere;
}
.weather-location {
margin: 0 0 18px;
}
.weather-location-form {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
margin-top: 14px;
}
.weather-location-form label {
font-size: 12px;
font-weight: 600;
}
.weather-location-form input {
width: 100%;
min-width: 0;
min-height: 40px;
padding: 8px 10px;
border: 1px solid var(--ui-border);
border-radius: 6px;
background: var(--app-bg);
outline: none;
}
.weather-location-form input:focus {
border-color: var(--ansi-4);
}
.weather-location-form .btn {
min-height: 40px;
}
.weather-privacy {
margin: 2px 0 0;
}
.weather-table-wrapper {
margin: 0 0 8px;
overflow-x: auto;
border: 1px solid var(--ui-border);
border-radius: 8px;
}
.weather-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
line-height: 1.45;
}
.weather-caption {
padding: 8px 10px;
font-weight: 600;
text-align: left;
}
.weather-table th,
.weather-table td {
padding: 8px 10px;
border-top: 1px solid var(--ui-border);
text-align: left;
white-space: nowrap;
}
.weather-table thead th {
border-top: none;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
opacity: 0.65;
}
.weather-now-row th,
.weather-now-row td {
background: color-mix(in srgb, var(--ansi-4) 10%, transparent);
}
.weather-na {
opacity: 0.45;
}
.weather-details {
margin: 0 0 12px;
font-size: 12px;
opacity: 0.7;
}
.weather-units {
display: flex;
align-items: center;
gap: 14px;
margin: 0;
padding: 0;
border: none;
}
.weather-units legend {
padding: 0;
font-size: 12px;
font-weight: 600;
}
.weather-units label {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 400;
min-height: 40px;
}
.weather-units input {
width: auto;
min-height: auto;
accent-color: var(--ansi-4);
}
.welcome-quotes {
margin-top: 16px;
max-width: 560px;
}
.welcome-quote {
margin: 0;
padding: 12px 16px;
border-left: 4px solid var(--ansi-4);
background: color-mix(in srgb, var(--app-fg) 4%, var(--app-bg));
border-radius: 0 8px 8px 0;
}
.welcome-quote-text {
margin: 0 0 6px;
font-size: 16px;
font-style: italic;
line-height: 1.5;
overflow-wrap: anywhere;
}
.welcome-quote footer {
font-size: 13px;
opacity: 0.7;
}
.welcome-quote-controls {
display: flex;
gap: 8px;
margin-top: 10px;
}
.btn-small {
min-height: 36px;
padding: 6px 12px;
font-size: 12px;
}
.quotes-card {
max-width: 760px;
margin: 16px auto 0;
}
.quote-form {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
margin-top: 12px;
}
.quote-form label {
font-size: 12px;
font-weight: 600;
}
.quote-form textarea,
.quote-form input {
width: 100%;
min-width: 0;
min-height: 40px;
padding: 8px 10px;
border: 1px solid var(--ui-border);
border-radius: 6px;
background: var(--app-bg);
outline: none;
font-size: 14px;
}
.quote-form textarea {
min-height: 76px;
resize: vertical;
line-height: 1.5;
}
.quote-form textarea:focus,
.quote-form input:focus {
border-color: var(--ansi-4);
}
.quote-list {
list-style: none;
margin: 14px 0 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.quote-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--ui-border);
border-radius: 8px;
}
.quote-item-body {
margin: 0;
flex: 1;
min-width: 0;
}
.quote-item-body p {
margin: 0 0 4px;
font-size: 14px;
font-style: italic;
line-height: 1.5;
overflow-wrap: anywhere;
}
.quote-item-body footer {
font-size: 12px;
opacity: 0.65;
}
.quote-delete-form {
flex-shrink: 0;
}
/* Mobile-only menu entries, hidden on desktop. Inverse of `.desktop-only`,
which hides on mobile. Keep the two breakpoints in sync. */
.mobile-only {
display: none;
}
/* Dedicated weather page. Separate document like `/about`: same menu bar and
status bar, own main column, own back link. */
.weather-page {
display: flex;
flex-direction: column;
height: 100dvh;
}
.weather-page-main {
flex: 1;
min-height: 0;
overflow-y: auto;
max-width: 720px;
width: 100%;
margin: 0 auto;
padding: 32px 24px;
}
.weather-page-main h1 {
margin: 0 0 16px;
font-size: 26px;
line-height: 1.2;
text-wrap: balance;
}
.weather-page-main .weather-card {
margin-bottom: 20px;
}
.weather-page .back-link {
display: inline-block;
padding: 8px 14px;
border: 1px solid var(--ui-border);
border-radius: 6px;
font-size: 14px;
font-weight: 500;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.weather-page .back-link:hover {
background-color: var(--ui-hover);
text-decoration: none;
}
/* Simplified welcome page backing the mobile-only `View → Welcome` entry.
Same separate-document pattern as `/weather`: header, quote rotator, and
recent notes only — no weather card, no quotes manager. */
.welcome-page {
display: flex;
flex-direction: column;
height: 100dvh;
}
.welcome-page-main {
flex: 1;
min-height: 0;
overflow-y: auto;
}
.welcome-page-main .welcome-heading {
max-width: 760px;
}
.welcome-page-main .recent-card {
max-width: 760px;
margin: 0 auto 20px;
}
.welcome-page-main .back-link {
display: block;
max-width: 760px;
margin: 0 auto;
padding: 8px 14px;
border: 1px solid var(--ui-border);
border-radius: 6px;
font-size: 14px;
font-weight: 500;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.welcome-page-main .back-link:hover {
background-color: var(--ui-hover);
text-decoration: none;
}
/* Preview Pane */
.preview-pane {
display: flex;
flex-direction: column;
background-color: var(--app-bg);
overflow-y: auto;
transition: background-color 0.15s ease;
}
.markdown-body {
flex: 1;
padding: 20px 24px;
font-size: 15px;
line-height: 1.7;
max-width: 100%;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 24px;
margin-bottom: 12px;
font-weight: 700;
line-height: 1.3;
}
.markdown-body h1 { font-size: 28px; border-bottom: 1px solid var(--ui-border); padding-bottom: 8px; }
.markdown-body h2 { font-size: 22px; border-bottom: 1px solid var(--ui-border); padding-bottom: 6px; }
.markdown-body h3 { font-size: 18px; }
.markdown-body h4 { font-size: 16px; }
.markdown-body p {
margin: 0 0 14px;
}
.markdown-body ul,
.markdown-body ol {
margin: 0 0 14px;
padding-left: 24px;
}
.markdown-body li {
margin-bottom: 4px;
}
.markdown-body blockquote {
margin: 0 0 14px;
padding: 8px 16px;
border-left: 4px solid var(--ansi-4);
background-color: var(--ui-hover);
border-radius: 0 6px 6px 0;
}
.markdown-body code {
font-family: "SF Mono", "Monaco", "Inconsolata", "Fira Code", monospace;
font-size: 13px;
padding: 2px 6px;
background-color: var(--ui-hover);
border-radius: 4px;
}
.markdown-body pre {
padding: 14px;
background-color: var(--ansi-0);
border-radius: 8px;
overflow-x: auto;
margin: 0 0 14px;
}
.markdown-body pre code {
background: transparent;
padding: 0;
}
.markdown-body table {
width: 100%;
border-collapse: collapse;
margin-bottom: 14px;
}
.markdown-body th,
.markdown-body td {
padding: 8px 12px;
border: 1px solid var(--ui-border);
text-align: left;
}
.markdown-body th {
background-color: var(--ui-hover);
font-weight: 600;
}
.markdown-body hr {
border: none;
border-top: 1px solid var(--ui-border);
margin: 20px 0;
}
.markdown-body input[type="checkbox"] {
margin-right: 8px;
}
/* Status Bar */
.status-bar {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 16px;
height: 24px;
padding: 0 12px;
background-color: var(--app-bg);
border-top: 1px solid var(--ui-border);
font-size: 11px;
opacity: 0.8;
flex-shrink: 0;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
/* Auth Pages (/setup, /login) */
.auth-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100dvh;
padding: 24px 16px;
overflow-y: auto;
}
.auth-card {
width: 100%;
max-width: 420px;
padding: 32px 28px;
background-color: var(--app-bg);
border: 1px solid var(--ui-border);
border-radius: 12px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
}
.auth-card h1 {
margin: 0 0 6px;
font-size: 22px;
font-weight: 700;
}
.auth-subtitle {
margin: 0 0 20px;
font-size: 14px;
line-height: 1.5;
opacity: 0.75;
}
.auth-error {
margin-bottom: 16px;
padding: 10px 12px;
border: 1px solid var(--ansi-1);
border-radius: 6px;
background-color: color-mix(in srgb, var(--ansi-1) 12%, transparent);
color: var(--ansi-9);
font-size: 13px;
line-height: 1.4;
}
.auth-qr {
display: flex;
justify-content: center;
margin-bottom: 18px;
padding: 12px;
background-color: #ffffff;
border-radius: 8px;
}
.auth-qr svg {
display: block;
width: 100%;
max-width: 240px;
height: auto;
}
.auth-manual {
margin-bottom: 16px;
text-align: center;
}
.auth-manual p {
margin: 0 0 6px;
font-size: 12px;
opacity: 0.7;
}
.auth-secret {
display: block;
padding: 8px 10px;
background-color: var(--ui-hover);
border: 1px solid var(--ui-border);
border-radius: 6px;
font-family: "SF Mono", "Monaco", "Inconsolata", "Fira Code", monospace;
font-size: 13px;
word-break: break-all;
user-select: all;
}
.auth-note {
margin: 0 0 18px;
font-size: 12px;
line-height: 1.5;
opacity: 0.65;
}
.auth-note code {
padding: 1px 5px;
background-color: var(--ui-hover);
border-radius: 4px;
font-size: 11px;
}
.auth-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.auth-form label {
font-size: 13px;
font-weight: 500;
}
.auth-input {
width: 100%;
min-height: 52px;
padding: 12px 14px;
background-color: var(--app-bg);
border: 1px solid var(--ui-border);
border-radius: 8px;
color: var(--app-fg);
font-family: "SF Mono", "Monaco", "Inconsolata", "Fira Code", monospace;
font-size: 24px;
letter-spacing: 0.35em;
text-align: center;
outline: none;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.auth-input:focus {
border-color: var(--ansi-4);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ansi-4) 25%, transparent);
}
.auth-form .btn {
min-height: 44px;
margin-top: 4px;
}
.auth-footer {
margin: 22px 0 0;
text-align: center;
font-size: 11px;
opacity: 0.5;
}
/* About Page */
.about-page {
display: flex;
flex-direction: column;
height: 100dvh;
}
.about-page main {
flex: 1;
padding: 40px;
overflow-y: auto;
}
/* The about page was the one screen in the app with no typography rules at
all, so it rendered at UA defaults: a 2em heading with 0.67em margins and
a 1.9 line-height feature list. That is what made it read as a desktop
page on a phone. */
.about-page main > h1 {
font-size: 26px;
line-height: 1.2;
margin: 0 0 4px;
/* Balances a wrapped title instead of leaving a ragged last line. */
text-wrap: balance;
}
.about-page main > p {
line-height: 1.6;
}
.about-page main > h2 {
font-size: 15px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
opacity: 0.6;
margin: 28px 0 10px;
}
/* Outbound links. Kept as a list rather than prose so each destination reads
as its own target, and so the mobile 44px touch-target rule can apply to
the whole row. */
.about-links {
list-style: none;
margin: 0 0 20px;
padding: 0;
}
.about-links li {
margin: 0;
}
.about-links a {
display: block;
padding: 8px 12px;
margin: 0 -12px;
border-radius: 6px;
font-size: 15px;
transition: background-color 0.15s ease;
}
.about-links a:hover {
background-color: var(--ui-hover);
text-decoration: none;
}
/* Sits last and stays muted; it is a colophon, not a call to action. */
.about-page .about-colophon {
font-size: 13px;
opacity: 0.7;
margin: 28px 0 20px;
}
.about-page .tagline {
font-size: 18px;
font-weight: 600;
color: var(--ansi-4);
margin-top: 0;
}
.about-features {
margin: 0 0 20px;
padding-left: 22px;
line-height: 1.9;
}
/* "Back to notes" is the only way off this screen, so it gets a real target
rather than bare link text. */
.about-page .back-link {
display: inline-block;
padding: 8px 14px;
border: 1px solid var(--ui-border);
border-radius: 6px;
font-size: 14px;
font-weight: 500;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.about-page .back-link:hover {
background-color: var(--ui-hover);
text-decoration: none;
}
/* Mobile Tab Bar — hidden on desktop */
.tab-bar {
display: none;
}
/* Responsive */
@media (max-width: 900px) {
.app-body {
grid-template-columns: 240px 1fr 1fr;
}
}
/* Mobile: <= 600px (covers iPhone 11 at 390px CSS width) */
@media (max-width: 600px) {
html, body {
height: 100%;
overscroll-behavior: none;
}
body {
height: 100dvh;
}
/* Desktop-only affordances, hidden once the tab bar takes over.
Paired with the `min-width: 601px` rules for the pane toggles above —
keep the two breakpoints in sync. */
.desktop-only {
display: none;
}
/* Menu bar wraps to a second row.
NOTE: do not use `overflow: auto` here — it would create a new stacking
context and trap the absolutely-positioned `.menu-dropdown` underneath
the toolbar and content panes. */
.menu-bar {
height: auto;
min-height: 40px;
padding: 0 8px;
flex-shrink: 0;
}
.menu-list {
flex-wrap: wrap;
overflow: visible;
}
.menu-item {
padding: 8px 10px;
min-height: 40px;
display: flex;
align-items: center;
white-space: nowrap;
font-size: 14px;
}
/* Constrain the dropdown to the viewport width and a conservative height
on phones. Safe to use `overflow` here: it only clips the dropdown's own
contents, it does not trap the dropdown inside an ancestor's context. */
.menu-dropdown {
max-width: calc(100vw - 16px);
max-height: 55dvh;
}
.menu-submenu-list {
width: calc(100vw - 205px);
min-width: 120px;
max-width: 180px;
max-height: calc(100dvh - 56px);
}
.app-title {
display: none;
}
/* Toolbar wraps instead of stretching off-screen */
.toolbar {
flex-wrap: wrap;
height: auto;
padding: 8px;
gap: 8px;
flex-shrink: 0;
}
.toolbar-group {
flex: 1 1 auto;
min-width: 0;
}
.search-form {
flex: 1 1 100%;
min-width: 0;
display: flex;
}
.search-form input {
flex: 1;
width: 100%;
min-width: 0;
}
.theme-form {
flex: 1 1 auto;
min-width: 0;
}
.theme-form select {
flex: 1;
min-width: 0;
}
/* Apple HIG minimum 44px touch targets */
.btn {
min-height: 44px;
padding: 10px 14px;
font-size: 15px;
flex-shrink: 0;
}
.search-form input,
.theme-form select {
min-height: 44px;
padding: 10px 12px;
font-size: 16px; /* prevents iOS zoom-on-focus */
}
/* Tab bar replaces the 3-column layout on mobile */
.tab-bar {
display: flex;
flex-shrink: 0;
background-color: var(--app-bg);
border-bottom: 1px solid var(--ui-border);
padding: 4px 8px;
gap: 4px;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.tab-button {
flex: 1;
min-height: 48px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
padding: 6px 4px;
background: none;
border: none;
border-radius: 8px;
color: var(--app-fg);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s ease, color 0.15s ease;
}
.tab-button.active {
background-color: var(--ui-hover);
}
.tab-icon {
font-size: 18px;
line-height: 1;
}
/* Single-pane view: only the active tab's pane is displayed */
.app-body {
display: block;
flex: 1;
min-height: 0;
overflow: hidden;
}
.app-body > * {
display: none;
height: 100%;
border-right: none;
border-bottom: none;
-webkit-overflow-scrolling: touch;
}
.app-body > .mobile-active {
display: flex;
flex-direction: column;
}
/* Sidebar fills the pane on mobile */
.sidebar-header {
padding: 12px 16px;
}
.new-folder-form {
padding: 8px;
}
.new-folder-form input,
.new-folder-form button {
min-height: 44px;
font-size: 16px; /* prevents iOS zoom-on-focus */
}
/* Drag-and-drop never fires on a touch screen, so the move control in the
editor pane is the only way to re-file a note on a phone. It therefore
has to clear the same 44px target the rest of the mobile UI enforces. */
.move-folder-bar {
flex-wrap: wrap;
gap: 8px;
}
.move-folder-form {
flex: 1;
min-width: 0;
}
/* Open the flyout downward rather than to the right. Beside the trigger
there is no room on a phone: the File dropdown already sits near the left
edge and `body` has `overflow: hidden`, so a rightward flyout runs off
screen. This is positioning on the dropdown itself, which is safe under
the stacking-context rule in AGENTS.md §7 — the rule is about ancestors. */
.menu-submenu-list {
left: 0;
top: 100%;
max-width: calc(100vw - 24px);
}
.move-folder-form select {
flex: 1;
min-width: 0;
max-width: none;
min-height: 44px;
font-size: 16px; /* prevents iOS zoom-on-focus */
}
.move-folder-form button {
min-height: 44px;
}
.folder-nav-toggle {
display: flex;
}
/* Independent scrolling: folder nav and note list each scroll within
their own area. The sidebar itself does not scroll on mobile. The
pull-tab button stays visible above the scrolling folder list. */
.sidebar {
overflow: hidden;
}
.folder-nav-container {
position: static;
display: flex;
flex-direction: column;
max-height: 40%;
flex-shrink: 0;
}
.folder-nav {
overflow-y: auto;
flex: 1;
min-height: 0;
-webkit-overflow-scrolling: touch;
padding: 8px;
}
.nav-item a {
min-height: 44px;
padding: 10px 12px;
}
.note-list {
overflow-y: auto;
flex: 1;
min-height: 0;
-webkit-overflow-scrolling: touch;
padding: 8px;
}
.note-item a {
padding: 14px 12px;
}
/* Editor fills the pane on mobile */
.note-title-input {
font-size: 18px;
padding: 14px 16px;
flex-shrink: 0;
}
.note-content-input {
font-size: 16px; /* prevents iOS zoom-on-focus */
padding: 16px;
}
.form-actions {
padding: 12px 16px;
flex-shrink: 0;
}
.form-actions .btn {
width: 100%;
}
/* Preview fills the pane on mobile */
.markdown-body {
padding: 20px 16px;
font-size: 16px;
}
/* Status bar stays compact */
.status-bar {
height: 28px;
flex-shrink: 0;
gap: 10px;
font-size: 11px;
overflow-x: auto;
scrollbar-width: none;
}
.status-bar::-webkit-scrollbar {
display: none;
}
/* Prevent horizontal page scroll */
.flash-message {
flex-shrink: 0;
}
/* About page. Without these it keeps the desktop reading measure: a 26px
heading, an 18px tagline, and a 1.9 line-height list, all sized for a
wide column rather than a phone. */
.about-page main {
padding: 20px 16px;
}
.about-page main > h1 {
font-size: 22px;
}
.about-page .tagline {
font-size: 16px;
}
.about-features {
padding-left: 20px;
line-height: 1.6;
}
/* Apple HIG minimum touch target, matching `.btn` above. The negative
margin is cancelled out so the hover background still bleeds to the
padding edge without pushing the row off the column. */
.about-links a {
min-height: 44px;
padding: 12px;
margin: 0 -4px;
font-size: 16px; /* prevents iOS zoom-on-focus */
}
.about-page main > h2 {
margin: 24px 0 8px;
}
.about-page .about-colophon {
font-size: 13px;
margin: 24px 0 20px;
}
/* Apple HIG minimum touch target, matching `.btn` above. */
.about-page .back-link {
display: block;
min-height: 44px;
padding: 12px 16px;
text-align: center;
font-size: 16px;
}
.welcome-state {
padding: 24px 18px;
}
.welcome-heading {
margin-bottom: 20px;
}
.welcome-heading h1 {
font-size: 27px;
}
.welcome-columns {
grid-template-columns: minmax(0, 1fr);
gap: 12px;
}
.welcome-card {
padding: 16px;
}
.weather-location-form .btn {
width: 100%;
min-height: 44px;
}
/* Mobile-only View entries. Shown here; `.mobile-only` is `display: none`
at wider widths so desktop keeps the full dashboard instead. */
.mobile-only {
display: list-item;
}
.weather-table {
font-size: 12px;
}
.weather-table th,
.weather-table td {
padding: 10px 8px;
}
.welcome-quote-controls .btn-small {
min-height: 44px;
flex: 1;
}
.quote-item {
flex-direction: column;
}
.quote-delete-form .btn {
width: 100%;
min-height: 44px;
}
.weather-page-main {
padding: 20px 16px;
}
.weather-page-main h1 {
font-size: 22px;
}
.weather-page .back-link {
display: block;
min-height: 44px;
padding: 12px 16px;
text-align: center;
font-size: 16px;
}
.welcome-page-main .back-link {
min-height: 44px;
padding: 12px 16px;
text-align: center;
font-size: 16px;
}
}
@media (min-width: 901px) {
.app-body.hide-preview .welcome-columns {
grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr);
}
}