oficina / memoria Public

Clone
git clone https://amadan.net/oficina/memoria

Plain git — no account needed to clone.

Download

Download this file

1/* memoria's own styles.
2 *
3 * Every colour is a token. Not one literal appears here, so the
4 * reader's light/dark choice resolves through theme.css and a passage
5 * never paints against the wrong ground. Sheets enforces the same rule
6 * with a test; memoria's equivalent is TestNoColourLiteralsInAppCSS.
7 */
8
9/* A stable gutter, so the scrollbar arriving between a short page (sign
10 * in) and a long one does not shift everything sideways. */
11html { scrollbar-gutter: stable; }
12
13[rst-page] { box-sizing: border-box; margin: 0 auto; max-width: 46rem; padding: 1.5rem 1rem 4rem; }
14
15/* The header is estilo's bar (chrome.css) and is not styled here. The
16 * footer is exactly the column's width — [rst-page]'s 46rem less its 1rem
17 * gutters — so its rule and its text start on the same line as every
18 * heading and rule above it (they sat 16px to the left, polish audit
19 * §B6). */
20[rst-page-footer] {
21 border-top: 1px solid var(--rst-line);
22 box-sizing: border-box;
23 margin: 2rem auto 0;
24 padding: 0.9rem 0 2rem;
25 width: min(44rem, 100% - 2rem);
26}
27[rst-inline] { display: inline; }
28
29/* Muted text. An attribute this app owns, because the design system's
30 * muted utility is a class, `.rst-cell-mut`, and spelled as an attribute
31 * — as every template here did, nineteen times — it styled nothing: the
32 * footer, table cells, result meta and every hint rendered at full body
33 * colour. The class is wrong for a sentence anyway (nowrap and an
34 * ellipsis, written for a table cell), so this is colour only. */
35[rst-muted] { color: var(--rst-text-muted); }
36
37/* The search form is rastrillo's [rst-search] field box (tokens.css): the
38 * border, radius and focus ring are the FORM's, and the input inside it
39 * is transparent. This file used to draw a second bordered box on the
40 * input, inside the first — the double border (polish audit §B3). Only
41 * the spacing below it is Memoria's. */
42[rst-search] { margin-bottom: 1.5rem; }
43
44[rst-section] { border-top: 1px solid var(--rst-line); margin-top: 2.5rem; padding-top: 1rem; }
45
46/* rastrillo's form bar is position:sticky; bottom:0, which is right for a
47 * long settings page and wrong here — it floated the submit button over
48 * the textarea it belongs under. Ours is an ordinary row. */
49[rst-stack] [rst-form-bar] { position: static; margin-top: 1rem; }
50[rst-stack] { display: flex; flex-direction: column; gap: 0.35rem; }
51[rst-stack] label { font-weight: 560; margin-top: 0.6rem; }
52/* A hint inside a label is quiet: muted and regular weight, not the
53 * label's 560 (polish audit r3 B7). */
54[rst-hint] { font-weight: 400; }
55/* border-box: the fields are width 100% PLUS their padding otherwise,
56 * which ran 22px past the column at 1440 and off the screen at 390
57 * (polish audit §B6). Password fields are here too: the Notion token
58 * fields were the browser's own. */
59[rst-stack] input[type="text"],
60[rst-stack] input[type="email"],
61[rst-stack] input[type="password"],
62[rst-stack] textarea {
63 background: var(--rst-bg);
64 border: 1px solid var(--rst-line);
65 border-radius: var(--rst-radius-sm, 6px);
66 box-sizing: border-box;
67 color: var(--rst-text);
68 font: inherit;
69 max-inline-size: 100%;
70 padding: 0.5rem 0.65rem;
71 width: 100%;
72}
73[rst-stack] input:focus-visible,
74[rst-stack] textarea:focus-visible {
75 border-color: var(--rst-accent);
76 outline: 2px solid var(--rst-accent);
77 outline-offset: 1px;
78}
79/* iOS zooms the page into any text control under 16px, and tokens.css
80 * brings controls up to 16px where the pointer is coarse. The fields
81 * above set `font: inherit` with a selector that outweighs tokens.css's
82 * bare `input` and `textarea`, and fell back to 14px: Add content's
83 * title and body, and the Notion token. Same rule, same max(), restated
84 * here at their weight. TestNoZoomOnTap (make browser) measures them. */
85@media (pointer: coarse) {
86 [rst-stack] input[type="text"],
87 [rst-stack] input[type="email"],
88 [rst-stack] input[type="password"],
89 [rst-stack] textarea {
90 font-size: max(1rem, 1em);
91 }
92}
93/* Pasted prose is prose: the body font, not a code box. */
94[rst-stack] textarea { resize: vertical; }
95/* A button is as wide as its label, not the column: [rst-stack] is a
96 * flex column, which stretched Connect and Replace edge to edge. */
97[rst-stack] > button { align-self: flex-start; }
98
99/* The file chooser. The native control stays — it is what works with no
100 * script, and it says which files were chosen — but its button is drawn
101 * as the app's buttons are, instead of the browser's grey "Choose Files". */
102[rst-stack] input[type="file"] { color: var(--rst-text-muted); font: inherit; font-size: var(--rst-fs-sm); max-inline-size: 100%; }
103[rst-stack] input[type="file"]::file-selector-button {
104 background: var(--rst-surface);
105 border: 1px solid var(--rst-line-strong);
106 border-radius: var(--rst-radius-sm, 6px);
107 color: var(--rst-text);
108 cursor: pointer;
109 font: inherit;
110 font-weight: 560;
111 margin-inline-end: 0.75rem;
112 padding: 0.4rem 0.8rem;
113}
114[rst-stack] input[type="file"]::file-selector-button:hover { background: var(--rst-surface-2); }
115
116/* The drop zone. Nothing here paints until static/drop.js has proved it
117 * can put a file in the input and written rst-dropzone="ready" — so a
118 * page whose script did not load shows an ordinary file input inside an
119 * invisible wrapper, rather than a dashed box that swallows drops. The
120 * bare [rst-dropzone] selector is deliberately styled with nothing.
121 */
122[rst-dropzone~="ready"] {
123 border: 1px dashed var(--rst-line-strong, var(--rst-line));
124 border-radius: var(--rst-radius-sm, 6px);
125 padding: 0.7rem 0.75rem;
126 transition: background-color 0.12s ease, border-color 0.12s ease;
127}
128/* A file is over the page. The whole document is the drop target — the
129 * zone is where it lands, not where it must be aimed — so this has to be
130 * visible from wherever the pointer happens to be.
131 */
132[rst-dropzone~="over"] {
133 background: var(--rst-accent-soft, var(--rst-surface-2));
134 border-color: var(--rst-accent, var(--rst-line-strong));
135 border-style: solid;
136}
137[rst-dropzone-hint], [rst-dropzone-note] { font-size: 0.8125rem; margin: 0.5rem 0 0; }
138/* Muted, and not with .rst-cell-mut: that utility exists only as a class,
139 * so `rst-cell-mut` written as an attribute selects nothing — and the
140 * class itself is a table-cell rule (nowrap, ellipsis) that would clip
141 * this sentence at the edge of the zone.
142 */
143[rst-dropzone-hint] { color: var(--rst-text-muted); }
144/* drop.js unhides the note while it is still empty, so the live region is
145 * in the accessibility tree before it ever has to announce a refusal. An
146 * empty paragraph with a top margin would push the Store it button down
147 * by half a line for no reason anyone could see, so while it is empty it
148 * takes no space at all.
149 */
150[rst-dropzone-note]:empty { margin: 0; }
151[rst-dropzone-note~="good"] { color: var(--rst-tone-positive-fg); }
152[rst-dropzone-note~="danger"] { color: var(--rst-tone-negative-fg); }
153@media (prefers-reduced-motion: reduce) {
154 [rst-dropzone~="ready"] { transition: none; }
155}
156
157/* The upload tray.
158 *
159 * It is in the DOM from the moment drop.js runs, because
160 * [rst-tray-status] is a live region and a live region has to be in the
161 * accessibility tree before it has anything to announce. So "not open"
162 * cannot be display:none — that would take the region back out again.
163 * Instead the head and the list are hidden, the panel paints nothing,
164 * and an empty status paragraph is zero-height, which leaves a fixed
165 * element of no size in the corner. pointer-events go with it: an
166 * invisible box that swallows clicks over the corner of the page would
167 * be worse than a visible one.
168 */
169[rst-tray] {
170 bottom: 1rem;
171 font-size: 0.8125rem;
172 inset-inline-end: 1rem;
173 position: fixed;
174 width: min(22rem, calc(100vw - 2rem));
175 z-index: 30;
176}
177[rst-tray]:not([rst-tray~="open"]) { pointer-events: none; }
178[rst-tray]:not([rst-tray~="open"]) [rst-tray-head],
179[rst-tray]:not([rst-tray~="open"]) [rst-tray-list] { display: none; }
180[rst-tray~="open"] {
181 background: var(--rst-surface);
182 border: 1px solid var(--rst-line);
183 border-radius: var(--rst-radius, 8px);
184 box-shadow: var(--rst-shadow-pop, none);
185 padding: 0.6rem 0.7rem;
186}
187[rst-tray-head] {
188 align-items: center;
189 display: flex;
190 gap: 0.5rem;
191 justify-content: space-between;
192}
193[rst-tray-title] { font-weight: 620; }
194/* 40vh so a drop of twenty-five files scrolls inside the tray rather
195 * than growing a panel taller than the window. */
196[rst-tray-list] {
197 list-style: none;
198 margin: 0.5rem 0 0;
199 max-height: 40vh;
200 overflow-y: auto;
201 padding: 0;
202}
203[rst-tray-row] {
204 border-top: 1px solid var(--rst-line);
205 display: grid;
206 gap: 0.15rem 0.5rem;
207 grid-template-columns: 1fr auto;
208 padding: 0.4rem 0;
209}
210[rst-tray-row]:first-child { border-top: 0; }
211/* A filename has no spaces to break at, and the tray is narrow. */
212[rst-tray-name] { font-weight: 560; overflow-wrap: anywhere; }
213[rst-tray-phase] { color: var(--rst-text-muted); text-align: end; }
214[rst-tray-row~="good"] [rst-tray-phase] { color: var(--rst-tone-positive-fg); }
215/* A refusal is a sentence, not a word: it gets the whole row width and
216 * reads left to right under the name it belongs to.
217 */
218[rst-tray-row~="danger"] [rst-tray-phase] {
219 color: var(--rst-tone-negative-fg);
220 grid-column: 1 / -1;
221 text-align: start;
222}
223/* accent-color rather than ::-webkit-progress-value and friends, and
224 * that is not a detail: styling those pseudo-elements replaces the
225 * browser's indeterminate rendering with a static bar, and the
226 * indeterminate state is the whole point of phase two — a bar that
227 * stops moving while the server works reads as broken.
228 */
229[rst-tray-bar] {
230 accent-color: var(--rst-accent, var(--rst-text));
231 block-size: 0.35rem;
232 grid-column: 1 / -1;
233 inline-size: 100%;
234}
235[rst-tray-status] { margin: 0.5rem 0 0; }
236/* Unhidden and empty from the moment the script runs, so it must cost no
237 * space until it says something — and the obvious "fix" for a stray
238 * half-line in the corner is to hide the region again, which is the bug
239 * this whole arrangement exists to avoid.
240 */
241[rst-tray-status]:empty { margin: 0; }
242[rst-tray-listed] { color: var(--rst-text-muted); margin: 0.4rem 0 0; }
243
244[rst-results] { list-style: none; margin: 0; padding: 0; }
245[rst-result] { border-top: 1px solid var(--rst-line); padding: 0.9rem 0; }
246[rst-result]:first-child { border-top: 0; }
247/* The document's ancestry in its source, above the title. Sits above
248 rather than below because it is read as a location — "Handbook ›
249 Onboarding ›" then the page — and colours come from tokens so light
250 and dark both resolve through theme.css. */
251[rst-result-path] {
252 /* --rst-text-muted, not --rst-text-mut. This rule was written by
253 * copying the trail rule above while it still carried that typo: an
254 * undefined token with no fallback makes the whole declaration invalid
255 * at computed-value time, so the path would have rendered as ordinary
256 * body text — muted in the markup, not muted on the screen. The merge
257 * brought the fix and the test that catches the class of it.
258 */
259 color: var(--rst-text-muted);
260 display: block;
261 font-size: 0.8125rem;
262 margin-bottom: 0.1rem;
263}
264/* Row titles — a result's, and a document's in the index table — are
265 * the row's subject, in the text colour like every list title in the
266 * suite, and underlined only on hover. The base link rule (estilo
267 * base.css) is zero-specificity, so these win without a fight; prose
268 * links (the fact box's citation, "Back to search") keep the accent and
269 * the underline, because colour alone must not carry "this is a link". */
270[rst-result-title],
271[rst-table] td > a:not([rst-btn]) {
272 color: var(--rst-text);
273 text-decoration: none;
274}
275[rst-result-title]:hover,
276[rst-table] td > a:not([rst-btn]):hover { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.15em; }
277[rst-result-title] { font-weight: 600; }
278[rst-table] td > a:not([rst-btn]) { font-weight: 550; }
279[rst-result-trail] {
280 /* The token is --rst-text-muted. This said --rst-text-mut, which
281 * theme.css does not define and which had no fallback, so the whole
282 * declaration was invalid at computed-value time — not "grey went
283 * wrong", but the colour rule silently absent, leaving the trail in
284 * ordinary body text: muted in the markup, not muted on the screen.
285 * TestEveryTokenReferencedHereIsDefinedOrHasAFallback now catches the
286 * whole class of that mistake.
287 */
288 color: var(--rst-text-muted);
289 display: block;
290 font-size: 0.8125rem;
291 margin-top: 0.1rem;
292}
293[rst-result-snippet] { margin: 0.4rem 0 0.2rem; }
294[rst-result-snippet] mark {
295 background: var(--rst-accent-soft, var(--rst-line));
296 color: inherit;
297 padding: 0 0.1em;
298}
299[rst-result-meta] { font-size: 0.8125rem; margin: 0; }
300
301/* The results on screen no longer describe what is in the box, because
302 the last live search did not run. Shown, not merely announced: the
303 status region is sr-only, so without this a sighted reader sees older
304 results and nothing saying they are older — the confident-zero failure
305 AGENTS.md names, in its politest form.
306
307 The sentence comes from the region's own rst-search-stale-note, which
308 the server wrote from the same {{define "searchFailed"}} as the
309 server-rendered callout. CSS renders the attribute so the words stay
310 in the template and no script ever composes a sentence. */
311[rst-search-stale]::before {
312 color: var(--rst-tone-negative-fg, var(--rst-text));
313 content: attr(rst-search-stale-note);
314 display: block;
315 font-size: 0.9375rem;
316 margin-bottom: 0.9rem;
317}
318
319/* The primed result: the one Enter goes to. Deliberately the same mark
320 as [rst-passage]:target on the document page, because it means the
321 same thing in both places — this is the one you are being sent to.
322 Set by the server, never by the script; see internal/memoria/query. */
323[rst-result-primed] {
324 border-inline-start: 3px solid var(--rst-accent, var(--rst-text));
325 padding-inline-start: 0.7rem;
326}
327[rst-result-primed]:first-child { border-top: 0; }
328/* Arrowing into the list moves real focus, so the browser's own ring is
329 the selection indicator. It is stated here only to give it room: the
330 default ring sits tight against 600-weight text and is easy to miss
331 when the eye is on the input. */
332[rst-result-title]:focus-visible {
333 outline: 2px solid var(--rst-accent, var(--rst-text));
334 outline-offset: 3px;
335}
336
337[rst-prose] { line-height: 1.6; }
338[rst-passage] { border-top: 1px solid var(--rst-line); padding: 0.8rem 0; }
339[rst-passage]:target { border-inline-start: 3px solid var(--rst-accent, var(--rst-text)); padding-inline-start: 0.7rem; }
340[rst-passage-heading] { font-size: 0.9375rem; margin: 0 0 0.4rem; text-transform: none; }
341[rst-passage] p { margin: 0; white-space: pre-wrap; }
342
343[rst-breadcrumb] { margin-bottom: 0.75rem; }
344
345[rst-table] { border-collapse: collapse; width: 100%; }
346[rst-table] th, [rst-table] td {
347 border-bottom: 1px solid var(--rst-line);
348 padding: 0.45rem 0.5rem 0.45rem 0;
349 text-align: start;
350}
351[rst-table] th { font-size: 0.8125rem; font-weight: 560; }
352
353/* 320px is the reflow floor the suite holds itself to. The search form
354 * stays one row: stacked, rastrillo's align-items: center shrank the input
355 * to a clipped 180px in the middle of the column. */
356@media (max-width: 26rem) {
357 [rst-table] { font-size: 0.875rem; }
358 /* The documents table keeps Title, Updated and Delete on a phone:
359 * with all five columns it ran 23px past a 320px screen. Source and
360 * Passages are on the document's own page. */
361 [rst-table] th:nth-child(2), [rst-table] td:nth-child(2),
362 [rst-table] th:nth-child(3), [rst-table] td:nth-child(3) { display: none; }
363}
364
365/* A zero-result line while the person is still typing (search.js marks
366 * the region pending until they pause): whole-token matching finds
367 * nothing for "rev" on the way to "revenue", and saying so on every
368 * keystroke is noise. Hidden, not removed, so the region's markup is the
369 * server's and a pause only has to lift the attribute. */
370[rst-search-pending] [rst-search-empty] { visibility: hidden; }
371[rst-search-empty] { margin: 0 0 1rem; }
372
373/* The Sources page's lists: rows with a rule between them. They were
374 * rastrillo's rounded rst-list card holding bare <li>s, so browser bullets
375 * sat inside the card with the text touching its edge. */
376[rst-sources] { border-top: 1px solid var(--rst-line); list-style: none; margin: 0.75rem 0 0; padding: 0; }
377[rst-sources] > li { border-bottom: 1px solid var(--rst-line); display: grid; gap: 0.35rem; padding: 0.75rem 0; }
378[rst-sources] > li > p { margin: 0; }
379[rst-source-name] { font-weight: 600; }
380
381/* The Notion import's figures: a two-column list, not the browser's
382 * indented one-per-line definition list. */
383[rst-facts] { display: grid; gap: 0.35rem 1rem; grid-template-columns: minmax(0, max-content) minmax(0, 1fr); margin: 0.75rem 0 0; }
384[rst-facts] dt { color: var(--rst-text-muted); }
385[rst-facts] dd { margin: 0; }
386@media (max-width: 26rem) {
387 [rst-facts] { grid-template-columns: minmax(0, 1fr); }
388 [rst-facts] dd { margin-bottom: 0.4rem; }
389}
390
391/* Delete, asked first (delete.html). */
392[rst-confirm] form { display: flex; flex-wrap: wrap; gap: var(--rst-sp-2); margin-top: 1.25rem; }
393
394/* §8.8's fact box: a validated fact lifted out of the passages below,
395 with its citation. Sits between the withholding notice and the result
396 list, and is styled to read as an answer rather than as a result —
397 the value is the thing the eye should land on. */
398[rst-fact-box] {
399 border: 1px solid var(--rst-line);
400 border-radius: var(--rst-radius);
401 padding: 0.7rem 0.9rem;
402 margin: 0 0 1rem;
403 background: var(--rst-surface);
404}
405[rst-fact] {
406 display: flex;
407 flex-wrap: wrap;
408 align-items: baseline;
409 gap: 0.2rem 0.6rem;
410 margin: 0;
411}
412[rst-fact] + [rst-fact],
413[rst-fact-note] + [rst-fact] {
414 margin-top: 0.5rem;
415 padding-top: 0.5rem;
416 border-top: 1px solid var(--rst-line);
417}
418[rst-fact-label] {
419 color: var(--rst-text-muted);
420 font-size: 0.8125rem;
421}
422[rst-fact-value] {
423 font-weight: 600;
424 font-size: 1.0625rem;
425}
426[rst-fact-cite] {
427 font-size: 0.8125rem;
428 margin-left: auto;
429}
430/* §8.8's narrowing line: which of the reader's words chose the rows
431 above, and how many candidates that left out. Quiet, under the rows. */
432[rst-fact-note] {
433 color: var(--rst-text-muted);
434 font-size: 0.8125rem;
435 margin: 0.5rem 0 0;
436}
437
438/* §8.9: a row a model pointed at, rather than one a checksum validated.
439 Distinct at a glance, and quieter — the value is still the document's
440 bytes, but the claim that it is an address is the model's. */
441[rst-fact-model] [rst-fact-value] { font-weight: 500; }
442[rst-fact-by] {
443 color: var(--rst-text-muted);
444 font-size: 0.75rem;
445}
446