The PWA’s design system#
The phone app is a tool you reach for one-handed, often in a hurry: open, capture, gone. The
design follows from that, and from keeping the app tiny: the shell (capture, lists, pages,
search) uses no UI framework, icon library or web font, and its JavaScript is held under 16.5 kB
gzipped (npm run size, web/scripts/size.mjs). The one exception is the drawing viewer for a
photo’s drawing: a separate chunk, fetched only when a drawing is shown, built on React and
Excalidraw with Excalidraw’s own fonts (served by scrap from /excalidraw/fonts/), and outside
that budget. Everything visual is defined once, in web/src/tokens.css; the components in
web/src/style.css (which imports web/src/css/*.css) and web/src/excalidraw.css (the photo panel and the drawing viewer) use
nothing else. This page is the portable description, for carrying the design into another tool
and back.
Principles#
Paper and ink, one colour. Warm off-white paper and warm black ink in daylight, a warm near-black and cream at night, and a single amber (a sticky note: scrap’s mark). Filled amber is the one hero (the screen’s primary action) and the badge dot; amber-soft means only “selected” or “active” (a chosen chip or segment, the current nav tab, a match highlight), never a label, count or state; and there is no amber decorative text and no amber link in the app’s chrome (counts, headers, Claude’s cards): links there are ink, underlined (Links, below). Amber link text is the owner’s own text’s alone. Red means only “destructive”, “failed” or “overdue”. No blue-greys, no second accent.
Links: every text link is underlined; its colour says whose it is. A link in the owner’s own text (a page’s Markdown and
[[wiki]]links, a topic’s notes, the editor’s Preview:.body) is--accent-ink, underlined. Every other text link, Claude’s (Related, Topics, Sources, a write-up’s links) and the app’s (Regenerate, Clear filters, Clear all, Edit pins, Put back, Discard it, “by Claude”), is its text colour (--ink, or--ink-2for a quiet control) over a thin underline:--line-strong, 1 px, offset 0.2 em. On a card where--line-strongis under 3:1 (the Claude card’s--sunk, the restored-draft line’s--accent-soft) the underline is--ink-2(--link-line). Each keeps its weight; Claude’s links are regular. One rule draws them all (a,.linkincss/base.css, and:where(.body) afor the owner’s, a write-up’s.bodyexcepted), so a new link follows it without a rule of its own. Not text links, so not underlined: whole-row list items (search, Todos, topic, Pinned and Recent rows), chips and pills (tags, filters, the due chip), icon buttons, the nav and the kind row, buttons that look like buttons (.button, Download, a link card, a file card), the top bar’s Back and jump, footnote marks, and a section’s heading that links (a Type heading, which reads as a heading).One hero per screen. Composer and editor: Save, the one amber fill. Todos: no hero (New todo is a quiet + in the top bar). Capture: no hero; its four actions (Camera, Gallery, File, Note) are equal tiles, since none is more likely than the others (on a computer two: Add files and Note). Page: a todo’s or a Later’s Mark done, the toolbar’s one fill; a page with no status has none. Everything else steps down: secondary (outlined surface), then ghost (text only).
Under the thumb. Actions on the thing in hand live at the bottom: Capture’s actions and the nav on a list screen, a page’s toolbar on a page. Getting around lives in the top bar (Back and the jump to the list), so a page has no nav and its bottom bar only ever acts on that page. On a computer (a mouse and a window 900 px wide or more:
css/desktop.css, one media query, so a phone or a touch tablet never changes) things go where the eye and the mouse already are instead: the nav is tabs in the middle of the top bar (icon beside its word, the current one on--accent-soft, the Todos count after the word; on a list screen the top bar’s own column is 960 px there, and a long development version in the wordmark is cut with …, so four tabs clear the wordmark and Settings; a page, which has no nav, keeps its Back, jump to the list and pager on the same column, 800 px by default and as wide as Settings’ Maximum column width allows), Capture’s tiles are a row over the list, a page’s actions are a row of buttons under its title, meta line, dates line and chip row, kept in view under the top bar as it scrolls (Mark done filled, Delete apart at the end), toasts sit at the window’s foot as wide as they need, and rows, chips, tiles and ghost buttons take--sunkon hover. Cancel, Preview and Save sit in the editor’s top bar because the keyboard owns the bottom.Drill down, then Back, named. A page is a drill-down. On every phone and browser the top bar swaps the wordmark for Back named for where it goes (‹ Search, ‹ Todos, ‹ Topics, ‹ Capture, or a page’s title), as a mail app’s ‹ Inbox, and it goes up one level: to the page you followed a link from, else to the list you opened it from (past the pages the pager stepped through). When Back doesn’t go to the list, the list’s name follows it as a second target, ‹ A · Search, which jumps straight back to the list. A page opened cold goes up to its parent list (Todos for a todo, Topics for a topic, Capture for any other page): Back goes where its label says, with no request, so while the page is still loading that is Capture and on a stalled link it is never a dead button. Every link to a page,
[[slug]]or its full address on any address the server is reached at (the one the app was opened at, and each inSCRAP_PUBLIC_URL, which the server writes intoindex.html’sscrap-originsmeta tag), opens in place, so the phone’s own Back walks back through them one step at a time, as the top bar’s Back does after a link. Delete is one step back (the list, or the page whose link opened it) with Undo; opened cold, it goes where Back names. Whichever Back (the top bar’s, the jump, the phone’s) or Forward, a screen comes back scrolled where it was left (scroll.ts: the position rides in its history entry, put back once the screen is tall enough, never smoothly; the search screen keeps its own).Icons with words. Icons help find a control; the word says what it does. Icon-only buttons (close, previous/next photo, remove a photo) carry an
aria-label.Calm states. Empty, offline and error states are an icon, one plain sentence and at most one action (offline or unreachable, everywhere search included: why, and Retry). On the capture screen a problem is a quiet inline line, never a whole screen; offline there is the badge, the waiting rows and one line, with no card, Retry or toast.
Say it once. A row shows its kind by its icon alone, and a status that ends it (done, cancelled) as a word on its meta line (”· done”). A page says its kind and status once, in its meta line. A count shown in a header is not said again by an empty state (a search with no results shows no count).
Few sizes, one style per job. A screen’s text uses few sizes, each for one role (a page’s header: two; a Claude card: two); a pill or chip has one style per job: selected or active on
--accent-soft, a tag on--sunk, an editable chip as the others it sits with. One chip size app-wide (Chip, below):--t-smtext in a 32 px pill drawn inside a 48 px target. A label never breaks inside a phrase: a line of “ · “-separated parts breaks only before a “·”, which leads the part after it, so no line ends in one.Dark is designed, not inverted. Surfaces get lighter as they come forward; amber is a step lighter; toasts stay dark slabs in both themes.
Every gesture has a button. Swipes are shortcuts, never the only way: a row’s Edit and Delete are also on its page (and a row’s tick is a button of its own, with Mark done and Reopen on the page too), the page’s swipe is also its ∧ ∨ pager and the arrow keys. Gestures start away from the screen’s edges, which belong to the phone’s Back gesture.
Motion confirms, never decorates. 90-260 ms, eased out; none under reduced motion. The two looping indicators (busy, loading image) are the only motion that runs longer.
Tokens#
Light / dark values, held to WCAG AA (4.5:1 for text, 3:1 for control edges) twice:
web/tests/contrast.test.ts reads tokens.css and checks the listed text/background pairs,
every rule in web/src/css/ that sets both a text colour and a background from tokens, and white
text on --overlay over a white photo; and an end-to-end sweep (the lowContrast helper in
web/e2e/contrast.ts, run by web/e2e/view.spec.ts on each screen in both themes and by
several other e2e specs) computes the real colour of every visible piece of text and of what
lies behind it.
The theme follows the system unless Settings (the top bar’s cog) says otherwise
(24. A settings pane in the top bar, with theme overrides): Light or Dark sets data-theme on
<html>, which tokens.css reads in place of the media query (web/src/theme.ts). Drawings
have a theme of their own, Light by default.
The browser’s theme-color follows the colour scheme: two <meta name="theme-color"> tags in
web/index.html, one per scheme, with the --bg values below (theme.ts sets both to the
chosen one when the theme is forced). The PWA manifest
(web/public/manifest.webmanifest) does not: its theme_color and background_color are a
fixed dark #161411.
Token |
Light |
Dark |
Role |
|---|---|---|---|
|
|
|
the page; theme-color |
|
|
|
cards, list groups, fields |
|
|
|
wells: tiles, segmented track, placeholders, pressed |
|
|
|
text |
|
|
|
secondary text: dates, hints |
|
|
|
placeholders, captions |
|
|
|
hairlines (decorative) |
|
|
|
field and control edges; the underline of a link that isn’t the owner’s ( |
|
|
|
the hero’s fill, the badge dot, a selected segment’s icon, a new topic’s dot |
|
|
|
text on accent |
|
|
|
accent as text: links in the owner’s text, focus ring, active icon |
|
|
|
selected or active only: a chosen chip, the active nav pill, a pressed toggle, match highlights, a swipe to Edit |
|
|
|
destructive, failed (text) |
|
|
|
failed badge, conflict banner |
|
|
|
toasts |
|
|
|
a toast’s action |
|
|
|
the photo viewer’s backdrop |
|
|
|
text and icons over photos |
|
|
same |
pills, scrims and buttons over photos (white on it is over 4.5:1 even on a white photo) |
Presses, edges and washes that need a translucent tint mix a token with transparent
(color-mix(in srgb, var(--ink) 14%, transparent)) rather than writing a new colour.
Type: the system UI face (Roboto on Android; --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif, and --mono for code: ui-monospace, "SF Mono", Menlo, Consolas, monospace); a 17 px root, so a rem is 17 px and --t-xs (0.75rem) is 12.75 px. Settings’ Text size sets the root: 100%
(the default, 17 px, sized as a web page is, so the browser’s zoom applies) or a percentage of it,
inline on <html> (main.ts at the start and when the app comes back, theme-init.js before
the first paint), or Phone (data-text="phone"). With Phone, on Android the root follows the
phone’s font size: index.html opts in with <meta name="text-scale" content="scale"> (Chrome
146+) and the root is calc(17px * env(preferred-text-scale, 1)); on iPhone the root follows the
phone’s text size, Larger Text too (font: -apple-system-body, applied only where
-webkit-touch-callout exists, so iOS and not a Mac), with scrap’s own family and line height
set again after it; a small check in main.ts keeps it from going under 17 px (the smaller
settings, with Phone), measured again whenever the app comes back. --t-xs 0.75rem (labels under icons,
chips), --t-sm 0.875rem (meta, small buttons), --t-md 1rem (body, list titles), --t-lg
1.1875rem (wordmark, field titles, a Markdown h2), --t-xl 1.5rem (page title). Markdown
headings step down a size a level, all under the page title: h1 1.3125rem, h2 --t-lg,
h3 1.0625rem semibold, h4-h6 --t-md. Line height
--lh 1.5, --lh-tight 1.25 for titles, 1.6 for page text. Weights 500 (controls), 600 (titles, labels), 700
(page title, active nav).
Space: a 4 px grid, --s-1..--s-7 = 4, 8, 12, 16, 24, 32, 48. Screen gutter 16.
Radius: --r-sm 8 (thumbnails, code), --r-md 12 (buttons, fields, toasts), --r-lg 16
(cards, lists, photos), --r-pill (badge, search field, nav indicator).
Size: --tap 48 (the touch target, extended with an invisible ::after where the drawn control
is smaller; the exceptions are listed under “What we don’t do”), --bar-h 56, --nav-h 64 (max(64px, 1.125rem + 44px): taller when its labels
are, at a large text size). Elevation: --shadow (selected segment), --shadow-2
(capture cluster, toasts).
Motion: --d-1 90 ms (press), --d-2 180 ms (view enter, toast), --d-3 260 ms (editor sheet,
viewer); --ease cubic-bezier(0.2, 0.8, 0.2, 1). Under prefers-reduced-motion all three are
0, and the two looping indicators stop: the placeholder shimmer (1.2 s, alternating) and the busy
icon’s pulse (0.8 s, see Busy under States). Those two are not on the --d-* scale: they repeat
only while the work or the image is in progress.
Components#
Component |
Class / element |
Notes |
|---|---|---|
Button, secondary |
|
surface, hairline edge, 48 high, icon 20 + label; presses scale to 0.97 on |
Button, primary |
|
amber fill, ink label; one per screen (Save) |
Button, ghost |
|
no fill (Cancel, Discard it, the pager’s ∧ ∨) |
Button, danger |
|
red label, never filled |
Tile |
|
icon over a short label on |
Toolbar |
|
icon-over-label tiles sharing the width by their labels ( |
Segmented control |
|
the editor’s kind ( |
Chip |
|
one chip size app-wide: every chip (the tag picker’s chosen and offered, the due chips, the Filters panel’s When, Order and Group, the Filters on, a page’s own chip row) is |
Top bar |
|
wordmark with the scrap mark; at the right, the queue badge, then Settings ( |
Badge |
|
a pill on |
Bottom nav |
|
four tabs, Capture, Todos, Search, Topics (the newest last, so the others keep their places): icon over label; active item gets an amber-soft pill and ink label ( |
Nav count |
|
the Todos tab’s open todos due today or overdue: a 20 px |
List group |
|
rows in one rounded card; |
List row |
|
48 thumbnail or kind icon (the icon says the kind), title, then one meta line: its date, or for a todo with a due line its due line in the date’s place (“high · due 26 Oct · 1 of 4”, |
Swipe row |
|
follows the finger; behind it Edit on |
Fold |
|
hairline above, a chevron before the heading: right folded, down open |
Pager |
|
a page opened from a list, in the top bar as a mail app has it: “3 of 23” ( |
Status word |
|
on a row only for a status that ends it: “· done”, “· cancelled”, |
Meta line |
|
a page’s header under its title: one quiet |
Dates line |
|
“Created 24 Oct, 09:12 · Edited 25 Oct, 14:03” ( |
Due line |
|
a todo’s priority and due date in words (“high · due 3 Nov”; a priority alone “low priority”), |
Public chip |
|
on a public page (#497), first in the chip row: a tag chip in every way ( |
Question |
|
public pages’ in-app questions (Make public’s warning, every time, and Refresh all’s first-time list): a |
Filters button |
|
a slider icon (filter) at the search field’s end, its whole 48 px width the target, nothing drawn around it, so the box keeps one clean edge at any text size: |
Filters on |
|
under the kind bar, each filter on (not the kind row’s kind) as a |
Results header |
|
one 48 px line that never changes shape: the count on the left ( |
Filters panel |
|
opens in place under the search field, above the kind row (the filters it puts on show under the kind row, as chips), a |
Todos screen |
|
the segmented control again, as the kind row (Now, a clock; Upcoming, the due calendar: due from tomorrow to 7 days away; All, the grid: each sets its own order, so no Sort), then the results header ( |
Topics screen |
|
the topic pages Claude keeps (#261), from |
search, a link, the pager); what was seen is |
||
Store health |
|
opened from Settings: an |
Trash |
|
opened from Settings: an |
Done fold |
|
Now’s done and cancelled todos, folded at the end: a 48 px summary in |
Search sections |
|
the search screen’s grouped results (Group row in Filters: Type, Now, Due, Area, None, A–Z, Tag; the list’s own first; for |
Row tick |
|
a todo’s or a Later’s row: a 48 px toggle ( |
Claude card |
|
everything Claude makes, so it never reads as the owner’s text: a |
Topic parts |
|
Claude’s topic pages (#261), in About this page. On a source, Topics is a fold (open until closed) drawn as Related is, straight before it: a line per topic, the topic icon hung in the indent, its title an --ink-2, “· taken out”, and Put back (button.link) in the ✕’s place; taking out toasts “Taken out: --ink-2 line (.page > .notes) says where they go |
Link card |
|
on a Later page, a paragraph that is only a link to the web ( |
Draft row |
|
the composer’s kept draft (left by Back), one quiet line on |
Offline line |
|
Capture offline, in Latest’s place: “Offline: showing what’s on this phone”, |
Figure |
|
a page’s photo as a preview, with no caption: a tap opens the viewer, which names it ( |
Claude’s reading |
|
under each photo, image or file that has a reading (#521): one fold, always closed when the page opens and not remembered, its line as every other fold’s (the chevron before the words “Claude’s reading”, semibold |
File row |
|
a file below a page’s text that is not a photo (a PDF, a text file): a card with the file icon, its name (wrapping) and, once read, a PDF’s page count under it ( |
Thumbnail |
|
fixed size or 4:3 frame reserved before load, shimmering |
Field |
|
surface, strong edge, 2 px accent-ink focus ring |
Search field |
|
pill with a search icon, the Filters button (above) at its end, drawn inside it; on the search screen a ✕ ( |
Recent searches |
|
under the search field while it is empty and focused (gone once it has text, or loses focus): an |
Editor sheet |
|
full screen over the keyboard; top bar Cancel / Preview / Save. The text box asks for what the kind needs: “Note or link”, “What needs doing?”, “Link or title”, “What the board is about”, “What the document is”. Just above the keyboard, one line of seven equal keys ( |
Link picker |
|
the Link key’s page picker, a chunk of its own loaded on the first press: a modal |
Embed chips |
|
the page editor and the composer show each of the page’s own images and files (the composer’s not yet uploaded: a thumbnail from the phone’s own bytes, |
Editor fold |
|
in the composer and editor, the rarely changed parts folded to one 48 px line each above the text, so the text stays the hero: Due, first, while the kind is Todo (the calendar, “24 Oct · high”, or “none”), then Tags (the tag icon, “3 · #work #garden…”, or “none”), in that order in the composer and the page editor. The page editor is where a page’s due date, priority and tags are changed: its due chip opens it with Due open; the name in |
Share menu |
|
Share’s choice, a popover just above it (below it where it does not fit above, as on a computer, whose tools are at the top; capped to the room there, scrolling past it): Link, the content (Text on a page, Photo in the viewer), and on a page Print / Save as PDF, HTML (Download HTML where the file can’t be shared), Zip and Download (a link drawn as the rows are; tapping it closes the menu). On a computer with no share sheet a page’s menu copies instead: Copy link, Copy as Markdown and Copy as rich text in place of Link and Text, each saying so in a toast. Ghost rows on |
Tag line |
|
one row of same-size chips under a page’s meta line, read only, drawn inside 48 px targets and wrapping to at most two rows: a todo’s due chip first ( |
Tag picker |
|
one picker wherever tags are picked: the tag editor ( |
Toast |
|
dark slab above what is under the thumb, amber text action. A capture saved online says “Saved” at once (offline, on another screen than Capture, “Saved on the phone: uploads when online”); if the server then refuses it, a second toast says “Kept on this phone” (its row says why, with Retry). “Update available” with Reload appears when a new version of the app has installed behind the one running; it has no timeout and stays through navigation until tapped, and Reload swaps it in and reloads the app |
Banner |
|
in-place notices, never colour alone. Conflict ( |
Settings sheet |
|
from the top bar’s cog (above): a |
Pinned list |
|
under the empty search box, above Recent, only when something is pinned: an |
Empty state |
|
icon in a |
Match highlight |
|
the matched words in a search hit’s snippet: |
Build line |
|
the running build (the service worker’s cache name, |
By Claude |
|
on a page Claude made, in its meta line right after the kind word: “by Claude”, a quiet |
Topic word |
|
a search hit that is a topic page: “· Topic” after its date, in |
Draft dot |
|
a 9 px |
File card |
|
under a PDF’s or text file’s row ( |
Photo panel |
|
under a photo’s page: the photo and, if Claude drew one, its drawing, as a |
Drawing viewer |
|
the full-screen drawing ( |
Photo viewer |
|
|
Icons |
|
24 px strokes (1.8), currentColor, |
Markdown |
|
headings a step down a level (see Type), all under the page title; on a Later page, a link alone on a line is a link card (above); code and a table’s head on |
States#
State |
How it shows |
|---|---|
Pressed |
scale 0.97 over |
Focus (keyboard) |
2 px |
Selected |
segmented: raised |
Disabled |
50 % opacity (exempt from contrast, and never the only sign: the label says why) |
Busy |
the label reads “Preparing…” and taps are ignored; the label is never dimmed, so it stays at full contrast. Only the control’s icon pulses (opacity down to 0.3 and back, 0.8 s, looping), so the state shows even where labels are hidden; it stops under reduced motion |
Loading image |
the reserved box shimmers between |
Loading view |
after 150 ms without an answer, a plain “Loading…” line in place of the last screen (a quick answer shows none; the same page again does not blank) |
Not found |
an empty state with the search icon, “Not found: it may have been deleted.” and Back to capture; any other failure is the offline icon, its reason and Retry |
Failed |
|
Overdue |
a todo’s due line in |
The Filters panel: in place, not a modal#
The refine row kept gaining controls at its left end (Clear, Tags, Sort, the count, then every
filter chosen), pushing what you came for off the screen. Those controls are used now and then,
not on every search, so they moved behind one Filters button. For a while it opened a bottom
sheet (a modal <dialog>), but a sheet hides the list its choices change, needs its own Back
handling (a history entry of its own, stepped over on Forward) and costs a tap to close after
every few taps. So the filters open in place instead:
Filters is a slider icon at the end of the search field, where the thumb already is (no count on it: the chips under the kind row say what is on). A tap opens or folds the panel (
aria-expanded,aria-controls); open, the icon sits in a 36 px--accent-softdisc with a 2 px--accent-inkring (6.5:1 on--surfacelight, 9.2:1 dark), a shape and a fill, not hue alone; it stays open or folded while the app is open, the next search too.The panel opens under the search field, above the kind row and the list, so each tap in it shows its effect at once. A row per kind of choice (Tags, When, Arrange), each a small heading over chips that wrap (never scrolling sideways), keeps it short. The tags it offers are capped at 8, and at two rows with the box (the tag picker). Typing matches the tags of the results (of every tag in use before a search), so there is no separate screen to browse them (the Tags screen is retired;
/tagsopens Search).What is on is always in view, panel open or not: a row of chips under the kind row, each with a ✕, then the results header. The kind row is always shown.
A tap changes the query at once, so the panel is never a form to submit and never holds state the address doesn’t: Back, a reload or a shared link show the same list. A control keeps its focus as the panel is drawn again.
It is
refine.ts, a chunk of its own loaded with the search screen and kept offline by the service worker, so the app shell stays small.
Layout and breakpoints#
Portrait phones (300 px and up): the default. Under 380 px wide a segmented control drops its icons, except the kind rows (search’s and the Todos screen’s) and the editor’s kind picker, which keep theirs over the word.
A wide window (760 px and up: a laptop, a tablet on its side): one centred column, at most 800 px by default (the top bar, the screen, the nav and the editor), the background still to the edges; Settings’ Maximum column width raises or lowers it (720 to 960 px), and a window narrower than the chosen width still shrinks the column to fit, with the usual gutter. On a computer a list screen’s top bar has a 960 px column of its own, so its four tabs clear the wordmark and Settings; a page’s top bar stays on the centred column.
Larger text (iPhone’s Larger Text at 150 % or 200 %): the kind rows and the kind picker wrap instead of clipping, the Filters tag box fits, and the nav grows with its labels. Left for later at 200 %: the page toolbar wraps Delete to a second row, the editor’s top bar is tight, its folds push the kind picker off-screen, and Capture’s tiles leave about two rows of the list in view.
Short screens (under 500 px high, i.e. landscape or a large text size): top bar and nav 48 px, and the page toolbar scrolls with the page instead of sticking. Landscape (also 560 px and wider): the nav’s label sits beside its icon and the capture tiles are 64 high.
Very narrow (under 300 px: a small phone at its largest text size): the capture cluster scrolls with the list instead of covering it (it is static and moves to the top,
order: -1, so Camera is on the first screen and the list follows), tiles go two by two, labels and list titles wrap, list thumbnails shrink to 32, and toolbars show icons only (the words remain their accessible names).Toasts sit just above whatever is under the thumb (the capture cluster, the page toolbar, the viewer’s tools or the nav), clear of the newest row and the title; in the editor, just under its top bar.
Text that may be right-to-left (titles, page text, snippets, the editor) has
dir="auto", and spacing uses logical properties (margin-inline-*,padding-inline-start).A page photo reserves a 4:3 box while it loads, then takes the photo’s own shape (
aspect-ratio: auto 4 / 3), at most 75 % of the screen high.
Print#
A page prints (the browser’s Print, or Save as PDF) as paper, not as the app: css/print.css,
imported last in style.css, so it is laid over everything else.
What prints: the title, the text with the photos in it (a board’s photo, not its drawing), and the text of the page’s Claude card, About this page: its summary, a write-up, topics, sources and related pages, with every fold open.
What is hidden: the top bar and nav, toasts and the debug panel, the page’s meta line, dates, tags and tools, the topic-notes prompt, the files listed under the text (
.originals), each photo’s and file’s own Claude card, a photo’s Photo / Drawing tabs, and every control and prompt in the card (Generate, Regenerate, the state pill, the guidance box). A card with no text is left out.Where breaks fall: each top-level block of the text asks not to be split from the next, except the one before a rule or an
h1/h2, so a sheet ends between sections (a question on a test paper) when it can. Paragraphs, code, list items without a nested list, short quotes, table rows, images, displayed maths and diagrams are not split; a long list or quote may be, but never between an item’s own line and the list nested in it. Wide code wraps and a wide table shows whole.The cost: a section that does not fit in what is left of a sheet starts the next one, so a sheet can end with blank space; one longer than a sheet still breaks inside.
Chrome does not keep a heading with a
detailsafter it, so in print the card’s title is drawn in its first fold’s heading, its short folds are kept whole, and a write-up’s heading sits on its byline.
High contrast#
Under forced-colors: active the system replaces fills and drops shadows, so the selected
segment and the current nav item get a 2 px Highlight outline, and the borderless tiles and
badge get a ButtonText edge. In the drawing viewer, the chosen Photo / Drawing tab and a pressed
toggle in its bar get a 3 px Highlight outline.
Icons#
Sizes: 20 in buttons, 24 in tiles, the nav and list placeholders, 18 in the
segmented control, 16 on a photo’s remove button, 52 (in a circle) in empty states.
Icons: camera, image, note, tag (the Tags fold), todo (a checklist: the Todo kind, so a row’s kind icon never reads
as a ticked box beside its tick box), tick (check in a square: a row’s tick, which hides the
check until done, and the Todos empty state; the nav’s Todos tab draws the same box inline), done (a check alone: Mark done, so a box always means a tick you can see), add (a plus: New todo, the tag row’s + circle), due (a calendar: a todo’s due pill, the Due fold, Upcoming), now (a
clock: Now), all (four tiles: All, on the kind row and the Todos screen), later (bookmark: Later), board (a whiteboard on
its stand), topic (an open book: the Topics tab, a topic’s row in search, the Topics empty state), link, open (an arrow out of a box: a link card’s Open), file, download,
share, edit, trash, search, filter (two sliders: the Filters chip), close, back, next, fit, retry (also Reopen), offline; the editor’s keys draw their own (an empty box, a bulleted and
a numbered list, lines moved in and out, Link’s chain, Undo’s arrow turning back); the nav’s plus, check box, magnifier and book are
inline in index.html. The app icon (web/public/icon.svg, PNGs by npm run icons) is the
paper scrap on amber; the header mark is the same scrap in amber.
Public pages#
A public page (#497) wears scrap’s look outside the app: web/src/public/page.css, built after
tokens.css into /assets/page.css (vite.config.ts, publicAssets), so the paper and ink
tokens and prefers-color-scheme are the app’s own, with no theme switch (the reader’s device
chooses). It styles only what the publish mode (publish.ts) writes: the site’s name over the
page (.site.top), the page’s header, text, files and Claude’s card as the app draws them, and the
footer “Published 1 Nov 2026 from scrap · All pages” (.site.foot). Tags are --sunk pills as in
the app; links follow the app’s rule (the owner’s text --accent-ink, the rest ink, every one
underlined). There is no script and no <style>: KaTeX’s CSS and fonts come beside it, as
/assets/katex.css.
What we don’t do#
No web font, icon font, CSS framework or component library in the shell. The drawing viewer is a separate lazily loaded chunk on React and Excalidraw, with Excalidraw’s bundled fonts, and is outside the 16.5 kB shell budget.
No colour outside
tokens.css(the debug panel aside), and no second accent; no gradients except the photo viewer’s scrim and the fade mask at the end of a sideways-scrolling row (.tags,.refine: a 32 pxmask-imagefade to transparent, not a colour). High-contrast mode uses the system’s colour keywords.No colour as the only signal: selection also raises, failure also says so in words.
No amber decorative text, no amber link in the app’s chrome (counts, headers, Claude’s cards), no text link without an underline, no amber-soft that doesn’t mean selected or active, and no filled amber but the screen’s one hero.
No sheet or dialog for choices: the Filters panel opens in place (above), the browser’s
confirm()stays for destructive questions, and the share menu is a light-dismiss popover. The full-screen photo viewer and drawing viewer (role="dialog",aria-modal) are the only modal surfaces of our own.No transition or entrance longer than 260 ms, no motion that carries meaning, none under reduced motion. The exceptions are the two looping indicators, which stop under reduced motion: the busy icon’s pulse (0.8 s) and the image placeholder’s shimmer (1.2 s).
No touch target under 48 px where it can be avoided (the 36 px badge, the 30 px remove button and the 42 px
.chipssegments extend their hit area with an invisible::after). The exceptions: the embed chip’s ✕ is 40 px wide (48 high); and the Photo / Drawing tabs are 42 px segments in a 48 px track, with no extension. (Chips and the fields beside them are 32 px drawn in a 48 px target: Chip, above.) No text under 12.75 px (--t-xs); no text pair under AA.No dimming to mean “busy”: the label (the text) is never dimmed, since opacity would break contrast; it says “Preparing…” in words, and only the control’s icon pulses.