With a hundred notes in it the list grew to its full height, the page scrolled instead of the list, and the editor — a flex sibling stretching to the row — became as tall as every note put together: 12419px next to a 700px window. The cause was one word. `body` had `min-height: 100dvh`, which leaves the page free to grow with its content, and a column of flex boxes with no definite height at the top cannot cap anything below it. `min-height: 0` on the items lets them shrink but nothing was telling them what to shrink *to*. The page now has a definite height and no scrolling of its own, so the two panes do the scrolling, which is what they were built for. The settings screen and the login screen scroll themselves, having no pane. While there: the editor's floor drops from 12rem to 6rem so a landscape phone with its keyboard up keeps the toolbar and the save button on screen rather than overflowing, and the login card centres with `margin: auto` rather than `justify-content`, because a centred flex item in a scrolling container cannot be scrolled back to once it overflows the top. Reproduced with 123 seeded day notes and measured in Firefox at 1100px and 390px, before and after: page 12684px → 700px, list scrolls itself, editor 12419px → 435px, toolbar and save button still on screen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
434 lines
12 KiB
CSS
434 lines
12 KiB
CSS
/*
|
|
* The app is used with one thumb, in a lesson, on a phone that may be at 10%.
|
|
* Everything below follows from that: one column, large touch targets, the
|
|
* editor taking every pixel that is not navigation, and no webfont — the CSP
|
|
* forbids outside resources anyway, and a font that has not loaded is a blank
|
|
* screen in a classroom with no signal.
|
|
*/
|
|
|
|
:root {
|
|
color-scheme: light dark;
|
|
--bg: #ffffff;
|
|
--fg: #1f2328;
|
|
--muted: #656d76;
|
|
--line: #d0d7de;
|
|
--accent: #1f6feb;
|
|
--ok: #1a7f37;
|
|
--error: #cf222e;
|
|
--warn: #9a6700;
|
|
--card: #f6f8fa;
|
|
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--bg: #0d1117;
|
|
--fg: #e6edf3;
|
|
--muted: #8b949e;
|
|
--line: #30363d;
|
|
--accent: #4493f8;
|
|
--ok: #3fb950;
|
|
--error: #f85149;
|
|
--warn: #d29922;
|
|
--card: #161b22;
|
|
}
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
/*
|
|
* A *definite* height, not a minimum.
|
|
*
|
|
* `min-height` leaves the page free to grow with its content, and a column of
|
|
* flex boxes with no definite height at the top cannot cap anything below it:
|
|
* with a hundred notes in the list, the list grew to its full height, the page
|
|
* scrolled instead of the list, and the editor — a flex sibling stretching to
|
|
* the row — became as tall as every note put together. Scrolling belongs to the
|
|
* two panes, so the page itself must not have any.
|
|
*/
|
|
body {
|
|
margin: 0;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
height: 100vh;
|
|
/* dvh where it exists: 100vh lies about the height while a phone's toolbar
|
|
is on screen. */
|
|
height: 100dvh;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
|
}
|
|
|
|
.screen { display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; }
|
|
|
|
/* The page does not scroll, so anything that is not itself a pane must. */
|
|
#login, #view-settings { overflow-y: auto; }
|
|
[hidden] { display: none !important; }
|
|
|
|
/* --- chrome ------------------------------------------------------------ */
|
|
|
|
header { border-bottom: 1px solid var(--line); }
|
|
|
|
.tabs { display: flex; }
|
|
|
|
.tab {
|
|
flex: 1;
|
|
padding: 0.9rem 0.5rem;
|
|
border: 0;
|
|
border-bottom: 2px solid transparent;
|
|
background: none;
|
|
color: var(--muted);
|
|
font: inherit;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.tab[aria-current="page"] { color: var(--fg); border-bottom-color: var(--accent); }
|
|
|
|
.view { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0.75rem; gap: 0.5rem; }
|
|
|
|
/* --- the notes screen: a list, and the note that is open ---------------- */
|
|
|
|
/*
|
|
* Two panes where there is room, one at a time where there is not. The
|
|
* breakpoint is about where a phone in landscape stops being a phone: below it
|
|
* `data-pane` on the container decides which of the two is on screen, and the
|
|
* back button is the way out of the note.
|
|
*/
|
|
.notes { flex-direction: row; gap: 0; padding: 0; }
|
|
|
|
.rail {
|
|
flex: 0 0 18rem;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-right: 1px solid var(--line);
|
|
background: var(--card);
|
|
}
|
|
|
|
.rail-head { display: flex; gap: 0.4rem; padding: 0.6rem 0.6rem 0.4rem; }
|
|
|
|
.rail-head form { flex: 1; min-width: 0; }
|
|
|
|
.rail-head input {
|
|
width: 100%;
|
|
padding: 0.55rem 0.7rem;
|
|
border: 1px solid var(--line);
|
|
border-radius: 0.5rem;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
font: inherit;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
#today { flex: 0 0 auto; width: 2.5rem; padding: 0; font-size: 1.1rem; line-height: 1; }
|
|
|
|
#rail-status { padding: 0 0.7rem 0.3rem; }
|
|
|
|
.rail-list { flex: 1; min-height: 0; overflow-y: auto; padding: 0 0.4rem 0.6rem; }
|
|
|
|
/* A whole row is the target: on a phone the thing being tapped is the note,
|
|
not a link inside it. */
|
|
.row {
|
|
display: block;
|
|
width: 100%;
|
|
text-align: left;
|
|
padding: 0.5rem 0.6rem;
|
|
margin-bottom: 0.25rem;
|
|
border: 1px solid transparent;
|
|
border-radius: 0.5rem;
|
|
background: none;
|
|
color: var(--fg);
|
|
font: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.row:hover { background: var(--bg); }
|
|
|
|
.row[aria-current="true"] {
|
|
background: var(--bg);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.row-title { font-weight: 600; font-size: 0.95rem; }
|
|
.row-line { margin: 0.1rem 0 0; color: var(--muted); font-size: 0.85rem; line-height: 1.35; }
|
|
/* Two lines of preview and no more: a row is a glance, not a read. */
|
|
.row-line.clamp {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.row mark { background: color-mix(in srgb, var(--accent) 28%, transparent); color: inherit; border-radius: 0.15rem; }
|
|
|
|
.detail {
|
|
flex: 1;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
padding: 0.75rem;
|
|
/* The editor inside it scrolls; the pane itself never does, so the toolbar
|
|
and the save button cannot be pushed off a short screen. */
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Only a phone needs a way back to the list; on a wide screen it is never gone. */
|
|
.back { display: none; align-self: flex-start; padding: 0.4rem 0.7rem; font-size: 0.9rem; }
|
|
|
|
@media (max-width: 720px) {
|
|
.rail { flex: 1; border-right: 0; }
|
|
.notes[data-pane="list"] .detail { display: none; }
|
|
.notes[data-pane="note"] .rail { display: none; }
|
|
.notes[data-pane="note"] .back { display: block; }
|
|
}
|
|
|
|
.note-preview { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 0.4rem; }
|
|
.note-preview h2 { margin: 0; font-size: 1.05rem; }
|
|
.note-preview p { margin: 0; }
|
|
|
|
/* --- the day bar ------------------------------------------------------- */
|
|
|
|
.daybar { display: flex; align-items: center; gap: 0.5rem; }
|
|
|
|
.daybar button {
|
|
flex: 0 0 auto;
|
|
width: 2.75rem;
|
|
height: 2.75rem;
|
|
font-size: 1.5rem;
|
|
line-height: 1;
|
|
border: 1px solid var(--line);
|
|
border-radius: 0.5rem;
|
|
background: var(--card);
|
|
color: var(--fg);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.daybar-centre { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 0.15rem; }
|
|
.daybar-centre strong { font-size: 1.05rem; }
|
|
.daybar-centre input { border: 0; background: none; color: var(--muted); font: inherit; font-size: 0.85rem; }
|
|
|
|
/* --- the toolbar ------------------------------------------------------- */
|
|
|
|
/*
|
|
* One row that scrolls sideways rather than wrapping into two: a second row
|
|
* would take a line of editor away from every note to hold buttons that are
|
|
* used once a lesson, and a thumb swipes a row far more easily than it hunts
|
|
* through a grid.
|
|
*/
|
|
.toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
overflow-x: auto;
|
|
scrollbar-width: none;
|
|
padding-bottom: 0.15rem;
|
|
}
|
|
|
|
.toolbar::-webkit-scrollbar { display: none; }
|
|
|
|
.toolbar button {
|
|
flex: 0 0 auto;
|
|
min-width: 2.5rem;
|
|
height: 2.5rem;
|
|
padding: 0 0.5rem;
|
|
font-size: 0.9rem;
|
|
line-height: 1;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.toolbar button[aria-pressed="true"] {
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
background: color-mix(in srgb, var(--accent) 12%, var(--card));
|
|
}
|
|
|
|
.toolbar button code { font-family: ui-monospace, monospace; font-size: 0.85rem; }
|
|
.sep { flex: 0 0 auto; width: 1px; height: 1.5rem; background: var(--line); margin: 0 0.15rem; }
|
|
|
|
/* --- the editor -------------------------------------------------------- */
|
|
|
|
/*
|
|
* The formatted document. It is the note as it will read, not as it is stored
|
|
* — the file underneath is still Markdown, and `MD` in the toolbar shows it.
|
|
*/
|
|
.editor {
|
|
flex: 1;
|
|
/* Small enough that a landscape phone with its keyboard up still shows the
|
|
toolbar and the actions rather than overflowing the pane. */
|
|
min-height: 6rem;
|
|
overflow-y: auto;
|
|
padding: 0.75rem;
|
|
border: 1px solid var(--line);
|
|
border-radius: 0.5rem;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
font-size: 1rem;
|
|
line-height: 1.55;
|
|
/* A long URL or a wide table must not push the page sideways. */
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
.editor:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
|
|
|
|
.editor.empty::before {
|
|
content: attr(data-placeholder);
|
|
color: var(--muted);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.editor > :first-child { margin-top: 0; }
|
|
.editor > :last-child { margin-bottom: 0; }
|
|
.editor p { margin: 0 0 0.75rem; }
|
|
|
|
/* A lesson heading is the note's structure — each one is indexed as its own
|
|
lesson — so it is given a rule to sit on rather than just a larger size. */
|
|
.editor h2 {
|
|
margin: 1.25rem 0 0.5rem;
|
|
padding-bottom: 0.2rem;
|
|
border-bottom: 1px solid var(--line);
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.editor h1 { font-size: 1.25rem; margin: 1.25rem 0 0.5rem; }
|
|
.editor h3, .editor h4, .editor h5, .editor h6 { margin: 1rem 0 0.35rem; font-size: 1rem; }
|
|
|
|
.editor ul, .editor ol { margin: 0 0 0.75rem; padding-left: 1.4rem; }
|
|
.editor li { margin: 0.15rem 0; }
|
|
.editor li.task { list-style: none; margin-left: -1.2rem; }
|
|
.editor li.task input { margin-right: 0.4rem; }
|
|
|
|
.editor blockquote {
|
|
margin: 0 0 0.75rem;
|
|
padding-left: 0.75rem;
|
|
border-left: 3px solid var(--line);
|
|
color: var(--muted);
|
|
}
|
|
|
|
.editor code {
|
|
padding: 0.1em 0.3em;
|
|
border-radius: 0.25rem;
|
|
background: var(--card);
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.editor pre {
|
|
margin: 0 0 0.75rem;
|
|
padding: 0.6rem 0.75rem;
|
|
border-radius: 0.5rem;
|
|
background: var(--card);
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.editor pre code { padding: 0; background: none; }
|
|
.editor hr { border: 0; border-top: 1px solid var(--line); margin: 1rem 0; }
|
|
.editor a { color: var(--accent); }
|
|
|
|
/* A table wider than the phone scrolls inside the note rather than stretching
|
|
it: the caret has to stay reachable. */
|
|
.editor table { display: block; overflow-x: auto; border-collapse: collapse; margin: 0 0 0.75rem; font-size: 0.9rem; }
|
|
.editor th, .editor td { border: 1px solid var(--line); padding: 0.3rem 0.5rem; text-align: left; min-width: 3rem; }
|
|
.editor th { background: var(--card); }
|
|
|
|
textarea {
|
|
flex: 1;
|
|
min-height: 6rem;
|
|
width: 100%;
|
|
padding: 0.75rem;
|
|
border: 1px solid var(--line);
|
|
border-radius: 0.5rem;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
/* Monospace: this is the Markdown view, and headings and list markers have
|
|
to line up to be read back as structure. */
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
font-size: 0.95rem;
|
|
line-height: 1.5;
|
|
resize: none;
|
|
}
|
|
|
|
.actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
|
|
|
|
button {
|
|
padding: 0.65rem 1rem;
|
|
border: 1px solid var(--line);
|
|
border-radius: 0.5rem;
|
|
background: var(--card);
|
|
color: var(--fg);
|
|
font: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
button:disabled { opacity: 0.5; cursor: default; }
|
|
|
|
#save, #login-form button, #token-form button {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: #ffffff;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* --- text -------------------------------------------------------------- */
|
|
|
|
.status { margin: 0; color: var(--muted); font-size: 0.85rem; min-height: 1.2em; }
|
|
.hint { color: var(--muted); font-size: 0.85rem; }
|
|
.ok { color: var(--ok); }
|
|
.error { color: var(--error); margin: 0.5rem 0 0; }
|
|
.warn { color: var(--warn); }
|
|
|
|
.conflict {
|
|
margin: 0;
|
|
padding: 0.6rem 0.75rem;
|
|
border: 1px solid var(--warn);
|
|
border-radius: 0.5rem;
|
|
color: var(--warn);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* --- cards (login, settings) ------------------------------------------- */
|
|
|
|
.card {
|
|
margin: 0.75rem;
|
|
padding: 1rem;
|
|
border: 1px solid var(--line);
|
|
border-radius: 0.75rem;
|
|
background: var(--card);
|
|
}
|
|
|
|
.card h1, .card h2 { margin-top: 0; font-size: 1.15rem; }
|
|
|
|
label { display: block; margin: 0.75rem 0 0.25rem; font-weight: 600; font-size: 0.9rem; }
|
|
|
|
input[type="password"], input[type="text"] {
|
|
width: 100%;
|
|
padding: 0.7rem;
|
|
border: 1px solid var(--line);
|
|
border-radius: 0.5rem;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
font: inherit;
|
|
}
|
|
|
|
/* `margin: auto` rather than `justify-content: center`: a centred flex item in
|
|
a scrolling container cannot be scrolled back to once it overflows the top,
|
|
which on a short screen with the keyboard up would put the password field out
|
|
of reach. */
|
|
#login .card { width: min(24rem, calc(100% - 1.5rem)); margin: auto; }
|
|
#login button { width: 100%; margin-top: 1rem; }
|
|
|
|
.steps { margin: 0.5rem 0; padding-left: 1.1rem; color: var(--muted); font-size: 0.85rem; line-height: 1.5; }
|
|
.steps code { font-family: ui-monospace, monospace; }
|
|
|
|
#token-form button, #logout { margin-top: 0.75rem; }
|
|
|
|
dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 0.35rem 0.75rem; font-size: 0.9rem; }
|
|
dt { color: var(--muted); }
|
|
dd { margin: 0; }
|