@tiagojct/subsub 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  Sub-Sub follows the version numbers of npm: a change in the second number (0.13) adds features or changes how you use it; a change in the third (0.13.1) only fixes.
4
4
 
5
+ ## 0.20.0 (2026-10-23)
6
+
7
+ Changes from a technical evaluation of 0.19.0 and Starbuck 0.2.0 (LLMSSH 2026).
8
+
9
+ - Searching the library: when you ask what you have on a topic, Sub-Sub first searches with your own words, then with close synonyms and other word forms, and says which words it searched. In the evaluation it missed items whose titles use a synonym or repeat your words. In a test on the 20 evaluation tasks, recall rose from 0.87 to 0.96 and precision fell from 0.98 to 0.91: the wider search also lists some items that are only near the topic. Check the list.
10
+ - Drafts: Sub-Sub now works from quotes. It reads the full text of each item, chooses a quote for each sentence, and writes the sentence so that it says no more than the quote, with one source for each sentence. A table "Quotes for the check" follows the draft with the quote, word for word, and the page or section for each sentence. A sentence without a quote is cut. In the evaluation, 37% to 53% of the factual sentences in drafts were only partly supported or unsupported by the works they cited. In a test on 18 drafts by two language-model raters, the share fell to 12% to 14% with the new rule. A second test on nine drafts of three other tasks, with the same raters, gave 14% and 16% against 35% and 37% for the old rule; one of the three tasks showed no change. The drafts have more and shorter sentences. Read the table as a way to check, not as a guarantee.
11
+ - Reference checks: a Check with "not found by search" is reported as "not confirmed", with the note that books, theses, reports, archives and works in Portuguese are often missing from Crossref and PubMed. Claim verdicts are reported as sentences to check; Sub-Sub does not change the text or the citation because of a verdict.
12
+ - Guide pages (English and Portuguese): the same two notes on reference checks.
13
+ - Website: a Changes page (English; the Portuguese menu links to it) shows this file.
14
+ - Bench: `retrievalScore` scores a retrieval answer by precision and recall against gold items, naming items by citekey or by title.
15
+
16
+ ## 0.19.0 (2026-10-16)
17
+
18
+ A new look for the browser view, the website, the tutorial and the workshop, after the engraved plates of 19th-century natural history books.
19
+
20
+ - A grey-green paper, blue-black ink, and four colours with one meaning each: blue for the Researcher and for links, madder red for the Librarian, green for what is applied or checked, ochre for a preview. In dark mode the page is a blue-black plate.
21
+ - Type: Literata for text, with true italics; Atkinson Hyperlegible Next for the interface; Atkinson Hyperlegible Mono for commands. They replace IBM Plex.
22
+ - The notes pane shows what a note was written from: a line under `evidence:`, solid for the full text, dashed for the abstract, dotted for the metadata.
23
+ - Reading mode in the notes pane: larger type and more space between lines, for long notes. Sub-Sub remembers it on this computer. The tutorial has the same button.
24
+ - In a preview, a tag that would be removed is struck through instead of red. Errors show in ink, with a word, so that red belongs to the Librarian only. Each mode button keeps its own colour, selected or not.
25
+ - A preview has a dashed frame: nothing has changed yet.
26
+
5
27
  ## 0.18.0 (2026-10-15)
6
28
 
7
29
  - Figures and tables from the PDF. With the Zotero server 0.6.0, Sub-Sub can cut a figure or a table out of an item's PDF (`zotero_get_figure`, with the label of its caption, such as "Figure 2" or "Table 1") and show it in a note, with its caption and a link to the page. `/read critique` shows the table or figure with the main result. The images are PNG files in `Literature/figures/`; the notes pane and Obsidian show them. When the figure cannot be cut out, Sub-Sub takes the whole page and says so.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sub-Sub
2
2
 
3
- [![npm](https://img.shields.io/npm/v/@tiagojct/subsub.svg?style=flat-square&color=1d4f7c)](https://www.npmjs.com/package/@tiagojct/subsub)
3
+ [![npm](https://img.shields.io/npm/v/@tiagojct/subsub.svg?style=flat-square&color=2c5d87)](https://www.npmjs.com/package/@tiagojct/subsub)
4
4
  [![Website](https://img.shields.io/badge/website-subsub.tiagojacinto.eu-a33b14.svg?style=flat-square)](https://subsub.tiagojacinto.eu)
5
5
  [![Licence: MIT](https://img.shields.io/badge/licence-MIT-5f5c55.svg?style=flat-square)](LICENSE)
6
6
 
@@ -24,7 +24,7 @@ Each mode can have its own model. Switch with `/researcher` and `/librarian`, or
24
24
 
25
25
  Every library change works the same way in both modes:
26
26
 
27
- - The Zotero server computes the change first. Sub-Sub shows it to you (added tags in green, removed tags in red) and applies it only when you select Yes. The check is in the program, so the model cannot skip it.
27
+ - The Zotero server computes the change first. Sub-Sub shows it to you (the tags it adds and the tags it removes) and applies it only when you select Yes. The check is in the program, so the model cannot skip it.
28
28
  - If you edit an item in Zotero while the preview is open, Sub-Sub shows you the new preview before it changes anything. Each change also sends the item's version, so a field you edit at the moment of writing is not overwritten.
29
29
  - Every change is journaled, including new collections. `/undo` reverts the last one, and the browser view shows an Undo button under each change.
30
30
  - Nothing is deleted: items go to the Zotero trash. Only tags from your tag list can be added.
@@ -183,4 +183,4 @@ This DOI always points to the latest version; each version also has its own DOI
183
183
 
184
184
  ## Licence
185
185
 
186
- [MIT](LICENSE), Tiago Jacinto. The `/lit`, `/compare` and `/review` prompts adapt text from [Feynman](https://github.com/Companion-Inc/feynman) (MIT); see [THIRD_PARTY_NOTICES](THIRD_PARTY_NOTICES). IBM Plex fonts: SIL Open Font License.
186
+ [MIT](LICENSE), Tiago Jacinto. The `/lit`, `/compare` and `/review` prompts adapt text from [Feynman](https://github.com/Companion-Inc/feynman) (MIT); see [THIRD_PARTY_NOTICES](THIRD_PARTY_NOTICES). Literata and Atkinson Hyperlegible fonts: SIL Open Font License.
@@ -55,99 +55,101 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
55
55
  SOFTWARE.
56
56
 
57
57
 
58
- web/fonts/ and the website use IBM Plex Sans, IBM Plex Serif and IBM Plex Mono,
59
- https://github.com/IBM/plex. Their licence:
60
-
61
- Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
62
-
63
- This Font Software is licensed under the SIL Open Font License, Version 1.1.
64
-
65
- This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
66
-
67
-
68
- -----------------------------------------------------------
69
- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
70
- -----------------------------------------------------------
71
-
72
- PREAMBLE
73
- The goals of the Open Font License (OFL) are to stimulate worldwide
74
- development of collaborative font projects, to support the font creation
75
- efforts of academic and linguistic communities, and to provide a free and
76
- open framework in which fonts may be shared and improved in partnership
77
- with others.
78
-
79
- The OFL allows the licensed fonts to be used, studied, modified and
80
- redistributed freely as long as they are not sold by themselves. The
81
- fonts, including any derivative works, can be bundled, embedded,
82
- redistributed and/or sold with any software provided that any reserved
83
- names are not used by derivative works. The fonts and derivatives,
84
- however, cannot be released under any other type of license. The
85
- requirement for fonts to remain under this license does not apply
86
- to any document created using the fonts or their derivatives.
87
-
88
- DEFINITIONS
89
- "Font Software" refers to the set of files released by the Copyright
90
- Holder(s) under this license and clearly marked as such. This may
91
- include source files, build scripts and documentation.
92
-
93
- "Reserved Font Name" refers to any names specified as such after the
94
- copyright statement(s).
95
-
96
- "Original Version" refers to the collection of Font Software components as
97
- distributed by the Copyright Holder(s).
98
-
99
- "Modified Version" refers to any derivative made by adding to, deleting,
100
- or substituting -- in part or in whole -- any of the components of the
101
- Original Version, by changing formats or by porting the Font Software to a
102
- new environment.
103
-
104
- "Author" refers to any designer, engineer, programmer, technical
105
- writer or other person who contributed to the Font Software.
106
-
107
- PERMISSION & CONDITIONS
108
- Permission is hereby granted, free of charge, to any person obtaining
109
- a copy of the Font Software, to use, study, copy, merge, embed, modify,
110
- redistribute, and sell modified and unmodified copies of the Font
111
- Software, subject to the following conditions:
112
-
113
- 1) Neither the Font Software nor any of its individual components,
114
- in Original or Modified Versions, may be sold by itself.
115
-
116
- 2) Original or Modified Versions of the Font Software may be bundled,
117
- redistributed and/or sold with any software, provided that each copy
118
- contains the above copyright notice and this license. These can be
119
- included either as stand-alone text files, human-readable headers or
120
- in the appropriate machine-readable metadata fields within text or
121
- binary files as long as those fields can be easily viewed by the user.
122
-
123
- 3) No Modified Version of the Font Software may use the Reserved Font
124
- Name(s) unless explicit written permission is granted by the corresponding
125
- Copyright Holder. This restriction only applies to the primary font name as
126
- presented to the users.
127
-
128
- 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
129
- Software shall not be used to promote, endorse or advertise any
130
- Modified Version, except to acknowledge the contribution(s) of the
131
- Copyright Holder(s) and the Author(s) or with their explicit written
132
- permission.
133
-
134
- 5) The Font Software, modified or unmodified, in part or in whole,
135
- must be distributed entirely under this license, and must not be
136
- distributed under any other license. The requirement for fonts to
137
- remain under this license does not apply to any document created
138
- using the Font Software.
139
-
140
- TERMINATION
141
- This license becomes null and void if any of the above conditions are
142
- not met.
143
-
144
- DISCLAIMER
145
- THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
146
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
147
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
148
- OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
149
- COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
150
- INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
151
- DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
152
- FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
153
- OTHER DEALINGS IN THE FONT SOFTWARE.
58
+ web/fonts/, the website, the tutorial and the workshop use Literata, Atkinson
59
+ Hyperlegible Next and Atkinson Hyperlegible Mono, subset to Latin. Their licence:
60
+
61
+ Copyright 2017 The Literata Project Authors (https://github.com/googlefonts/literata)
62
+ Copyright 2020-2024 The Atkinson Hyperlegible Next Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next)
63
+ Copyright 2020-2024 The Atkinson Hyperlegible Mono Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next-mono)
64
+
65
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
66
+
67
+ This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
68
+
69
+
70
+ -----------------------------------------------------------
71
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
72
+ -----------------------------------------------------------
73
+
74
+ PREAMBLE
75
+ The goals of the Open Font License (OFL) are to stimulate worldwide
76
+ development of collaborative font projects, to support the font creation
77
+ efforts of academic and linguistic communities, and to provide a free and
78
+ open framework in which fonts may be shared and improved in partnership
79
+ with others.
80
+
81
+ The OFL allows the licensed fonts to be used, studied, modified and
82
+ redistributed freely as long as they are not sold by themselves. The
83
+ fonts, including any derivative works, can be bundled, embedded,
84
+ redistributed and/or sold with any software provided that any reserved
85
+ names are not used by derivative works. The fonts and derivatives,
86
+ however, cannot be released under any other type of license. The
87
+ requirement for fonts to remain under this license does not apply
88
+ to any document created using the fonts or their derivatives.
89
+
90
+ DEFINITIONS
91
+ "Font Software" refers to the set of files released by the Copyright
92
+ Holder(s) under this license and clearly marked as such. This may
93
+ include source files, build scripts and documentation.
94
+
95
+ "Reserved Font Name" refers to any names specified as such after the
96
+ copyright statement(s).
97
+
98
+ "Original Version" refers to the collection of Font Software components as
99
+ distributed by the Copyright Holder(s).
100
+
101
+ "Modified Version" refers to any derivative made by adding to, deleting,
102
+ or substituting -- in part or in whole -- any of the components of the
103
+ Original Version, by changing formats or by porting the Font Software to a
104
+ new environment.
105
+
106
+ "Author" refers to any designer, engineer, programmer, technical
107
+ writer or other person who contributed to the Font Software.
108
+
109
+ PERMISSION & CONDITIONS
110
+ Permission is hereby granted, free of charge, to any person obtaining
111
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
112
+ redistribute, and sell modified and unmodified copies of the Font
113
+ Software, subject to the following conditions:
114
+
115
+ 1) Neither the Font Software nor any of its individual components,
116
+ in Original or Modified Versions, may be sold by itself.
117
+
118
+ 2) Original or Modified Versions of the Font Software may be bundled,
119
+ redistributed and/or sold with any software, provided that each copy
120
+ contains the above copyright notice and this license. These can be
121
+ included either as stand-alone text files, human-readable headers or
122
+ in the appropriate machine-readable metadata fields within text or
123
+ binary files as long as those fields can be easily viewed by the user.
124
+
125
+ 3) No Modified Version of the Font Software may use the Reserved Font
126
+ Name(s) unless explicit written permission is granted by the corresponding
127
+ Copyright Holder. This restriction only applies to the primary font name as
128
+ presented to the users.
129
+
130
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
131
+ Software shall not be used to promote, endorse or advertise any
132
+ Modified Version, except to acknowledge the contribution(s) of the
133
+ Copyright Holder(s) and the Author(s) or with their explicit written
134
+ permission.
135
+
136
+ 5) The Font Software, modified or unmodified, in part or in whole,
137
+ must be distributed entirely under this license, and must not be
138
+ distributed under any other license. The requirement for fonts to
139
+ remain under this license does not apply to any document created
140
+ using the Font Software.
141
+
142
+ TERMINATION
143
+ This license becomes null and void if any of the above conditions are
144
+ not met.
145
+
146
+ DISCLAIMER
147
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
148
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
149
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
150
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
151
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
152
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
153
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
154
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
155
+ OTHER DEALINGS IN THE FONT SOFTWARE.
package/dist/bench.js CHANGED
@@ -212,6 +212,37 @@ export function prf(pred, gold) {
212
212
  const fn = gold.size - tp;
213
213
  return { tp, fp, fn };
214
214
  }
215
+ function plainTitle(t) {
216
+ return t.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
217
+ }
218
+ /**
219
+ * The library items that an answer names. An item is named by its citekey, or by the first 40 characters of its title
220
+ * (at least 15). A duplicate copy (citekey ending in -dup) counts as its original.
221
+ */
222
+ export function itemsNamed(text, library) {
223
+ const plain = plainTitle(text);
224
+ const found = new Set();
225
+ for (const item of library) {
226
+ const key = item.citekey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
227
+ const title = plainTitle(item.title).slice(0, 40).trim();
228
+ if (new RegExp(`(?<![A-Za-z0-9_])${key}(?![A-Za-z0-9_])`).test(text) || (title.length >= 15 && plain.includes(title))) {
229
+ found.add(item.citekey.replace(/-dup$/, ""));
230
+ }
231
+ }
232
+ return found;
233
+ }
234
+ /**
235
+ * Precision and recall of the items in a retrieval answer against the gold items. Items in `acceptable` are on the topic but
236
+ * outside the gold list: they count as neither hits nor errors. "(copy)" and "-dup" mark a duplicate and count as the original.
237
+ */
238
+ export function retrievalScore(text, gold, acceptable, library) {
239
+ const original = (k) => k.replace(/\s*\(copy\)\s*$/, "").replace(/-dup$/, "");
240
+ const goldSet = new Set(gold.map(original));
241
+ const skip = new Set(acceptable.map(original));
242
+ const said = new Set([...itemsNamed(text, library)].filter((k) => goldSet.has(k) || !skip.has(k)));
243
+ const { tp, fp, fn } = prf(said, goldSet);
244
+ return { ...f1(tp, fp, fn), tp, fp, fn };
245
+ }
215
246
  function f1(tp, fp, fn) {
216
247
  const p = tp + fp ? tp / (tp + fp) : 0;
217
248
  const r = tp + fn ? tp / (tp + fn) : 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiagojct/subsub",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Sub-Sub: a research assistant that works from your own Zotero library, in the browser or the terminal, built on pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -8,7 +8,7 @@ Make a short slug: lower case, hyphens, at most five words. Use today's date (YY
8
8
 
9
9
  1. Find the sources.
10
10
  - If the user gave citekeys, DOIs or PMIDs, use exactly those.
11
- - Otherwise: library first (zotero_find_items), then scholar_search_multi with three phrasings. Choose 3 to 10 sources and say why.
11
+ - Otherwise: library first (zotero_find_items, with the user's own words and then close synonyms), then scholar_search_multi with three phrasings. Choose 3 to 10 sources and say why.
12
12
  2. Read each source before you describe it: zotero_get_fulltext for library items, scholar_read_oa_fulltext (methods and results) for open-access works, scholar_get_work for the abstract of the others. Note whether you read the full text or only the abstract.
13
13
  3. Build the matrix, one row per source:
14
14
  | # | Source | Claim on the question | Evidence type | Population and setting | Caveats | Confidence |
package/prompts/lit.md CHANGED
@@ -18,7 +18,7 @@ Make a short slug from the topic: lower case, hyphens, at most five words. Use t
18
18
 
19
19
  ## 2. Gather
20
20
 
21
- 1. Library first: zotero_find_items for the topic (and the topic tags). Note what the library already has.
21
+ 1. Library first: zotero_find_items for the topic, with the user's own words first and then with close synonyms and word forms, one call for each (and the topic tags). Note what the library already has.
22
22
  2. Outside: scholar_search_multi with three or four phrasings of the question (synonyms, the test or drug name, the condition name, MeSH terms). Give include_terms (the key concepts) and exclude_terms (populations or designs outside the question) so that the server pre-screens the results. Use year limits when the question needs recent work.
23
23
  3. Screen. For every work in the merged list, decide: read, maybe, or exclude, with a reason of a few words. Prefer systematic reviews, guidelines and primary studies with the right design. Write the screening log to `Research/.plans/<slug>-screening.md`: a table (#, work, identifier, pre-screen, decision, reason) and the counts for a flow diagram: found by the searches, unique works, pre-screen excluded, screened, read, used.
24
24
  4. Read before you summarise:
package/prompts/verify.md CHANGED
@@ -7,12 +7,12 @@ Check the references of $1 with Starbuck.
7
7
  1. If the verify_ tools are not available, tell the user that reference checks need Starbuck: add "addons": ["starbuck"] to the Sub-Sub settings file and start Sub-Sub again. Then stop.
8
8
  2. Call verify_check_manuscript with path $1.
9
9
  3. Report the summary: how many references failed, need checking, have notes and passed.
10
- 4. For each Fail and each Check, give the citation key, Starbuck's reason and its suggested action. Keep Starbuck's result; do not make it stronger or weaker.
10
+ 4. For each Fail and each Check, give the citation key, Starbuck's reason and its suggested action. Keep Starbuck's result; do not make it stronger or weaker. When the reason is that no record was found, write "not confirmed" and say that books, theses, reports, archives and works in Portuguese are often missing from Crossref and PubMed.
11
11
  5. List the cited keys without a bibliography entry.
12
12
  6. Give the path of the HTML report.
13
13
  7. If the second argument is "claims", or the user asks whether the sources support the text:
14
14
  1. Call verify_prepare_claims with path $1. Get all pages (offset, limit).
15
15
  2. Judge each claim with status ready from its passages only: supported, partly_supported, not_supported or cannot_assess. Copy the passage you rely on character for character into quote.
16
16
  3. Call verify_record_claims once, with the verdicts of all claims.
17
- 4. Report the claims that are not supported or only partly supported, with the line and the reason. Say for each whether the evidence was the full text or the abstract only.
17
+ 4. Report the claims that are not supported or only partly supported as sentences to check, not as errors, with the line and the reason. Say for each whether the evidence was the full text or the abstract only.
18
18
  8. Do not change the manuscript. Offer fixes and wait for the user's answer.
@@ -30,7 +30,7 @@ You read the Zotero library (zotero_ tools), search PubMed, Europe PMC and OpenA
30
30
 
31
31
  ## Questions about the library
32
32
 
33
- - "What do I have on X": zotero_find_items (query, tags, fulltext=true when needed). Answer in the chat: citekey, year, one line per item.
33
+ - "What do I have on X": search with zotero_find_items (query, tags, fulltext=true when needed). First search with {{user}}'s own words, as written. Then search again with close synonyms and other word forms (singular and plural, noun and adjective, spelling variants, an older or foreign name), one call for each. Join the results and list each item once. Answer in the chat: citekey, year, one line per item, and the words you searched.
34
34
 
35
35
  ## Outside search and gaps
36
36
 
@@ -103,10 +103,12 @@ If zotero_get_vocabulary has no topic/ tags, propose topics before tagging: read
103
103
  2. Propose fixes for missing keys. Do not edit the manuscript text without approval.
104
104
  3. scholar_export_bibliography to the file named in the YAML header, or references.json next to the manuscript.
105
105
 
106
- Draft text for {{user}}'s manuscripts or theses only when {{user}} asks. Cite library items as [@citekey], use only claims from sources you read, and mark drafted text clearly so that {{user}} can check it. Never draft text for an assignment (coursework that {{user}} hands in for a grade), even when asked: say so, and offer /read, quotes and questions instead.
106
+ Draft text for {{user}}'s manuscripts or theses only when {{user}} asks. Cite library items as [@citekey], use only claims from sources you read, and mark drafted text clearly so that {{user}} can check it. Work from quotes. Read the full text (zotero_get_fulltext) of each item you will cite. First choose the quotes: for each sentence of the draft, one passage of one source, word for word, with its page or section. Then write each sentence so that it says no more than its quote says: one source for each sentence, no sentence that joins claims from two sources, no figure, date, name or cause that is not in the quote, and no stronger wording than the quote has. End every factual sentence (a fact, date, number, name, cause or comparison) with [@citekey]. If you have only the abstract or the metadata of an item, say so, and use it for no more than it says. After the draft, give a table "Quotes for the check" with one row for each factual sentence: its number, the citekey, the quote, and the page or section. A sentence that you cannot support with a quote is cut. Never draft text for an assignment (coursework that {{user}} hands in for a grade), even when asked: say so, and offer /read, quotes and questions instead.
107
107
 
108
108
  ## Reference checks (Starbuck, when the verify_ tools are available)
109
109
 
110
110
  - verify_check_manuscript checks that each cited work exists, matches its citation and was not retracted. Use it on {{user}}'s manuscripts and on your own reports before you deliver them.
111
111
  - Report Starbuck's results as Starbuck states them. Do not turn a Check into a Fail, or a Pass into "verified". Give the path of the report.
112
+ - When Starbuck found no record for a reference (a Check with "not found by search"), say "not confirmed", not "wrong". The work may exist: books, theses, reports, archives and works in Portuguese are often not in Crossref or PubMed. Suggest a library catalogue or the publisher's page.
113
+ - Report claim verdicts as sentences for {{user}} to check. Do not change the text or the citation because of a verdict. For a verdict of not supported, write "not supported by the passages I saw" and give the passage.
112
114
  - Claim support (level 4) only when {{user}} asks: verify_prepare_claims, then judge each claim from its passages only, then verify_record_claims with all verdicts. Quote the passage you rely on character for character. With an abstract only, answer cannot_assess for details an abstract would not report.
package/src/bench.ts CHANGED
@@ -262,6 +262,46 @@ export function prf(pred: Set<string>, gold: Set<string>) {
262
262
  return { tp, fp, fn };
263
263
  }
264
264
 
265
+ /** A library item as the retrieval score needs it: the citekey and the title. */
266
+ export interface LibraryItem {
267
+ citekey: string;
268
+ title: string;
269
+ }
270
+
271
+ function plainTitle(t: string): string {
272
+ return t.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
273
+ }
274
+
275
+ /**
276
+ * The library items that an answer names. An item is named by its citekey, or by the first 40 characters of its title
277
+ * (at least 15). A duplicate copy (citekey ending in -dup) counts as its original.
278
+ */
279
+ export function itemsNamed(text: string, library: LibraryItem[]): Set<string> {
280
+ const plain = plainTitle(text);
281
+ const found = new Set<string>();
282
+ for (const item of library) {
283
+ const key = item.citekey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
284
+ const title = plainTitle(item.title).slice(0, 40).trim();
285
+ if (new RegExp(`(?<![A-Za-z0-9_])${key}(?![A-Za-z0-9_])`).test(text) || (title.length >= 15 && plain.includes(title))) {
286
+ found.add(item.citekey.replace(/-dup$/, ""));
287
+ }
288
+ }
289
+ return found;
290
+ }
291
+
292
+ /**
293
+ * Precision and recall of the items in a retrieval answer against the gold items. Items in `acceptable` are on the topic but
294
+ * outside the gold list: they count as neither hits nor errors. "(copy)" and "-dup" mark a duplicate and count as the original.
295
+ */
296
+ export function retrievalScore(text: string, gold: string[], acceptable: string[], library: LibraryItem[]) {
297
+ const original = (k: string) => k.replace(/\s*\(copy\)\s*$/, "").replace(/-dup$/, "");
298
+ const goldSet = new Set(gold.map(original));
299
+ const skip = new Set(acceptable.map(original));
300
+ const said = new Set([...itemsNamed(text, library)].filter((k) => goldSet.has(k) || !skip.has(k)));
301
+ const { tp, fp, fn } = prf(said, goldSet);
302
+ return { ...f1(tp, fp, fn), tp, fp, fn };
303
+ }
304
+
265
305
  function f1(tp: number, fp: number, fn: number) {
266
306
  const p = tp + fp ? tp / (tp + fp) : 0;
267
307
  const r = tp + fn ? tp / (tp + fn) : 0;
package/web/app.css CHANGED
@@ -1,93 +1,97 @@
1
- /* Sub-Sub web view.
2
- Type: IBM Plex Sans for the interface, IBM Plex Serif for replies, IBM Plex Mono for commands.
3
- The fonts are local: the page's CSP allows no other origin. */
1
+ /* Sub-Sub web view: the plate, at work.
2
+ A grey-green sheet, a lighter field, copperplate ink, and four washes as the only colour:
3
+ Prussian blue (the Researcher), madder (the Librarian), sap green (applied), gamboge (a preview).
4
+ Type: Atkinson Hyperlegible Next for the interface, Literata for replies and notes (true italics),
5
+ Atkinson Hyperlegible Mono for commands. The fonts are local: the page's CSP allows no other origin. */
4
6
 
5
- @font-face { font-family: "IBM Plex Sans"; src: url("/fonts/IBMPlexSans.woff2") format("woff2"); font-weight: 100 700; font-stretch: 75% 100%; font-display: swap; }
6
- @font-face { font-family: "IBM Plex Serif"; src: url("/fonts/IBMPlexSerif.woff2") format("woff2"); font-weight: 100 700; font-display: swap; }
7
- @font-face { font-family: "IBM Plex Mono"; src: url("/fonts/IBMPlexMono.woff2") format("woff2"); font-weight: 400; font-display: swap; }
7
+ @font-face { font-family: "Literata"; src: url("/fonts/Literata.woff2") format("woff2"); font-weight: 400 700; font-style: normal; font-display: swap; }
8
+ @font-face { font-family: "Literata"; src: url("/fonts/Literata-Italic.woff2") format("woff2"); font-weight: 400 700; font-style: italic; font-display: swap; }
9
+ @font-face { font-family: "Atkinson Hyperlegible Next"; src: url("/fonts/AtkinsonHyperlegibleNext.woff2") format("woff2"); font-weight: 400 700; font-display: swap; }
10
+ @font-face { font-family: "Atkinson Hyperlegible Mono"; src: url("/fonts/AtkinsonHyperlegibleMono.woff2") format("woff2"); font-weight: 400 600; font-display: swap; }
8
11
 
9
12
  /* ---------------------------------------------------------------- tokens */
10
13
 
11
14
  :root {
12
- --sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
13
- --serif: "IBM Plex Serif", Georgia, serif;
14
- --mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
15
-
16
- --bg: #f7f6f2;
17
- --surface: #efede7;
18
- --raised: #ffffff;
19
- --text: #1c1b19;
20
- --muted: #5f5c55;
21
- --faint: #8a867d;
22
- --rule: #e0ddd4;
23
- --rule-strong: #c8c3b8;
24
- --shadow: 0 1px 2px rgb(28 27 25 / 0.05), 0 8px 24px -12px rgb(28 27 25 / 0.18);
25
-
26
- --researcher: #1d4f7c;
27
- --researcher-soft: #e3ebf3;
28
- --librarian: #a33b14;
29
- --librarian-soft: #f5e6de;
30
- --on-accent: #ffffff;
31
-
32
- --add: #256b3b;
33
- --add-soft: #e2f0e5;
34
- --rem: #a8321d;
35
- --rem-soft: #f7e3de;
36
- --warn: #946412;
37
-
38
- --radius: 6px;
39
- --radius-lg: 10px;
15
+ --sans: "Atkinson Hyperlegible Next", system-ui, -apple-system, "Segoe UI", sans-serif;
16
+ --serif: "Literata", Georgia, serif;
17
+ --mono: "Atkinson Hyperlegible Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
18
+
19
+ --bg: #e9ece6;
20
+ --surface: #dfe3dc;
21
+ --raised: #f4f5f1;
22
+ --text: #16202a;
23
+ --muted: #44505b;
24
+ --faint: #5d6870;
25
+ --rule: #c6ccc6;
26
+ --rule-strong: #7d878e;
27
+
28
+ --researcher: #2c5d87;
29
+ --researcher-soft: #d8e2eb;
30
+ --librarian: #a8432d;
31
+ --librarian-soft: #f0ddd6;
32
+ --on-accent: #f4f5f1;
33
+
34
+ --add: #4f6a2e;
35
+ --add-soft: #dfe7d2;
36
+ --rem: #a8432d;
37
+ --rem-soft: #f0ddd6;
38
+ --warn: #86601a;
39
+ --warn-soft: #f2e5c6;
40
+
41
+ --radius: 2px;
42
+ --radius-lg: 3px;
40
43
  --side-w: 17rem;
41
44
  --measure: 46rem;
42
45
  }
43
46
 
47
+ /* Dark: the steel plate. */
44
48
  @media (prefers-color-scheme: dark) {
45
49
  :root:not([data-theme="light"]) {
46
- --bg: #141413;
47
- --surface: #1c1c1a;
48
- --raised: #232320;
49
- --text: #ebe8e1;
50
- --muted: #a7a399;
51
- --faint: #7b776f;
52
- --rule: #2e2d2a;
53
- --rule-strong: #45433e;
54
- --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 12px 32px -12px rgb(0 0 0 / 0.6);
55
- --researcher: #7fb0de;
56
- --researcher-soft: #1b2733;
57
- --librarian: #e48a5f;
58
- --librarian-soft: #33221a;
59
- --on-accent: #141413;
60
- --add: #7cc493;
61
- --add-soft: #1a2c20;
62
- --rem: #ec8a70;
63
- --rem-soft: #36201b;
64
- --warn: #d6a84e;
50
+ --bg: #12171c;
51
+ --surface: #0d1115;
52
+ --raised: #192128;
53
+ --text: #dce1de;
54
+ --muted: #a6b0b3;
55
+ --faint: #8d989d;
56
+ --rule: #2c363f;
57
+ --rule-strong: #66727c;
58
+ --researcher: #86b4da;
59
+ --researcher-soft: #1e3042;
60
+ --librarian: #e6866d;
61
+ --librarian-soft: #3d2620;
62
+ --on-accent: #12171c;
63
+ --add: #a3c078;
64
+ --add-soft: #253220;
65
+ --rem: #e6866d;
66
+ --rem-soft: #3d2620;
67
+ --warn: #e2b45a;
68
+ --warn-soft: #3a3019;
65
69
  }
66
70
  }
67
71
 
68
72
  :root[data-theme="dark"] {
69
- --bg: #141413;
70
- --surface: #1c1c1a;
71
- --raised: #232320;
72
- --text: #ebe8e1;
73
- --muted: #a7a399;
74
- --faint: #7b776f;
75
- --rule: #2e2d2a;
76
- --rule-strong: #45433e;
77
- --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 12px 32px -12px rgb(0 0 0 / 0.6);
78
- --researcher: #7fb0de;
79
- --researcher-soft: #1b2733;
80
- --librarian: #e48a5f;
81
- --librarian-soft: #33221a;
82
- --on-accent: #141413;
83
- --add: #7cc493;
84
- --add-soft: #1a2c20;
85
- --rem: #ec8a70;
86
- --rem-soft: #36201b;
87
- --warn: #d6a84e;
88
- }
89
-
90
- /* The mode sets the accent: blue for the Researcher, rust for the Librarian. */
73
+ --bg: #12171c;
74
+ --surface: #0d1115;
75
+ --raised: #192128;
76
+ --text: #dce1de;
77
+ --muted: #a6b0b3;
78
+ --faint: #8d989d;
79
+ --rule: #2c363f;
80
+ --rule-strong: #66727c;
81
+ --researcher: #86b4da;
82
+ --researcher-soft: #1e3042;
83
+ --librarian: #e6866d;
84
+ --librarian-soft: #3d2620;
85
+ --on-accent: #12171c;
86
+ --add: #a3c078;
87
+ --add-soft: #253220;
88
+ --rem: #e6866d;
89
+ --rem-soft: #3d2620;
90
+ --warn: #e2b45a;
91
+ --warn-soft: #3a3019;
92
+ }
93
+
94
+ /* The mode sets the accent: Prussian blue for the Researcher, madder for the Librarian. */
91
95
  body { --accent: var(--researcher); --accent-soft: var(--researcher-soft); }
92
96
  body[data-mode="librarian"] { --accent: var(--librarian); --accent-soft: var(--librarian-soft); }
93
97
 
@@ -106,6 +110,8 @@ body {
106
110
  -webkit-font-smoothing: antialiased;
107
111
  -moz-osx-font-smoothing: grayscale;
108
112
  text-rendering: optimizeLegibility;
113
+ caret-color: var(--accent);
114
+ scrollbar-color: var(--rule-strong) transparent;
109
115
  }
110
116
 
111
117
  button, input, select, textarea { font: inherit; color: inherit; }
@@ -116,7 +122,7 @@ svg { flex: none; }
116
122
  .visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
117
123
  [hidden] { display: none !important; }
118
124
  :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
119
- ::selection { background: var(--accent-soft); }
125
+ ::selection { background: var(--accent-soft); color: var(--text); }
120
126
 
121
127
  /* Icons drawn with strokes, at the size of the text. */
122
128
  .icon-btn svg, .new-btn svg, .brand .mark, .icon {
@@ -135,7 +141,7 @@ button { cursor: pointer; background: none; border: 1px solid transparent; borde
135
141
  button:disabled { opacity: 0.45; cursor: default; }
136
142
 
137
143
  .primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 550; }
138
- .primary:hover:not(:disabled) { filter: brightness(1.08); }
144
+ .primary:hover:not(:disabled) { background: var(--text); border-color: var(--text); color: var(--bg); }
139
145
  .secondary { background: var(--raised); border-color: var(--rule-strong); }
140
146
  .secondary:hover:not(:disabled) { border-color: var(--text); }
141
147
  .ghost { color: var(--muted); }
@@ -215,9 +221,9 @@ select:hover { border-color: var(--rule-strong); }
215
221
  display: flex;
216
222
  align-items: center;
217
223
  gap: 0.45rem;
224
+ font-family: var(--serif);
218
225
  font-weight: 600;
219
- font-size: 1.02rem;
220
- letter-spacing: -0.01em;
226
+ font-size: 1.1rem;
221
227
  margin-right: 0.25rem;
222
228
  }
223
229
  .brand .mark { color: var(--accent); }
@@ -225,24 +231,25 @@ select:hover { border-color: var(--rule-strong); }
225
231
 
226
232
  .modes {
227
233
  display: inline-flex;
228
- padding: 3px;
229
- border-radius: 8px;
230
- background: var(--surface);
231
- border: 1px solid var(--rule);
234
+ border: 1px solid var(--rule-strong);
235
+ background: var(--raised);
232
236
  }
233
237
  .modes button {
234
- padding: 0.28rem 0.8rem;
235
- border-radius: 5px;
238
+ padding: 0.3rem 0.8rem;
239
+ border-radius: 0;
236
240
  font-size: 0.86rem;
237
241
  color: var(--muted);
238
242
  border: 0;
239
243
  }
240
244
  .modes button:hover { color: var(--text); }
245
+ /* Each mode keeps its own colour, chosen or not. */
246
+ .modes button[data-mode="researcher"] { color: var(--researcher); }
247
+ .modes button[data-mode="librarian"] { color: var(--librarian); }
241
248
  .modes button[aria-pressed="true"] {
242
- background: var(--raised);
249
+ background: var(--accent-soft);
243
250
  color: var(--accent);
244
251
  font-weight: 600;
245
- box-shadow: 0 1px 2px rgb(0 0 0 / 0.08), 0 0 0 1px var(--rule);
252
+ box-shadow: inset 0 -2px 0 var(--accent);
246
253
  }
247
254
 
248
255
  .status {
@@ -258,7 +265,6 @@ select:hover { border-color: var(--rule-strong); }
258
265
  align-items: center;
259
266
  gap: 0.45rem;
260
267
  padding: 0.3rem 0.7rem;
261
- border-radius: 999px;
262
268
  font-size: 0.8rem;
263
269
  color: var(--muted);
264
270
  white-space: nowrap;
@@ -268,9 +274,10 @@ select:hover { border-color: var(--rule-strong); }
268
274
  font-variant-numeric: tabular-nums;
269
275
  }
270
276
  .library-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--rule-strong); flex: none; }
271
- .library.reachable .library-dot { background: var(--add); box-shadow: 0 0 0 3px var(--add-soft); }
272
- .library.down { color: var(--rem); background: var(--rem-soft); font-weight: 550; }
273
- .library.down .library-dot { background: var(--rem); }
277
+ .library.reachable .library-dot { background: var(--add); }
278
+ /* Errors are drawn in ink, with a hollow mark and a word: madder means only the Librarian. */
279
+ .library.down { color: var(--text); background: var(--surface); font-weight: 600; }
280
+ .library.down .library-dot { background: none; border: 1.5px solid var(--text); }
274
281
  .library.checking .library-dot { background: var(--warn); animation: pulse 1.2s ease-in-out infinite; }
275
282
 
276
283
  /* ---------------------------------------------------------------- side panel */
@@ -289,11 +296,12 @@ select:hover { border-color: var(--rule-strong); }
289
296
  }
290
297
 
291
298
  .side h2 {
292
- font-size: 0.7rem;
293
- font-weight: 600;
299
+ font-family: var(--serif);
300
+ font-size: 0.98rem;
301
+ font-weight: 400;
302
+ font-variant-caps: all-small-caps;
294
303
  letter-spacing: 0.08em;
295
- text-transform: uppercase;
296
- color: var(--faint);
304
+ color: var(--muted);
297
305
  margin: 0 0 0.35rem;
298
306
  padding: 0 0.5rem;
299
307
  }
@@ -306,10 +314,9 @@ select:hover { border-color: var(--rule-strong); }
306
314
  padding: 0.5rem 0.7rem;
307
315
  background: var(--raised);
308
316
  border: 1px solid var(--rule);
309
- font-weight: 550;
310
- box-shadow: 0 1px 1px rgb(0 0 0 / 0.03);
317
+ font-weight: 600;
311
318
  }
312
- .new-btn:hover { border-color: var(--rule-strong); }
319
+ .new-btn:hover { border-color: var(--text); }
313
320
  .new-btn svg { width: 16px; height: 16px; color: var(--accent); }
314
321
 
315
322
  .actions { display: flex; flex-direction: column; }
@@ -318,10 +325,10 @@ select:hover { border-color: var(--rule-strong); }
318
325
  padding: 0.32rem 0.5rem;
319
326
  font-size: 0.88rem;
320
327
  color: var(--text);
321
- border-radius: 5px;
328
+ border-radius: 0;
322
329
  }
323
330
  .actions button:hover { background: var(--bg); }
324
- .actions .group { margin: 0.55rem 0 0.15rem; padding: 0 0.5rem; font-size: 0.74rem; font-weight: 600; color: var(--muted); }
331
+ .actions .group { margin: 0.6rem 0 0.15rem; padding: 0 0.5rem; font-family: var(--serif); font-style: italic; font-size: 0.86rem; color: var(--muted); }
325
332
  .actions .group:first-child { margin-top: 0; }
326
333
 
327
334
  .side .grow { flex: 1; display: flex; flex-direction: column; min-height: 8rem; }
@@ -345,10 +352,10 @@ select:hover { border-color: var(--rule-strong); }
345
352
  text-align: left;
346
353
  padding: 0.42rem 0.5rem;
347
354
  display: block;
348
- border-radius: 5px;
355
+ border-radius: 0;
349
356
  }
350
357
  .sessions button:hover { background: var(--bg); }
351
- .sessions .current button { background: var(--raised); box-shadow: inset 2px 0 0 var(--accent), 0 0 0 1px var(--rule); }
358
+ .sessions .current button { background: var(--raised); outline: 1px solid var(--rule-strong); outline-offset: -1px; }
352
359
  .sessions .what {
353
360
  display: -webkit-box;
354
361
  -webkit-line-clamp: 2;
@@ -384,7 +391,7 @@ select:hover { border-color: var(--rule-strong); }
384
391
  flex: none;
385
392
  width: 28px;
386
393
  height: 16px;
387
- border-radius: 999px;
394
+ border-radius: var(--radius);
388
395
  background: var(--rule-strong);
389
396
  transition: background-color 0.15s ease;
390
397
  }
@@ -395,9 +402,8 @@ select:hover { border-color: var(--rule-strong); }
395
402
  left: 2px;
396
403
  width: 12px;
397
404
  height: 12px;
398
- border-radius: 50%;
399
- background: #fff;
400
- box-shadow: 0 1px 2px rgb(0 0 0 / 0.25);
405
+ border-radius: 1px;
406
+ background: var(--raised);
401
407
  transition: transform 0.15s ease;
402
408
  }
403
409
  .switch input:checked + .track { background: var(--accent); }
@@ -416,7 +422,7 @@ body.layout-full { --measure: 72rem; }
416
422
  margin: 0.9rem auto 0;
417
423
  padding: 0.6rem 0.6rem 0.6rem 0.9rem;
418
424
  border: 1px solid var(--rule);
419
- border-left: 3px solid var(--warn);
425
+ border-top: 2px solid var(--warn);
420
426
  border-radius: var(--radius);
421
427
  background: var(--raised);
422
428
  display: flex;
@@ -441,7 +447,7 @@ body.layout-full { --measure: 72rem; }
441
447
  .msg.user .bubble {
442
448
  background: var(--raised);
443
449
  border: 1px solid var(--rule);
444
- border-radius: var(--radius-lg) var(--radius-lg) 3px var(--radius-lg);
450
+ border-radius: var(--radius-lg);
445
451
  padding: 0.6rem 0.95rem;
446
452
  max-width: min(36rem, 88%);
447
453
  white-space: pre-wrap;
@@ -456,6 +462,7 @@ body.layout-full { --measure: 72rem; }
456
462
  font-family: var(--serif);
457
463
  font-size: 1.02rem;
458
464
  line-height: 1.68;
465
+ font-variant-numeric: oldstyle-nums proportional-nums;
459
466
  overflow-wrap: anywhere;
460
467
  }
461
468
  .md > :first-child { margin-top: 0; }
@@ -464,9 +471,8 @@ body.layout-full { --measure: 72rem; }
464
471
  .md li { margin: 0.2rem 0; }
465
472
  .md li::marker { color: var(--faint); }
466
473
  .md h1, .md h2, .md h3, .md h4 {
467
- font-family: var(--sans);
474
+ font-family: var(--serif);
468
475
  line-height: 1.3;
469
- letter-spacing: -0.01em;
470
476
  margin: 1.6rem 0 0.5rem;
471
477
  font-weight: 600;
472
478
  }
@@ -484,14 +490,17 @@ body.layout-full { --measure: 72rem; }
484
490
  padding: 0.1rem 0 0.1rem 1rem;
485
491
  border-left: 2px solid var(--accent);
486
492
  color: var(--muted);
493
+ font-style: italic;
487
494
  }
495
+ .md blockquote code, .md blockquote a { font-style: normal; }
496
+ /* A page link stays whole: (p. 12) never breaks after "p.". */
497
+ .md a[href^="zotero://open-pdf"] { white-space: nowrap; }
488
498
 
489
499
  .md code {
490
500
  font-family: var(--mono);
491
501
  font-size: 0.84em;
492
502
  background: var(--surface);
493
- border: 1px solid var(--rule);
494
- border-radius: 4px;
503
+ border-radius: var(--radius);
495
504
  padding: 0.05em 0.32em;
496
505
  }
497
506
 
@@ -530,9 +539,8 @@ body.layout-full { --measure: 72rem; }
530
539
  .md-table-wrap {
531
540
  overflow-x: auto;
532
541
  margin: 1.1rem 0;
533
- border: 1px solid var(--rule);
534
- border-radius: var(--radius);
535
- background: var(--raised);
542
+ border-top: 1.5px solid var(--text);
543
+ border-bottom: 1.5px solid var(--text);
536
544
  }
537
545
  .md table {
538
546
  border-collapse: collapse;
@@ -540,18 +548,19 @@ body.layout-full { --measure: 72rem; }
540
548
  font-family: var(--sans);
541
549
  font-size: 0.86rem;
542
550
  line-height: 1.45;
543
- font-variant-numeric: tabular-nums;
551
+ font-variant-numeric: lining-nums tabular-nums;
544
552
  }
545
553
  .md th, .md td { padding: 0.5rem 0.75rem; text-align: left; vertical-align: top; border-bottom: 1px solid var(--rule); }
546
554
  .md tr:last-child td { border-bottom: 0; }
547
555
  .md th {
548
- font-size: 0.72rem;
549
- font-weight: 600;
550
- letter-spacing: 0.04em;
551
- text-transform: uppercase;
556
+ font-family: var(--serif);
557
+ font-size: 0.98rem;
558
+ font-weight: 400;
559
+ font-variant-caps: all-small-caps;
560
+ letter-spacing: 0.07em;
552
561
  color: var(--muted);
553
562
  white-space: nowrap;
554
- background: var(--surface);
563
+ border-bottom-color: var(--text);
555
564
  }
556
565
  .md td:first-child { white-space: nowrap; }
557
566
  .md td code { white-space: nowrap; }
@@ -576,7 +585,7 @@ body.layout-full { --measure: 72rem; }
576
585
  align-items: baseline;
577
586
  padding: 0.28rem 0.55rem;
578
587
  margin-left: -0.55rem;
579
- border-radius: 5px;
588
+ border-radius: 0;
580
589
  width: fit-content;
581
590
  max-width: calc(100% + 0.55rem);
582
591
  }
@@ -594,8 +603,8 @@ body.layout-full { --measure: 72rem; }
594
603
  .tool .label { color: var(--text); white-space: nowrap; }
595
604
  .tool.running summary::before { background: var(--accent); animation: pulse 1.1s ease-in-out infinite; }
596
605
  .tool.done summary::before { background: var(--add); }
597
- .tool.error summary::before { background: var(--rem); }
598
- .tool.error .label { color: var(--rem); }
606
+ .tool.error summary::before { background: none; border: 1.5px solid var(--text); }
607
+ .tool.error .label { color: var(--text); font-weight: 600; }
599
608
  .tool .args { font-family: var(--mono); font-size: 0.74rem; color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
600
609
  .tool[open] .args { white-space: normal; overflow-wrap: anywhere; }
601
610
  .tool pre {
@@ -631,8 +640,8 @@ body.layout-full { --measure: 72rem; }
631
640
  white-space: pre-wrap;
632
641
  overflow-wrap: anywhere;
633
642
  }
634
- .note.warning { border-left: 3px solid var(--warn); }
635
- .note.error { border-left: 3px solid var(--rem); }
643
+ .note.warning { border-top: 2px solid var(--warn); }
644
+ .note.error { border-top: 2px solid var(--text); }
636
645
  .note.mono { font-family: var(--mono); font-size: 0.76rem; line-height: 1.55; }
637
646
  .note pre { font-family: var(--mono); font-size: 0.74rem; white-space: pre-wrap; }
638
647
  .note button { margin-top: 0.5rem; }
@@ -640,7 +649,7 @@ body.layout-full { --measure: 72rem; }
640
649
  .note details { margin-top: 0.4rem; color: var(--muted); }
641
650
  .note details summary { cursor: pointer; font-size: 0.78rem; }
642
651
  .note details pre { margin: 0.3rem 0 0; overflow-wrap: anywhere; }
643
- .free-note { border-left: 3px solid var(--accent); padding-left: 0.6rem; }
652
+ .free-note { border-top: 1px solid var(--accent); padding-top: 0.4rem; }
644
653
 
645
654
  .working {
646
655
  margin-top: 1rem;
@@ -665,13 +674,18 @@ body.layout-full { --measure: 72rem; }
665
674
  /* Empty conversation. */
666
675
  .empty { padding-top: 11vh; }
667
676
  .empty .kicker {
668
- font-size: 0.72rem;
669
- font-weight: 600;
677
+ display: flex;
678
+ justify-content: space-between;
679
+ margin: 0 0 1.5rem;
680
+ padding-bottom: 0.3rem;
681
+ border-bottom: 1px solid var(--text);
682
+ font-family: var(--serif);
683
+ font-size: 1rem;
684
+ font-variant-caps: all-small-caps;
670
685
  letter-spacing: 0.08em;
671
- text-transform: uppercase;
672
686
  color: var(--accent);
673
- margin: 0 0 0.6rem;
674
687
  }
688
+ .empty .kicker::before { content: "Sub-Sub"; color: var(--muted); order: -1; }
675
689
  .empty h1 {
676
690
  font-family: var(--serif);
677
691
  font-weight: 500;
@@ -681,17 +695,18 @@ body.layout-full { --measure: 72rem; }
681
695
  margin: 0 0 0.6rem;
682
696
  }
683
697
  .empty .lede { color: var(--muted); max-width: 34rem; margin: 0 0 1.75rem; }
684
- .chips { display: grid; gap: 0.5rem; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); }
698
+ /* Suggestions: a short list of lines to start from, not a row of cards. */
699
+ .chips { display: grid; max-width: 34rem; border-bottom: 1px solid var(--rule-strong); }
685
700
  .chips button {
686
701
  text-align: left;
687
- border: 1px solid var(--rule);
688
- border-radius: var(--radius-lg);
689
- background: var(--raised);
690
- padding: 0.75rem 0.9rem;
691
- font-size: 0.9rem;
702
+ border: 0;
703
+ border-top: 1px solid var(--rule-strong);
704
+ border-radius: 0;
705
+ padding: 0.6rem 0.25rem;
706
+ font-size: 0.95rem;
692
707
  line-height: 1.4;
693
708
  }
694
- .chips button:hover { border-color: var(--accent); }
709
+ .chips button:hover { background: var(--accent-soft); color: var(--accent); }
695
710
 
696
711
  body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0; margin: 0; }
697
712
 
@@ -702,12 +717,11 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
702
717
  width: min(100%, var(--measure));
703
718
  margin-inline: auto;
704
719
  border: 1px solid var(--rule-strong);
705
- border-radius: 12px;
720
+ border-radius: var(--radius-lg);
706
721
  background: var(--raised);
707
- box-shadow: var(--shadow);
708
722
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
709
723
  }
710
- .composer-box:focus-within { border-color: var(--accent); box-shadow: var(--shadow), 0 0 0 3px var(--accent-soft); }
724
+ .composer-box:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
711
725
  .composer textarea {
712
726
  display: block;
713
727
  width: 100%;
@@ -737,11 +751,10 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
737
751
  background: var(--raised);
738
752
  border: 1px solid var(--rule-strong);
739
753
  border-radius: var(--radius-lg);
740
- box-shadow: var(--shadow);
741
754
  padding: 0.3rem;
742
755
  z-index: 5;
743
756
  }
744
- .suggest button, .suggest .hint-row { display: flex; align-items: baseline; gap: 0.6rem; width: 100%; text-align: left; padding: 0.38rem 0.55rem; border-radius: 5px; }
757
+ .suggest button, .suggest .hint-row { display: flex; align-items: baseline; gap: 0.6rem; width: 100%; text-align: left; padding: 0.38rem 0.55rem; border-radius: 0; }
745
758
  .suggest button[aria-selected="true"], .suggest button:hover { background: var(--accent-soft); }
746
759
  .suggest .name { font-family: var(--mono); font-size: 0.8rem; color: var(--accent); white-space: nowrap; }
747
760
  .suggest .desc { color: var(--muted); font-size: 0.82rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -749,32 +762,38 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
749
762
  /* ---------------------------------------------------------------- dialogs */
750
763
 
751
764
  .modal {
752
- border: 1px solid var(--rule-strong);
753
- border-radius: 12px;
765
+ border: 1px solid var(--text);
766
+ border-radius: var(--radius-lg);
754
767
  background: var(--raised);
755
768
  color: var(--text);
756
769
  padding: 1.4rem 1.5rem 1.25rem;
757
770
  width: min(40rem, calc(100vw - 2rem));
758
771
  max-height: calc(100dvh - 2rem);
759
772
  overflow-y: auto;
760
- box-shadow: 0 24px 60px -16px rgb(0 0 0 / 0.45);
761
773
  }
762
774
  .modal.wide { width: min(44rem, calc(100vw - 2rem)); }
763
- .modal::backdrop { background: rgb(20 20 19 / 0.45); backdrop-filter: blur(2px); }
775
+ .modal::backdrop { background: rgb(18 23 28 / 0.5); }
764
776
  .modal[open] { animation: rise 0.16s ease-out; }
765
777
  @keyframes rise { from { opacity: 0; transform: translateY(6px); } }
766
778
 
779
+ /* A preview is gamboge, and drawn dashed: nothing has happened yet. Its label is the dialog's
780
+ running head, with "Sub-Sub" opposite, as on the website's plates. */
767
781
  .modal-kicker {
768
- margin: 0 0 0.3rem;
769
- font-size: 0.7rem;
770
- font-weight: 600;
782
+ display: flex;
783
+ justify-content: space-between;
784
+ margin: 0 0 0.9rem;
785
+ padding-bottom: 0.25rem;
786
+ border-bottom: 1px dashed var(--warn);
787
+ font-family: var(--serif);
788
+ font-size: 1rem;
789
+ font-variant-caps: all-small-caps;
771
790
  letter-spacing: 0.08em;
772
- text-transform: uppercase;
773
- color: var(--accent);
791
+ color: var(--warn);
774
792
  }
793
+ .modal-kicker::after { content: "Sub-Sub"; color: var(--muted); }
775
794
  .modal-kicker:empty { display: none; }
776
- .modal h2 { font-size: 1.2rem; font-weight: 600; letter-spacing: -0.01em; line-height: 1.3; margin: 0 0 0.3rem; overflow-wrap: anywhere; }
777
- .modal h3 { font-size: 0.72rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint); margin: 1.4rem 0 0.5rem; }
795
+ .modal h2 { font-family: var(--serif); font-size: 1.25rem; font-weight: 600; line-height: 1.3; margin: 0 0 0.3rem; overflow-wrap: anywhere; }
796
+ .modal h3 { font-family: var(--serif); font-size: 0.98rem; font-weight: 400; font-variant-caps: all-small-caps; letter-spacing: 0.08em; color: var(--muted); margin: 1.4rem 0 0.5rem; }
778
797
  .modal .hint { color: var(--muted); margin: 0 0 1rem; font-size: 0.88rem; }
779
798
 
780
799
  .preview {
@@ -784,16 +803,17 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
784
803
  white-space: pre-wrap;
785
804
  overflow-wrap: anywhere;
786
805
  background: var(--bg);
787
- border: 1px solid var(--rule);
788
- border-radius: var(--radius);
806
+ border: 1px dashed var(--warn);
807
+ border-radius: 0;
789
808
  padding: 0.8rem 0.95rem;
790
809
  max-height: 55vh;
791
810
  overflow-y: auto;
792
811
  margin: 0 0 1.1rem;
793
812
  }
794
- .preview .add, .preview .rem { border-radius: 3px; padding: 0.1em 0.3em; }
813
+ .preview .add, .preview .rem { padding: 0.1em 0.3em; }
795
814
  .preview .add { color: var(--add); background: var(--add-soft); }
796
- .preview .rem { color: var(--rem); background: var(--rem-soft); }
815
+ .preview .rem { color: var(--muted); background: none; text-decoration: line-through; text-decoration-thickness: 1px; }
816
+ /* A removal is struck through: madder stays the Librarian's colour. */
797
817
 
798
818
  .modal-buttons { display: flex; justify-content: flex-end; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.75rem; }
799
819
  .modal-buttons button { min-width: 5.5rem; }
@@ -846,7 +866,7 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
846
866
  .models button { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.6rem; }
847
867
  .models .needs-key { opacity: 0.65; }
848
868
  .models .model-name { min-width: 0; overflow-wrap: anywhere; }
849
- .models .model-tag { font-family: var(--sans); font-size: 0.72rem; font-weight: 600; color: var(--accent); border: 1px solid currentColor; border-radius: 999px; padding: 0 0.45rem; }
869
+ .models .model-tag { font-family: var(--sans); font-size: 0.72rem; font-weight: 600; color: var(--accent); border: 1px solid currentColor; padding: 0 0.45rem; }
850
870
  .models .model-why { font-family: var(--sans); font-size: 0.75rem; color: var(--muted); }
851
871
 
852
872
  /* ---------------------------------------------------------------- narrow screens */
@@ -873,7 +893,7 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
873
893
 
874
894
  .items { list-style: none; margin: 0; padding: 0; max-height: 18rem; overflow-y: auto; }
875
895
  .items:empty { display: none; }
876
- .items > li { padding: 0.4rem 0.5rem; border-radius: 5px; }
896
+ .items > li { padding: 0.4rem 0.5rem; }
877
897
  .items > li:hover { background: var(--bg); }
878
898
  .items > li.muted { font-size: 0.82rem; }
879
899
  .item-title { font-size: 0.86rem; line-height: 1.35; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
@@ -883,7 +903,7 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
883
903
  .item-actions button:hover, .item-actions a:hover { text-decoration: underline; text-underline-offset: 0.18em; }
884
904
 
885
905
  .sessions li { position: relative; }
886
- .session-tools { position: absolute; top: 0.3rem; right: 0.3rem; display: none; gap: 0.3rem; background: var(--raised); border-radius: 4px; }
906
+ .session-tools { position: absolute; top: 0.3rem; right: 0.3rem; display: none; gap: 0.3rem; background: var(--raised); }
887
907
  .sessions li:hover .session-tools, .sessions li:focus-within .session-tools { display: flex; }
888
908
  /* No hover on a touch screen: the buttons stay under the conversation. */
889
909
  @media (hover: none) { .session-tools { display: flex; position: static; justify-content: flex-end; background: none; } }
@@ -902,14 +922,26 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
902
922
  }
903
923
  .note-pane[hidden] { display: none; }
904
924
  .note-head { display: flex; align-items: center; gap: 0.6rem; padding: 0.6rem 0.9rem; border-bottom: 1px solid var(--rule); }
905
- .note-name { flex: 1; min-width: 0; font-size: 0.84rem; font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
925
+ .note-name { flex: 1; min-width: 0; font-family: var(--mono); font-size: 0.8rem; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
906
926
  .note-pane > .search { margin: 0.6rem 0.9rem 0; width: auto; }
907
927
  .note-body { flex: 1; overflow-y: auto; padding: 0.9rem 1.1rem 1.5rem; }
908
928
  .note-body .front { font-size: 0.74rem; color: var(--muted); margin: 0 0 1rem; }
929
+ /* What a note was written from, drawn by line: solid for the full text, dashed for the abstract,
930
+ dotted for the metadata. The same lines as on the website's Plate II. */
931
+ .evidence { margin: 0 0 0.6rem; font-family: var(--mono); font-size: 0.8rem; color: var(--muted); }
932
+ .evidence span { color: var(--text); padding-bottom: 2px; border-bottom: 2px solid var(--rule-strong); }
933
+ .evidence.ev-full span { border-bottom-style: solid; border-color: var(--add); }
934
+ .evidence.ev-abstract span { border-bottom-style: dashed; border-color: var(--warn); }
935
+ .evidence.ev-meta span { border-bottom-style: dotted; border-color: var(--muted); }
936
+ /* Reading mode: larger type, wider lines and spacing, a shorter measure. */
937
+ .note-pane.reading .note-body { padding: 1.4rem 1.6rem 2.5rem; }
938
+ .note-pane.reading .note-body .md { font-size: 1.2rem; line-height: 1.9; word-spacing: 0.06em; max-width: 36rem; }
939
+ .note-pane.reading .note-body .md p, .note-pane.reading .note-body .md li { margin-block: 0.9rem; }
940
+ #note-read[aria-pressed="true"] { color: var(--accent); text-decoration: underline; }
909
941
  .note-body .front summary { cursor: pointer; }
910
942
  .note-body .front pre { background: var(--bg); border: 1px solid var(--rule); border-radius: var(--radius); padding: 0.5rem 0.7rem; margin: 0.4rem 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }
911
943
  .note-list { list-style: none; margin: 0; padding: 0; }
912
- .note-list button { width: 100%; text-align: left; padding: 0.4rem 0.5rem; border-radius: 5px; display: grid; gap: 0.1rem; }
944
+ .note-list button { width: 100%; text-align: left; padding: 0.4rem 0.5rem; border-radius: 0; display: grid; gap: 0.1rem; }
913
945
  .note-list button:hover { background: var(--bg); }
914
946
  .note-list .rel { font-size: 0.84rem; overflow-wrap: anywhere; }
915
947
  .note-list .when { font-size: 0.72rem; color: var(--faint); }
@@ -930,7 +962,7 @@ a[data-note] { color: var(--accent); text-decoration: underline; text-decoration
930
962
  width: min(19rem, 86vw);
931
963
  transform: translateX(-100%);
932
964
  transition: transform 0.2s ease;
933
- box-shadow: var(--shadow);
965
+ border-right: 1px solid var(--text);
934
966
  }
935
967
  .side.open { transform: none; }
936
968
  .app.note-open, .app.note-open.side-collapsed { grid-template-columns: minmax(0, 1fr); }
@@ -955,12 +987,12 @@ a[data-note] { color: var(--accent); text-decoration: underline; text-decoration
955
987
  }
956
988
 
957
989
  /* First run: what is still missing */
958
- .setup { margin-block: 1.4rem 1rem; padding: 0.9rem 1rem; border: 1px solid var(--rule); border-left: 3px solid var(--accent); border-radius: 6px; background: var(--raised); text-align: left; max-width: 40rem; }
959
- .setup h2 { margin: 0 0 0.5rem; font-size: 0.95rem; }
990
+ .setup { margin-block: 1.4rem 1rem; padding: 0.9rem 1rem; border: 1px solid var(--rule-strong); border-top: 2px solid var(--accent); background: var(--raised); text-align: left; max-width: 40rem; }
991
+ .setup h2 { margin: 0 0 0.5rem; font-family: var(--serif); font-size: 1.02rem; }
960
992
  .setup ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.55rem; }
961
993
  .setup li { display: grid; grid-template-columns: 4.5rem 1fr auto; gap: 0.6rem; align-items: start; font-size: 0.88rem; line-height: 1.45; }
962
994
  .setup li > * { min-width: 0; }
963
- .setup .mark { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; padding-top: 0.15rem; color: var(--muted); }
964
- .setup li.todo .mark { color: var(--rem); }
995
+ .setup .mark { font-family: var(--serif); font-size: 0.95rem; font-variant-caps: all-small-caps; letter-spacing: 0.07em; color: var(--muted); }
996
+ .setup li.todo .mark { color: var(--warn); }
965
997
  .setup li.ok .step-text { color: var(--muted); }
966
998
  @media (max-width: 560px) { .setup li { grid-template-columns: 1fr; gap: 0.2rem; } }
package/web/app.js CHANGED
@@ -429,6 +429,7 @@ async function showNoteList() {
429
429
  $("note-name").textContent = t("notes");
430
430
  $("note-back").hidden = true;
431
431
  $("note-app").hidden = true;
432
+ $("note-read").hidden = true;
432
433
  $("note-filter").hidden = false;
433
434
  showPane(true);
434
435
  try {
@@ -461,7 +462,15 @@ async function openNote(path) {
461
462
  noteDir = "";
462
463
  attachCopyButtons(content);
463
464
  attachTableButtons(content, data.name.replace(/\.md$/i, ""));
464
- $("note-body").replaceChildren(front ? el("details", { class: "front" }, el("summary", { text: t("properties") }), el("pre", { text: front[1] })) : "", content);
465
+ // The evidence line: what the note was written from, drawn solid, dashed or dotted.
466
+ const evidence = front && /^evidence:\s*(.+?)\s*$/m.exec(front[1])?.[1];
467
+ const kind = evidence && (/full/i.test(evidence) ? "ev-full" : /abstract/i.test(evidence) ? "ev-abstract" : /metadata/i.test(evidence) ? "ev-meta" : "");
468
+ $("note-body").replaceChildren(
469
+ evidence ? el("p", { class: `evidence ${kind}` }, "evidence: ", el("span", { text: evidence })) : "",
470
+ front ? el("details", { class: "front" }, el("summary", { text: t("properties") }), el("pre", { text: front[1] })) : "",
471
+ content,
472
+ );
473
+ $("note-read").hidden = false;
465
474
  $("note-body").scrollTop = 0;
466
475
  showPane(true);
467
476
  }
@@ -1513,6 +1522,14 @@ $("library-search").addEventListener("input", searchLibrary);
1513
1522
  $("notes-btn").addEventListener("click", () => ($("note-pane").hidden ? showNoteList() : showPane(false)));
1514
1523
  $("note-back").addEventListener("click", showNoteList);
1515
1524
  $("note-close").addEventListener("click", () => showPane(false));
1525
+ // Reading mode for notes: larger type and wider spacing, remembered on this computer.
1526
+ function setReading(on) {
1527
+ $("note-pane").classList.toggle("reading", on);
1528
+ $("note-read").setAttribute("aria-pressed", String(on));
1529
+ localStorage.setItem("subsub-reading", String(on));
1530
+ }
1531
+ $("note-read").addEventListener("click", () => setReading(!$("note-pane").classList.contains("reading")));
1532
+ setReading(localStorage.getItem("subsub-reading") === "true");
1516
1533
  $("note-app").addEventListener("click", () => noteShown && api("/api/open", { path: noteShown.path }).catch((err) => note(err.message, "warning")));
1517
1534
  $("note-filter").addEventListener("input", renderNoteList);
1518
1535
  $("note-pane").addEventListener("keydown", (e) => {
Binary file
Binary file
package/web/i18n.js CHANGED
@@ -109,6 +109,8 @@ export const STRINGS = {
109
109
  notesHint: "The notes in your Sub-Sub folder",
110
110
  allNotes: "All notes",
111
111
  properties: "Properties",
112
+ readingMode: "Reading mode",
113
+ readingModeHint: "Larger type and wider line spacing for the note",
112
114
  downloadCsv: "Download CSV",
113
115
  filterNotes: "Filter notes",
114
116
  noNotes: "No notes yet.",
@@ -249,6 +251,8 @@ export const STRINGS = {
249
251
  notesHint: "As notas da sua pasta do Sub-Sub",
250
252
  allNotes: "Todas as notas",
251
253
  properties: "Propriedades",
254
+ readingMode: "Modo de leitura",
255
+ readingModeHint: "Letra maior e mais espaço entre linhas na nota",
252
256
  downloadCsv: "Transferir CSV",
253
257
  filterNotes: "Filtrar notas",
254
258
  noNotes: "Ainda não há notas.",
package/web/icon.svg CHANGED
@@ -1,6 +1,6 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
- <rect width="32" height="32" rx="7" fill="#1d4f7c"/>
3
- <rect x="5.5" y="8" width="21" height="16" rx="1.5" fill="#f7f6f2"/>
4
- <path d="M9 12.5h8M9 16h14" stroke="#1d4f7c" stroke-width="1.6" stroke-linecap="round"/>
5
- <circle cx="16" cy="20.3" r="1.5" fill="#1d4f7c"/>
2
+ <rect width="32" height="32" rx="7" fill="#2c5d87"/>
3
+ <rect x="5.5" y="8" width="21" height="16" rx="1.5" fill="#f4f5f1"/>
4
+ <path d="M9 12.5h8M9 16h14" stroke="#2c5d87" stroke-width="1.6" stroke-linecap="round"/>
5
+ <circle cx="16" cy="20.3" r="1.5" fill="#2c5d87"/>
6
6
  </svg>
package/web/index.html CHANGED
@@ -6,7 +6,7 @@
6
6
  <meta name="color-scheme" content="light dark">
7
7
  <title>Sub-Sub</title>
8
8
  <link rel="icon" href="/icon.svg" type="image/svg+xml">
9
- <link rel="preload" href="/fonts/IBMPlexSans.woff2" as="font" type="font/woff2" crossorigin>
9
+ <link rel="preload" href="/fonts/AtkinsonHyperlegibleNext.woff2" as="font" type="font/woff2" crossorigin>
10
10
  <link rel="stylesheet" href="/app.css">
11
11
  <script type="module" src="/app.js"></script>
12
12
  </head>
@@ -86,6 +86,7 @@
86
86
  <div class="note-head">
87
87
  <button type="button" class="link-btn" id="note-back" data-i18n="allNotes">All notes</button>
88
88
  <span class="note-name" id="note-name"></span>
89
+ <button type="button" class="link-btn" id="note-read" aria-pressed="false" data-i18n="readingMode" data-i18n-title="readingModeHint" hidden>Reading mode</button>
89
90
  <button type="button" class="link-btn" id="note-app" data-i18n="openInApp" hidden>Open in app</button>
90
91
  <button type="button" class="link-btn" id="note-close" data-i18n="close">Close</button>
91
92
  </div>
Binary file
Binary file
Binary file