{{ p }}
{% endfor %} + {% for paragraph in narrative.paragraphs %} ++ {% for sentence in paragraph -%} + {{ sentence.text.en }}{% if not loop.last %} {% endif -%} + {% endfor %} +
+ {% endfor %}{{ p }}
{% endfor %} + {% for paragraph in narrative.paragraphs %} ++ {% for sentence in paragraph -%} + {{ sentence.text.ru }}{% if not loop.last %} {% endif -%} + {% endfor %} +
+ {% endfor %}{{ p }}
{% endfor %} + {% for paragraph in narrative.paragraphs %} ++ {% for sentence in paragraph -%} + {{ sentence.text.de }}{% if not loop.last %} {% endif -%} + {% endfor %} +
+ {% endfor %}@@ -383,6 +484,26 @@ setLang(langs[(idx + 1) % langs.length]); }); + // ── Notes (audit) toggle (ADR-014 sentence-level provenance) ── + // Mirrors the editorial template's toggle vocabulary so a creator + // who flips notes on in one style sees the audit UI everywhere. + // Preference persists in localStorage. + var NOTES_KEY = 'trailstory.notes'; + var notesBtn = document.getElementById('notesBtn'); + function setNotes(on) { + document.body.classList.toggle('audit', !!on); + if (notesBtn) notesBtn.setAttribute('aria-pressed', on ? 'true' : 'false'); + try { localStorage.setItem(NOTES_KEY, on ? '1' : '0'); } catch (e) {} + } + if (notesBtn) { + var notesStored = null; + try { notesStored = localStorage.getItem(NOTES_KEY); } catch (e) {} + if (notesStored === '1') setNotes(true); + notesBtn.addEventListener('click', function () { + setNotes(!document.body.classList.contains('audit')); + }); + } + var titleEn = {{ narrative.title.en | tojson }}; var quoteEn = {{ narrative.pull_quote.en | tojson }}; var msg = titleEn + ' — ' + quoteEn; diff --git a/templates/styles/log.html.j2 b/templates/styles/log.html.j2 index 2ea81c3..17e68ac 100644 --- a/templates/styles/log.html.j2 +++ b/templates/styles/log.html.j2 @@ -79,6 +79,84 @@ body.lang-ru .en, body.lang-ru .de { display: none; } body.lang-de .en, body.lang-de .ru { display: none; } + /* ADR-014 / Phase 4: sentence-level provenance. + The log register can't carry editorial's always-on treatment + without looking busy, so provenance is opt-in here via a "Notes" + toggle next to the language switcher. The vocabulary (body.audit, + #notesBtn, data-tip, localStorage key) mirrors editorial.html.j2 + so a creator who turns notes on in one style sees the same audit + UI everywhere. + + When body.audit is set: + - INFERRED sentences gain a faint highlighter background + - Per-source bottom-edge underline colors mark photo / seed / + gpx provenance (matches editorial) + - Hovering any sentence surfaces a custom ::after tooltip + reading data-tip (the native title attribute is slow, + low-contrast, and absent on touch — same fix as editorial) + When unset, the sentence spans render as plain text. */ + .sent { + transition: background-color 120ms ease, box-shadow 120ms ease; + } + body.audit .sent { position: relative; } + body.audit .sent[data-prov="inferred"] { + background: #fff8d4; + border-radius: 2px; + padding: 0 2px; + } + body.audit .sent[data-prov="photo"] { box-shadow: inset 0 -2px 0 oklch(0.78 0.10 220 / 0.55); } + body.audit .sent[data-prov="seed"] { box-shadow: inset 0 -2px 0 oklch(0.78 0.10 150 / 0.55); } + body.audit .sent[data-prov="gpx"] { box-shadow: inset 0 -2px 0 oklch(0.78 0.06 60 / 0.55); } + body.audit .sent:hover { + background: #ffeb9c; + cursor: help; + } + body.audit .sent:hover::after { + content: attr(data-tip); + position: absolute; + bottom: calc(100% + 4px); left: 0; + background: #1f2328; color: #f4f5f7; + padding: 6px 8px; + font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; + font-size: 11px; + letter-spacing: 0.02em; line-height: 1.35; + max-width: 280px; white-space: normal; + border-radius: 2px; + z-index: 30; pointer-events: none; + box-shadow: 0 4px 10px rgba(0,0,0,0.18); + } + @media print { + body.audit .sent[data-prov] { background: transparent; box-shadow: none; } + } + + /* ── notes (audit) toggle ──────────────────────────────────────── */ + .article-tools { + display: flex; + justify-content: flex-end; + margin: 0 0 0.5rem; + } + .article-tools button { + font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; + font-size: 0.7rem; + letter-spacing: 0.06em; + text-transform: uppercase; + background: transparent; + color: #57606a; + border: 1px solid #d0d7de; + padding: 0.25rem 0.6rem; + cursor: pointer; + border-radius: 3px; + } + .article-tools button:hover { + border-color: #1f2328; + color: #1f2328; + } + .article-tools button[aria-pressed="true"] { + background: #1f2328; + color: #f4f5f7; + border-color: #1f2328; + } + /* stats — list, not a grid; aligned key/value pairs */ .stats { list-style: none; @@ -236,20 +314,52 @@ L 100,30"/> + {# Reading tools — Notes (audit) toggle. Defaults to hidden so the + log reads as plain prose; the author flips body.audit on to see + the per-sentence provenance treatment. Same vocabulary as the + editorial template so a creator's preference travels. #} ++ ++- {# ADR-014 / Phase 4: paragraphs now carry per-sentence provenance. - The log style uses the flat-text fallback (no sentence spans / - hover UI yet — that's the editorial template's job; Phase 4.1 - will port log + encyclopedia to render sentence-level provenance - in this style's idiom). #} + {# ADR-014 / Phase 4: sentence-level provenance. + Each sentence becomes a so the rendered HTML + carries the grounding info. The log register hides the + treatment by default — `body.audit` opts in (see the + article-tools button above). Tooltips render via the custom + ::after data-tip pattern, not the native title attribute. #} - {% for p in flat_paragraphs.en %}{{ p }}
{% endfor %} + {% for paragraph in narrative.paragraphs %} ++ {% for sentence in paragraph -%} + {{ sentence.text.en }}{% if not loop.last %} {% endif -%} + {% endfor %} +
+ {% endfor %}- {% for p in flat_paragraphs.ru %}{{ p }}
{% endfor %} + {% for paragraph in narrative.paragraphs %} ++ {% for sentence in paragraph -%} + {{ sentence.text.ru }}{% if not loop.last %} {% endif -%} + {% endfor %} +
+ {% endfor %}- {% for p in flat_paragraphs.de %}{{ p }}
{% endfor %} + {% for paragraph in narrative.paragraphs %} ++ {% for sentence in paragraph -%} + {{ sentence.text.de }}{% if not loop.last %} {% endif -%} + {% endfor %} +
+ {% endfor %}@@ -329,6 +439,26 @@ setLang(langs[(idx + 1) % langs.length]); }); + // ── Notes (audit) toggle (ADR-014 sentence-level provenance) ── + // Mirrors the editorial template's toggle vocabulary so a creator + // who flips notes on in one style sees the audit UI everywhere. + // Preference persists in localStorage. + var NOTES_KEY = 'trailstory.notes'; + var notesBtn = document.getElementById('notesBtn'); + function setNotes(on) { + document.body.classList.toggle('audit', !!on); + if (notesBtn) notesBtn.setAttribute('aria-pressed', on ? 'true' : 'false'); + try { localStorage.setItem(NOTES_KEY, on ? '1' : '0'); } catch (e) {} + } + if (notesBtn) { + var notesStored = null; + try { notesStored = localStorage.getItem(NOTES_KEY); } catch (e) {} + if (notesStored === '1') setNotes(true); + notesBtn.addEventListener('click', function () { + setNotes(!document.body.classList.contains('audit')); + }); + } + var titleEn = {{ narrative.title.en | tojson }}; var quoteEn = {{ narrative.pull_quote.en | tojson }}; var msg = titleEn + ' — ' + quoteEn; diff --git a/tests/golden/test-render-editorial.html b/tests/golden/test-render-editorial.html index 2d09177..7bd229e 100644 --- a/tests/golden/test-render-editorial.html +++ b/tests/golden/test-render-editorial.html @@ -560,7 +560,10 @@Ein Morgen über dem Wolkenmeer + Written from notes, photographs, and a recorded path. + Написано по заметкам, фотографиям и записанному треку. + Aus Notizen, Fotos und einem aufgezeichneten Pfad geschrieben. + BAVARIAN ALPS · 2025-08-15
diff --git a/tests/golden/test-render-encyclopedia.html b/tests/golden/test-render-encyclopedia.html index 7f7a489..5d6bbf0 100644 --- a/tests/golden/test-render-encyclopedia.html +++ b/tests/golden/test-render-encyclopedia.html @@ -94,6 +94,75 @@ body.lang-ru .en, body.lang-ru .de { display: none; } body.lang-de .en, body.lang-de .ru { display: none; } + /* ADR-014 / Phase 4: sentence-level provenance, opt-in. + The encyclopedia register asks for restraint — the engraving + tradition this style draws on doesn't have a vocabulary for + "I am inferring." So default off. The Notes toggle flips + body.audit (same vocabulary as editorial + log) to reveal a + restrained sepia-keyed treatment: INFERRED sentences gain a sepia + ground; per-source underlines mark photo / seed / gpx; hover + surfaces a custom ::after tooltip reading data-tip. */ + .sent { + transition: background-color 120ms ease, box-shadow 120ms ease; + } + body.audit .sent { position: relative; } + body.audit .sent[data-prov="inferred"] { + background: rgba(107, 90, 55, 0.10); + border-radius: 2px; + padding: 0 2px; + } + body.audit .sent[data-prov="photo"] { box-shadow: inset 0 -2px 0 rgba(60, 100, 140, 0.45); } + body.audit .sent[data-prov="seed"] { box-shadow: inset 0 -2px 0 rgba(80, 120, 70, 0.45); } + body.audit .sent[data-prov="gpx"] { box-shadow: inset 0 -2px 0 rgba(140, 100, 50, 0.45); } + body.audit .sent:hover { + background: rgba(107, 90, 55, 0.22); + cursor: help; + } + body.audit .sent:hover::after { + content: attr(data-tip); + position: absolute; + bottom: calc(100% + 4px); left: 0; + background: #2a2520; color: #ede4d3; + padding: 6px 8px; + font-family: "Didot", Garamond, ui-serif, serif; + font-size: 11px; + letter-spacing: 0.04em; line-height: 1.4; + max-width: 280px; white-space: normal; + z-index: 30; pointer-events: none; + box-shadow: 0 4px 12px rgba(0,0,0,0.22); + } + @media print { + body.audit .sent[data-prov] { background: transparent; box-shadow: none; } + } + + /* ── notes (audit) toggle ──────────────────────────────────────── */ + .article-tools { + column-span: all; + display: flex; + justify-content: flex-end; + margin: 0 0 1rem; + } + .article-tools button { + font-family: "Didot", Garamond, ui-serif, serif; + font-size: 0.7rem; + letter-spacing: 0.18em; + text-transform: uppercase; + background: transparent; + color: #6b5a37; + border: 1px solid #6b5a37; + padding: 0.35rem 0.8rem; + cursor: pointer; + } + .article-tools button:hover { + background: rgba(107, 90, 55, 0.08); + color: #2a2520; + } + .article-tools button[aria-pressed="true"] { + background: #2a2520; + color: #ede4d3; + border-color: #2a2520; + } + /* stats — labelled vignette */ .stats { display: grid; @@ -280,12 +349,39 @@
+ + ++-+We left the trailhead at first light, the air sharp with damp moss.
By the saddle the cloud was thinning into a soft white scarf.
Mia slept the whole climb, her cheek warm against the carrier.
+We left the trailhead at first light, the air sharp with damp moss.
++By the saddle the cloud was thinning into a soft white scarf.
++Mia slept the whole climb, her cheek warm against the carrier.
+-+Вышли на тропу с первыми лучами; воздух пах мхом и хвоей.
К седловине облака уже редели, превращаясь в белый шарф.
Мия проспала весь подъём, прижавшись щекой к переноске.
+Вышли на тропу с первыми лучами; воздух пах мхом и хвоей.
++К седловине облака уже редели, превращаясь в белый шарф.
++Мия проспала весь подъём, прижавшись щекой к переноске.
+-+Bei erstem Licht brachen wir auf, die Luft scharf von feuchtem Moos.
Am Sattel zog die Wolke sich zu einem weichen weißen Schal zusammen.
Mia schlief den ganzen Aufstieg, die Wange warm an der Trage.
+Bei erstem Licht brachen wir auf, die Luft scharf von feuchtem Moos.
++Am Sattel zog die Wolke sich zu einem weichen weißen Schal zusammen.
++Mia schlief den ganzen Aufstieg, die Wange warm an der Trage.
+The fog cleared just as we reached the ridge. @@ -412,6 +508,26 @@setLang(langs[(idx + 1) % langs.length]); }); + // ── Notes (audit) toggle (ADR-014 sentence-level provenance) ── + // Mirrors the editorial template's toggle vocabulary so a creator + // who flips notes on in one style sees the audit UI everywhere. + // Preference persists in localStorage. + var NOTES_KEY = 'trailstory.notes'; + var notesBtn = document.getElementById('notesBtn'); + function setNotes(on) { + document.body.classList.toggle('audit', !!on); + if (notesBtn) notesBtn.setAttribute('aria-pressed', on ? 'true' : 'false'); + try { localStorage.setItem(NOTES_KEY, on ? '1' : '0'); } catch (e) {} + } + if (notesBtn) { + var notesStored = null; + try { notesStored = localStorage.getItem(NOTES_KEY); } catch (e) {} + if (notesStored === '1') setNotes(true); + notesBtn.addEventListener('click', function () { + setNotes(!document.body.classList.contains('audit')); + }); + } + var titleEn = "Above the fog line"; var quoteEn = "The fog cleared just as we reached the ridge."; var msg = titleEn + ' — ' + quoteEn; diff --git a/tests/golden/test-render-log.html b/tests/golden/test-render-log.html index 0e31415..1e55a38 100644 --- a/tests/golden/test-render-log.html +++ b/tests/golden/test-render-log.html @@ -79,6 +79,84 @@ body.lang-ru .en, body.lang-ru .de { display: none; } body.lang-de .en, body.lang-de .ru { display: none; } + /* ADR-014 / Phase 4: sentence-level provenance. + The log register can't carry editorial's always-on treatment + without looking busy, so provenance is opt-in here via a "Notes" + toggle next to the language switcher. The vocabulary (body.audit, + #notesBtn, data-tip, localStorage key) mirrors editorial.html.j2 + so a creator who turns notes on in one style sees the same audit + UI everywhere. + + When body.audit is set: + - INFERRED sentences gain a faint highlighter background + - Per-source bottom-edge underline colors mark photo / seed / + gpx provenance (matches editorial) + - Hovering any sentence surfaces a custom ::after tooltip + reading data-tip (the native title attribute is slow, + low-contrast, and absent on touch — same fix as editorial) + When unset, the sentence spans render as plain text. */ + .sent { + transition: background-color 120ms ease, box-shadow 120ms ease; + } + body.audit .sent { position: relative; } + body.audit .sent[data-prov="inferred"] { + background: #fff8d4; + border-radius: 2px; + padding: 0 2px; + } + body.audit .sent[data-prov="photo"] { box-shadow: inset 0 -2px 0 oklch(0.78 0.10 220 / 0.55); } + body.audit .sent[data-prov="seed"] { box-shadow: inset 0 -2px 0 oklch(0.78 0.10 150 / 0.55); } + body.audit .sent[data-prov="gpx"] { box-shadow: inset 0 -2px 0 oklch(0.78 0.06 60 / 0.55); } + body.audit .sent:hover { + background: #ffeb9c; + cursor: help; + } + body.audit .sent:hover::after { + content: attr(data-tip); + position: absolute; + bottom: calc(100% + 4px); left: 0; + background: #1f2328; color: #f4f5f7; + padding: 6px 8px; + font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; + font-size: 11px; + letter-spacing: 0.02em; line-height: 1.35; + max-width: 280px; white-space: normal; + border-radius: 2px; + z-index: 30; pointer-events: none; + box-shadow: 0 4px 10px rgba(0,0,0,0.18); + } + @media print { + body.audit .sent[data-prov] { background: transparent; box-shadow: none; } + } + + /* ── notes (audit) toggle ──────────────────────────────────────── */ + .article-tools { + display: flex; + justify-content: flex-end; + margin: 0 0 0.5rem; + } + .article-tools button { + font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; + font-size: 0.7rem; + letter-spacing: 0.06em; + text-transform: uppercase; + background: transparent; + color: #57606a; + border: 1px solid #d0d7de; + padding: 0.25rem 0.6rem; + cursor: pointer; + border-radius: 3px; + } + .article-tools button:hover { + border-color: #1f2328; + color: #1f2328; + } + .article-tools button[aria-pressed="true"] { + background: #1f2328; + color: #f4f5f7; + border-color: #1f2328; + } + /* stats — list, not a grid; aligned key/value pairs */ .stats { list-style: none; @@ -232,13 +310,40 @@
+ + ++ -+We left the trailhead at first light, the air sharp with damp moss.
By the saddle the cloud was thinning into a soft white scarf.
Mia slept the whole climb, her cheek warm against the carrier.
+We left the trailhead at first light, the air sharp with damp moss.
++By the saddle the cloud was thinning into a soft white scarf.
++Mia slept the whole climb, her cheek warm against the carrier.
+-+Вышли на тропу с первыми лучами; воздух пах мхом и хвоей.
К седловине облака уже редели, превращаясь в белый шарф.
Мия проспала весь подъём, прижавшись щекой к переноске.
+Вышли на тропу с первыми лучами; воздух пах мхом и хвоей.
++К седловине облака уже редели, превращаясь в белый шарф.
++Мия проспала весь подъём, прижавшись щекой к переноске.
+-+Bei erstem Licht brachen wir auf, die Luft scharf von feuchtem Moos.
Am Sattel zog die Wolke sich zu einem weichen weißen Schal zusammen.
Mia schlief den ganzen Aufstieg, die Wange warm an der Trage.
+Bei erstem Licht brachen wir auf, die Luft scharf von feuchtem Moos.
++Am Sattel zog die Wolke sich zu einem weichen weißen Schal zusammen.
++Mia schlief den ganzen Aufstieg, die Wange warm an der Trage.
+The fog cleared just as we reached the ridge. @@ -359,6 +464,26 @@setLang(langs[(idx + 1) % langs.length]); }); + // ── Notes (audit) toggle (ADR-014 sentence-level provenance) ── + // Mirrors the editorial template's toggle vocabulary so a creator + // who flips notes on in one style sees the audit UI everywhere. + // Preference persists in localStorage. + var NOTES_KEY = 'trailstory.notes'; + var notesBtn = document.getElementById('notesBtn'); + function setNotes(on) { + document.body.classList.toggle('audit', !!on); + if (notesBtn) notesBtn.setAttribute('aria-pressed', on ? 'true' : 'false'); + try { localStorage.setItem(NOTES_KEY, on ? '1' : '0'); } catch (e) {} + } + if (notesBtn) { + var notesStored = null; + try { notesStored = localStorage.getItem(NOTES_KEY); } catch (e) {} + if (notesStored === '1') setNotes(true); + notesBtn.addEventListener('click', function () { + setNotes(!document.body.classList.contains('audit')); + }); + } + var titleEn = "Above the fog line"; var quoteEn = "The fog cleared just as we reached the ridge."; var msg = titleEn + ' — ' + quoteEn; diff --git a/web/static/builder.css b/web/static/builder.css index 924a642..bea7fd1 100644 --- a/web/static/builder.css +++ b/web/static/builder.css @@ -1,47 +1,69 @@ -/* Trailstory builder — editorial design system. - Cream paper, near-black ink, one serif + one mono. Mirrors the output - page tokens (templates/styles/editorial.html.j2) so the builder and the - rendered memory feel like one product. */ +/* Trailstory builder — THE NOTEBOOK. + A workshop, not a publication. Distinct from every output style. + Soft dot-grid background, fountain-pen ink blue accent, Onest variable + for both display and body (one file, two roles via weight), JetBrains + Mono for section numbers and stats, Caveat in only two places — the + "your day" doodle in the hero and the "composed by you" signature in + the footer. */ /* ── fonts ────────────────────────────────────────────────────────────── - Self-hosted under /static/fonts. Same variable Source Serif 4 + JetBrains - Mono pair the output renderer uses. Latin and Cyrillic are separate - subsets; the browser activates only what it needs. Unicode-range - block-list matches templates/styles/editorial.html.j2. */ + Self-hosted under /static/fonts. Onest substitutes for both Bricolage + (display) and Hanken (body) from the design brief: it is the only + variable family in the same visual register that ships full Cyrillic on + Google Fonts. Caveat is the script accent. JetBrains Mono is shared + with the editorial output renderer. Subsets split into latin / cyrillic + / latin-ext so the browser activates only what each glyph needs. + See web/static/fonts/LICENSE.md for substitution notes. */ @font-face { - font-family: 'Editorial Serif'; - font-style: italic; - font-weight: 300 600; + font-family: 'Notebook Display'; + font-style: normal; + font-weight: 300 900; font-display: swap; - src: url('/static/fonts/SourceSerif4-Italic-VF.latin.woff2') format('woff2'); + src: url('/static/fonts/Onest-VF.latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { - font-family: 'Editorial Serif'; - font-style: italic; - font-weight: 300 600; + font-family: 'Notebook Display'; + font-style: normal; + font-weight: 300 900; font-display: swap; - src: url('/static/fonts/SourceSerif4-Italic-VF.cyrillic.woff2') format('woff2'); + src: url('/static/fonts/Onest-VF.latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Notebook Display'; + font-style: normal; + font-weight: 300 900; + font-display: swap; + src: url('/static/fonts/Onest-VF.cyrillic.woff2') format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } @font-face { - font-family: 'Editorial Serif'; + font-family: 'Notebook Script'; font-style: normal; - font-weight: 300 600; + font-weight: 400 700; font-display: swap; - src: url('/static/fonts/SourceSerif4-Roman-VF.latin.woff2') format('woff2'); + src: url('/static/fonts/Caveat-VF.latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { - font-family: 'Editorial Serif'; + font-family: 'Notebook Script'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url('/static/fonts/Caveat-VF.latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Notebook Script'; font-style: normal; - font-weight: 300 600; + font-weight: 400 700; font-display: swap; - src: url('/static/fonts/SourceSerif4-Roman-VF.cyrillic.woff2') format('woff2'); + src: url('/static/fonts/Caveat-VF.cyrillic.woff2') format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } @font-face { - font-family: 'Editorial Mono'; + font-family: 'Notebook Mono'; font-style: normal; font-weight: 400 500; font-display: swap; @@ -49,7 +71,7 @@ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { - font-family: 'Editorial Mono'; + font-family: 'Notebook Mono'; font-style: normal; font-weight: 400 500; font-display: swap; @@ -59,48 +81,76 @@ /* ── design tokens ─────────────────────────────────────────────────── */ :root { - --paper: oklch(0.972 0.008 80); - --paper-2: oklch(0.945 0.012 80); - --ink: oklch(0.18 0.01 80); - --ink-2: oklch(0.32 0.01 80); - --ink-3: oklch(0.55 0.01 80); - --rule: oklch(0.18 0.01 80 / 0.18); - --hairline: oklch(0.18 0.01 80 / 0.10); - - --serif: 'Editorial Serif', 'Times New Roman', Georgia, serif; - --mono: 'Editorial Mono', ui-monospace, Menlo, monospace; - --sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + /* Notebook palette — workshop, not publication. Cooler, slightly + bluer cream than the Letter paper; fountain-pen blue is the load- + bearing accent. */ + --nb-bg: oklch(0.965 0.005 90); + --nb-surface: oklch(0.988 0.003 90); + --nb-grid: oklch(0.20 0.02 250 / 0.045); + --nb-grid-strong: oklch(0.20 0.02 250 / 0.085); + --nb-ink: oklch(0.22 0.02 250); + --nb-ink-2: oklch(0.42 0.02 250); + --nb-ink-3: oklch(0.62 0.02 250); + --nb-rule: oklch(0.22 0.02 250 / 0.16); + --nb-hairline: oklch(0.22 0.02 250 / 0.08); + --nb-pen: oklch(0.32 0.10 252); + --nb-pen-2: oklch(0.42 0.13 252); + --nb-pen-tint: oklch(0.32 0.10 252 / 0.08); + --nb-pen-tint-2: oklch(0.32 0.10 252 / 0.14); + --nb-marker: oklch(0.74 0.16 92); + --nb-saved: oklch(0.66 0.16 145); + --nb-saved-glow: oklch(0.66 0.16 145 / 0.18); + + --nb-display: 'Notebook Display', system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + --nb-sans: 'Notebook Display', system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + --nb-mono: 'Notebook Mono', ui-monospace, Menlo, monospace; + --nb-script: 'Notebook Script', 'Brush Script MT', cursive; + + /* Legacy editorial aliases — kept so any third-party CSS or future + pages that opt into the editorial palette can still resolve them. + The builder itself uses --nb-* exclusively. */ + --paper: var(--nb-bg); + --paper-2: var(--nb-surface); + --ink: var(--nb-ink); + --ink-2: var(--nb-ink-2); + --ink-3: var(--nb-ink-3); + --rule: var(--nb-rule); + --hairline: var(--nb-hairline); + --serif: var(--nb-display); + --mono: var(--nb-mono); + --sans: var(--nb-sans); --tap: 44px; - /* Style-thumbnail palettes — used only by the style-picker thumbs. - When the real renderers for these styles ship, each one will define - its own bundle; these tokens stand in for the preview. */ - --zine-paper: oklch(0.948 0.022 86); - --zine-ink: oklch(0.14 0.01 70); - --zine-spot: oklch(0.62 0.19 35); + /* Style-thumbnail palettes — used only by the style-picker thumbs + under web/templates/style_thumbs/. When the real renderers for + these styles ship, each one will define its own bundle; these + tokens stand in for the preview. */ + --zine-paper: oklch(0.948 0.022 86); + --zine-ink: oklch(0.14 0.01 70); + --zine-spot: oklch(0.62 0.19 35); --zine-display: 'Impact', 'Haettenschweiler', 'Arial Narrow Bold', sans-serif; - --sunday-paper: oklch(0.965 0.038 95); - --sunday-sun: oklch(0.88 0.17 92); - --sunday-coral: oklch(0.71 0.17 30); - --sunday-ink: oklch(0.22 0.04 55); - --sunday-display: var(--serif); + --sunday-paper: oklch(0.965 0.038 95); + --sunday-sun: oklch(0.88 0.17 92); + --sunday-coral: oklch(0.71 0.17 30); + --sunday-ink: oklch(0.22 0.04 55); + --sunday-display: var(--nb-display); --pc-paper: oklch(0.948 0.024 80); - --pc-card: oklch(0.975 0.02 82); - --pc-red: oklch(0.58 0.20 28); - --pc-blue: oklch(0.60 0.10 230); - --pc-ink: oklch(0.20 0.02 50); - - --album-paper: oklch(0.94 0.025 88); - --album-paper-2: oklch(0.91 0.03 86); - --album-photo: oklch(0.985 0.005 80); - --album-ink: oklch(0.20 0.01 60); - --album-pen: oklch(0.32 0.10 260); + --pc-card: oklch(0.975 0.02 82); + --pc-red: oklch(0.58 0.20 28); + --pc-blue: oklch(0.60 0.10 230); + --pc-ink: oklch(0.20 0.02 50); + + --album-paper: oklch(0.94 0.025 88); + --album-paper-2: oklch(0.91 0.03 86); + --album-photo: oklch(0.985 0.005 80); + --album-ink: oklch(0.20 0.01 60); + --album-pen: oklch(0.32 0.10 260); --album-tape-pink: oklch(0.78 0.10 350 / 0.78); - --album-warm: oklch(0.88 0.17 92); - --album-coral: oklch(0.71 0.17 30); + --album-warm: oklch(0.88 0.17 92); + --album-coral: oklch(0.71 0.17 30); } *, @@ -113,17 +163,27 @@ html, body { margin: 0; padding: 0; - background: var(--paper); - color: var(--ink); + background: var(--nb-bg); + color: var(--nb-ink); } body { - font-family: var(--sans); + font-family: var(--nb-sans); + font-weight: 400; + font-size: 16px; + line-height: 1.55; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; min-height: 100vh; display: flex; flex-direction: column; + /* Dot-grid notebook surface. Lays a soft ink dot at every 18px so the + page reads as graph paper. Disable on print so the rendered PDF + stays white. */ + background-image: + radial-gradient(circle at center, var(--nb-grid) 1px, transparent 1.4px); + background-size: 18px 18px; + background-position: 0 0; } [x-cloak] { @@ -146,8 +206,10 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { position: sticky; top: 0; z-index: 50; - background: var(--paper); - border-bottom: 1px solid var(--hairline); + background: color-mix(in oklab, var(--nb-bg) 92%, transparent); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border-bottom: 1px solid var(--nb-hairline); display: flex; align-items: center; justify-content: space-between; @@ -157,41 +219,68 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { .bp-logo { display: inline-flex; align-items: center; - gap: 10px; + gap: 12px; text-decoration: none; - color: var(--ink); + color: var(--nb-ink); +} + +.bp-logo svg { + color: var(--nb-pen); } .bp-logo-word { - font-family: var(--mono); - font-size: 12px; - letter-spacing: 0.14em; - font-weight: 600; + font-family: var(--nb-display); + font-size: 18px; + font-weight: 700; + letter-spacing: -0.01em; + color: var(--nb-ink); +} + +/* Hand-noted "builder" tag next to the logo. Inserted by builder_base. */ +.bp-logo-tag { + font-family: var(--nb-mono); + font-size: 10px; + letter-spacing: 0.12em; + color: var(--nb-pen); + font-style: italic; + padding: 2px 8px; + border: 1px solid var(--nb-pen); + transform: rotate(-2deg); + display: inline-block; + text-transform: lowercase; } .bp-langnav { display: inline-flex; - border: 1px solid var(--ink); + border: 1px solid var(--nb-ink); + background: var(--nb-surface); } .bp-lang-btn { - font-family: var(--mono); - font-size: 10px; + font-family: var(--nb-mono); + font-size: 11px; letter-spacing: 0.18em; font-weight: 500; border: 0; background: transparent; - color: var(--ink); + color: var(--nb-ink); padding: 6px 12px; cursor: pointer; min-height: 32px; text-decoration: none; display: inline-flex; align-items: center; + transition: + background 0.12s, + color 0.12s; +} +.bp-lang-btn:hover { + background: var(--nb-pen-tint); + color: var(--nb-pen); } .bp-lang-btn[aria-pressed="true"] { - background: var(--ink); - color: var(--paper); + background: var(--nb-ink); + color: var(--nb-bg); } /* ── main column ────────────────────────────────────────────────────── */ @@ -199,50 +288,132 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { max-width: 760px; width: 100%; margin: 0 auto; - padding: 48px 28px 96px; + padding: 56px 28px 96px; flex: 1; + position: relative; } /* ── hero ───────────────────────────────────────────────────────────── */ .bp-hero { margin-bottom: 56px; + position: relative; +} + +/* Hand-sketched "your day" route loop in pen-ink blue. Sits in the + top-right of the hero. Inserted via inline SVG in landing.html.j2 so + we can apply the #bp-hand turbulence filter for the rough feel. */ +.bp-hero-doodle { + position: absolute; + top: -10px; + right: -10px; + width: 140px; + height: 100px; + opacity: 0.95; + pointer-events: none; + overflow: visible; +} +.bp-hero-doodle text { + font-family: var(--nb-script); + font-size: 18px; + fill: var(--nb-pen); + opacity: 0.7; } .bp-eyebrow { - font-family: var(--mono); + font-family: var(--nb-mono); font-size: 11px; - letter-spacing: 0.22em; + letter-spacing: 0.20em; font-weight: 500; - color: var(--ink-2); + color: var(--nb-pen); text-transform: uppercase; + display: inline-flex; + align-items: center; + gap: 8px; +} +.bp-eyebrow::before { + content: ""; + width: 16px; + height: 1.5px; + background: var(--nb-pen); + display: inline-block; } .bp-h1 { - font-family: var(--serif); - font-weight: 600; - font-variation-settings: "opsz" 60; - font-size: 64px; + font-family: var(--nb-display); + font-weight: 700; + font-size: 54px; line-height: 1.02; letter-spacing: -0.025em; - color: var(--ink); - margin: 14px 0 24px; + color: var(--nb-ink); + margin: 14px 0 18px; white-space: pre-line; + max-width: 580px; } .bp-dek { - font-family: var(--serif); - font-size: 19px; - line-height: 1.5; - color: var(--ink-2); + font-family: var(--nb-sans); + font-weight: 400; + font-size: 17px; + line-height: 1.55; + color: var(--nb-ink-2); margin: 0; - max-width: 560px; + max-width: 540px; +} + +/* Draft badge + saved indicator under the dek. Optional decorative + row in the hero — populated by landing.html.j2 if at all. */ +.bp-hero-meta { + margin-top: 22px; + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.bp-draft-badge { + display: inline-flex; + align-items: center; + gap: 6px; + font-family: var(--nb-mono); + font-size: 10px; + letter-spacing: 0.16em; + font-weight: 600; + padding: 4px 10px; + border: 1.5px solid var(--nb-pen); + color: var(--nb-pen); + background: var(--nb-surface); + transform: rotate(-1.2deg); + text-transform: uppercase; +} + +.bp-saved { + font-family: var(--nb-mono); + font-size: 10px; + letter-spacing: 0.06em; + color: var(--nb-ink-3); + display: inline-flex; + align-items: center; + gap: 5px; +} +.bp-saved-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--nb-saved); + box-shadow: 0 0 0 3px var(--nb-saved-glow); + animation: bp-pulse 2s infinite; +} +@keyframes bp-pulse { + 0%, 100% { box-shadow: 0 0 0 3px var(--nb-saved-glow); } + 50% { box-shadow: 0 0 0 5px color-mix(in oklab, var(--nb-saved) 8%, transparent); } } /* ── sections (numbered cards) ──────────────────────────────────────── */ .bp-section { margin: 0 0 36px; padding-top: 28px; - border-top: 1px solid var(--rule); + border-top: 1px solid var(--nb-rule); + position: relative; } .bp-section-head { @@ -253,27 +424,30 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { } .bp-section-num { - font-family: var(--mono); - font-size: 11px; - color: var(--ink-3); - letter-spacing: 0.14em; + font-family: var(--nb-mono); + font-style: italic; + font-size: 12px; + color: var(--nb-pen); + letter-spacing: 0.04em; + border-bottom: 1.5px dotted var(--nb-pen); + padding-bottom: 1px; } .bp-section-title { margin: 0; flex: 1; - font-family: var(--serif); + font-family: var(--nb-display); font-weight: 600; - font-size: 30px; - font-variation-settings: "opsz" 28; + font-size: 26px; letter-spacing: -0.015em; line-height: 1.05; + color: var(--nb-ink); } .bp-section-hint { - font-family: var(--mono); + font-family: var(--nb-mono); font-size: 11px; - color: var(--ink-3); + color: var(--nb-ink-3); letter-spacing: 0.06em; } @@ -287,38 +461,40 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { flex-direction: column; align-items: center; gap: 8px; - padding: 32px 24px; - border: 1.5px dashed var(--ink-3); - background: var(--paper-2); - color: var(--ink-2); + padding: 36px 24px; + border: 1.5px dashed var(--nb-pen); + background: var(--nb-pen-tint); + color: var(--nb-pen); cursor: pointer; transition: - border-color 0.15s, - background 0.15s; + background 0.15s, + transform 0.15s, + border-color 0.15s; } .bp-dropzone:hover, .bp-dropzone:focus-within { - border-color: var(--ink); - background: var(--paper); + background: var(--nb-pen-tint-2); + transform: translateY(-1px); } .bp-dropzone-icon { - color: var(--ink); + color: var(--nb-pen); display: flex; } .bp-dropzone-title { - font-family: var(--serif); + font-family: var(--nb-display); + font-weight: 600; font-size: 19px; - color: var(--ink); + color: var(--nb-ink); margin-top: 4px; text-align: center; } .bp-dropzone-hint { - font-family: var(--mono); + font-family: var(--nb-mono); font-size: 11px; - color: var(--ink-3); + color: var(--nb-ink-3); letter-spacing: 0.04em; text-align: center; } @@ -339,63 +515,82 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { .bp-dropzone-filename { margin-top: 6px; - font-family: var(--mono); + font-family: var(--nb-mono); font-size: 11px; - color: var(--ink); + color: var(--nb-ink); letter-spacing: 0.04em; word-break: break-all; } -/* ── textarea ───────────────────────────────────────────────────────── */ +/* ── textarea — notebook-ruled paper feel ───────────────────────────── */ .bp-textarea { width: 100%; - border: 1px solid var(--ink); - background: var(--paper); - padding: 16px 18px; - font-family: var(--serif); - font-size: 16px; - line-height: 1.55; - color: var(--ink); + border: 1px solid var(--nb-rule); + background: var(--nb-surface); + /* Faux notebook ruled lines: a faint horizontal line every 24px. The + padding-top + line-height keep the typing baseline on the rules. */ + background-image: repeating-linear-gradient( + to bottom, + transparent, + transparent 23px, + var(--nb-grid) 23px, + var(--nb-grid) 24px + ); + background-attachment: local; + padding: 18px 20px; + font-family: var(--nb-sans); + font-size: 15px; + line-height: 24px; + color: var(--nb-ink); resize: vertical; outline: none; border-radius: 0; - min-height: 180px; + min-height: 192px; + transition: + border-color 0.12s, + box-shadow 0.12s; } .bp-textarea:focus { - box-shadow: inset 3px 0 0 var(--ink); + border-color: var(--nb-pen); + box-shadow: inset 4px 0 0 var(--nb-pen); +} +.bp-textarea::placeholder { + color: var(--nb-ink-3); + font-style: italic; } .bp-textarea-foot { display: flex; justify-content: space-between; - margin-top: 6px; - font-family: var(--mono); + margin-top: 8px; + font-family: var(--nb-mono); font-size: 10px; - color: var(--ink-3); + color: var(--nb-ink-3); letter-spacing: 0.04em; } -/* ── optional location field ────────────────────────────────────────── */ +/* ── optional location field (legacy) ───────────────────────────────── */ .bp-input { width: 100%; - border: 1px solid var(--hairline); - background: var(--paper); + border: 1px solid var(--nb-rule); + background: var(--nb-surface); padding: 12px 14px; - font-family: var(--serif); + font-family: var(--nb-sans); font-size: 16px; - color: var(--ink); + color: var(--nb-ink); outline: none; border-radius: 0; } .bp-input:focus { - border-color: var(--ink); - box-shadow: inset 3px 0 0 var(--ink); + border-color: var(--nb-pen); + box-shadow: inset 3px 0 0 var(--nb-pen); } /* ── track card (populated state after GPX upload) ──────────────────── */ .bp-track-card { - border: 1px solid var(--hairline); - background: var(--paper-2); + border: 1px solid var(--nb-hairline); + background: var(--nb-surface); + box-shadow: 0 1px 0 var(--nb-hairline); } .bp-track-row { @@ -406,7 +601,7 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { } .bp-track-icon { - color: var(--ink); + color: var(--nb-pen); display: flex; } @@ -416,40 +611,41 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { } .bp-track-filename { - font-family: var(--mono); + font-family: var(--nb-mono); font-size: 13px; font-weight: 500; - color: var(--ink); + color: var(--nb-ink); word-break: break-all; } .bp-track-parsed { - font-family: var(--mono); + font-family: var(--nb-mono); font-size: 10px; - color: var(--ink-3); + color: var(--nb-ink-3); letter-spacing: 0.04em; margin-top: 2px; } .bp-track-replace { - font-family: var(--mono); + font-family: var(--nb-mono); font-size: 10px; letter-spacing: 0.12em; padding: 6px 10px; background: transparent; - color: var(--ink-2); - border: 1px solid var(--hairline); + color: var(--nb-ink-2); + border: 1px solid var(--nb-rule); cursor: pointer; + font-weight: 500; } .bp-track-replace:hover { - background: var(--paper); - border-color: var(--ink-2); - color: var(--ink); + background: var(--nb-pen-tint); + border-color: var(--nb-pen); + color: var(--nb-pen); } .bp-track-divider { height: 1px; - background: var(--hairline); + background: var(--nb-hairline); border: 0; margin: 0; } @@ -474,22 +670,23 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { } .bp-stat-k { - font-family: var(--mono); + font-family: var(--nb-mono); font-size: 9px; - color: var(--ink-3); - letter-spacing: 0.12em; + color: var(--nb-ink-3); + letter-spacing: 0.14em; } .bp-stat-v { - font-family: var(--serif); + font-family: var(--nb-display); + font-weight: 500; font-size: 18px; - color: var(--ink); + color: var(--nb-ink); margin-top: 2px; } .bp-track-mini { - background: var(--paper); - border: 1px solid var(--hairline); + background: var(--nb-bg); + border: 1px solid var(--nb-hairline); padding: 8px; } @@ -498,8 +695,11 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { width: 100%; height: auto; } +/* The track-mini SVG uses `var(--ink)` / `var(--paper)` because it's + inline in landing.html.j2. The legacy aliases in :root keep those + resolving to the Notebook palette. */ -/* ── photo grid (read-only in phase 2) ──────────────────────────────── */ +/* ── photo grid ──────────────────────────────────────────────────────── */ .bp-photogrid { display: grid; grid-template-columns: repeat(4, 1fr); @@ -510,8 +710,8 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { position: relative; aspect-ratio: 1 / 1; overflow: hidden; - border: 1px solid var(--hairline); - background: var(--paper-2); + border: 1px solid var(--nb-hairline); + background: var(--nb-surface); } .bp-phototile img { @@ -526,25 +726,26 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { position: absolute; top: 6px; left: 6px; - background: var(--paper); - font-family: var(--mono); + background: var(--nb-surface); + font-family: var(--nb-mono); font-size: 10px; padding: 2px 6px; letter-spacing: 0.08em; font-weight: 600; + color: var(--nb-ink); } .bp-mini { - margin-top: 8px; - font-family: var(--mono); + margin-top: 10px; + font-family: var(--nb-mono); font-size: 10px; letter-spacing: 0.04em; - color: var(--ink-3); + color: var(--nb-ink-3); } /* ── chips (auto-extracted location / date) ─────────────────────────── */ .bp-chips { - margin-top: 20px; + margin-top: 22px; display: flex; flex-wrap: wrap; gap: 8px; @@ -552,32 +753,36 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { } .bp-chip-label { - font-family: var(--mono); + font-family: var(--nb-mono); font-size: 10px; letter-spacing: 0.18em; - color: var(--ink-3); + font-weight: 600; + color: var(--nb-pen); margin-right: 4px; + text-transform: uppercase; } .bp-chip { display: inline-flex; align-items: center; - gap: 6px; - background: var(--paper-2); + gap: 7px; + background: var(--nb-surface); padding: 7px 12px; - font-family: var(--serif); + font-family: var(--nb-sans); font-size: 14px; - color: var(--ink); - border: 1px solid var(--hairline); + font-weight: 500; + color: var(--nb-ink); + border: 1px solid var(--nb-rule); cursor: pointer; transition: border-color 0.12s, + color 0.12s, background 0.12s; } .bp-chip:hover, .bp-chip:focus { - border-color: var(--ink); - background: var(--paper); + border-color: var(--nb-pen); + color: var(--nb-pen); outline: none; } @@ -586,17 +791,20 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { opacity: 0.85; } .bp-chip--static:hover { - border-color: var(--hairline); - background: var(--paper-2); + border-color: var(--nb-rule); + color: var(--nb-ink); + background: var(--nb-surface); } .bp-chip--editing { - background: var(--paper); - border-color: var(--ink); + background: var(--nb-surface); + border-color: var(--nb-pen); + box-shadow: 0 0 0 3px var(--nb-pen-tint); } .bp-chip-icon { font-size: 13px; + color: var(--nb-pen); } .bp-chip-val { @@ -604,16 +812,17 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { } .bp-chip-src { - font-family: var(--mono); + font-family: var(--nb-mono); font-size: 10px; - color: var(--ink-3); + color: var(--nb-ink-3); letter-spacing: 0.04em; + font-weight: 400; } .bp-chip-edit { margin-left: 2px; font-size: 11px; - color: var(--ink-3); + color: var(--nb-ink-3); opacity: 0; transition: opacity 0.12s; } @@ -626,9 +835,10 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { border: 0; background: transparent; outline: none; - font-family: var(--serif); + font-family: var(--nb-sans); font-size: 14px; - color: var(--ink); + font-weight: 500; + color: var(--nb-ink); width: 220px; max-width: 100%; } @@ -639,32 +849,34 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { cursor: pointer; padding: 2px 6px; font-size: 14px; - color: var(--ink); + color: var(--nb-pen); } .bp-chip-act--cancel { - color: var(--ink-3); + color: var(--nb-ink-3); } /* ── style picker (radio cards) ─────────────────────────────────────── */ .bp-stylegrid { display: grid; grid-template-columns: 1fr 1fr; - gap: 12px; + gap: 14px; } .bp-stylecard { display: block; cursor: pointer; - background: var(--paper); - border: 1px solid var(--hairline); + background: var(--nb-surface); + border: 1px solid var(--nb-rule); transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s; + position: relative; } .bp-stylecard:hover { - border-color: var(--ink-3); - transform: translateY(-1px); + border-color: var(--nb-pen); + transform: translateY(-2px); + box-shadow: 0 6px 14px -8px color-mix(in oklab, var(--nb-pen) 32%, transparent); } .bp-stylecard input[type="radio"] { position: absolute; @@ -676,15 +888,34 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { clip: rect(0, 0, 0, 0); } .bp-stylecard:has(input[type="radio"]:checked) { - border-color: var(--ink); + border-color: var(--nb-pen); box-shadow: - 0 0 0 1px var(--ink), - 0 8px 20px -8px rgba(0, 0, 0, 0.1); + 0 0 0 1px var(--nb-pen), + 0 8px 22px -10px color-mix(in oklab, var(--nb-pen) 38%, transparent); +} +.bp-stylecard:has(input[type="radio"]:checked)::before { + content: "✓"; + position: absolute; + top: -10px; + left: -10px; + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--nb-pen); + color: var(--nb-bg); + display: flex; + align-items: center; + justify-content: center; + font-family: var(--nb-mono); + font-size: 12px; + font-weight: 700; + z-index: 2; + box-shadow: 0 2px 6px color-mix(in oklab, var(--nb-pen) 40%, transparent); } .bp-stylecard-thumb { height: 130px; - border-bottom: 1px solid var(--hairline); + border-bottom: 1px solid var(--nb-hairline); overflow: hidden; position: relative; } @@ -700,35 +931,36 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { } .bp-stylecard-name { - font-family: var(--serif); - font-weight: 500; + font-family: var(--nb-display); + font-weight: 600; font-size: 19px; - color: var(--ink); + letter-spacing: -0.01em; + color: var(--nb-ink); } .bp-stylecard-sub { - font-family: var(--mono); + font-family: var(--nb-mono); font-size: 10px; letter-spacing: 0.04em; - color: var(--ink-3); + color: var(--nb-ink-3); margin-top: 2px; } .bp-stylecard-desc { - font-family: var(--serif); + font-family: var(--nb-sans); font-size: 13.5px; line-height: 1.5; - color: var(--ink-2); - margin: 8px 0 0; + color: var(--nb-ink-2); + margin: 10px 0 0; } .bp-stylecard-tag { - font-family: var(--mono); + font-family: var(--nb-mono); font-size: 9px; letter-spacing: 0.14em; font-weight: 600; - background: var(--ink-3); - color: var(--paper); + background: var(--nb-marker); + color: var(--nb-ink); padding: 2px 6px; } @@ -738,73 +970,95 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { } .bp-stylecard--soon:hover { transform: none; - border-color: var(--hairline); + border-color: var(--nb-rule); + box-shadow: none; } /* ── generate CTA + meta line ───────────────────────────────────────── */ .bp-generate { margin-top: 56px; padding-top: 28px; - border-top: 1px solid var(--rule); + border-top: 1px solid var(--nb-rule); display: flex; flex-direction: column; align-items: center; + position: relative; +} +/* Hand-drawn "↓" mark hovering above the button. Pure decoration — + uses Caveat to keep the workshop note feeling. */ +.bp-generate::before { + content: "↓"; + position: absolute; + top: 4px; + right: 50%; + transform: translateX(70px) rotate(8deg); + font-family: var(--nb-script); + font-weight: 600; + font-size: 30px; + color: var(--nb-pen); + opacity: 0.55; + line-height: 1; + pointer-events: none; } .bp-cta { - font-family: var(--serif); - font-weight: 500; - font-size: 19px; - padding: 16px 28px; - background: var(--ink); - color: var(--paper); + font-family: var(--nb-display); + font-weight: 600; + font-size: 18px; + padding: 16px 30px; + background: var(--nb-pen); + color: var(--nb-bg); border: 0; cursor: pointer; display: inline-flex; align-items: center; - gap: 10px; + gap: 12px; min-height: var(--tap); + box-shadow: 0 6px 18px -6px color-mix(in oklab, var(--nb-pen) 45%, transparent); transition: - opacity 0.15s, + background 0.15s, + box-shadow 0.15s, transform 0.15s; } .bp-cta:hover { - opacity: 0.9; + background: var(--nb-pen-2); + box-shadow: 0 8px 22px -6px color-mix(in oklab, var(--nb-pen) 55%, transparent); } .bp-cta:active { transform: translateY(1px); } .bp-cta:disabled { - opacity: 0.6; + opacity: 0.65; cursor: progress; + box-shadow: none; } .bp-cta-arrow { - font-family: var(--mono); - font-weight: 400; + font-family: var(--nb-mono); + font-weight: 500; } .bp-generate-meta { - margin-top: 12px; + margin-top: 14px; display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; - font-family: var(--mono); + font-family: var(--nb-mono); font-size: 10px; letter-spacing: 0.06em; - color: var(--ink-3); + color: var(--nb-ink-3); text-align: center; text-transform: uppercase; } .bp-generate-meta a { color: inherit; text-decoration: underline; - text-decoration-color: var(--hairline); + text-decoration-color: var(--nb-hairline); text-underline-offset: 2px; } .bp-generate-meta a:hover { - color: var(--ink); + color: var(--nb-pen); text-decoration-color: currentColor; } @@ -815,13 +1069,13 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { /* ── live SSE draft block (generating page) ─────────────────────────── */ .bp-livedraft { min-height: 12rem; - border: 1px solid var(--hairline); - background: var(--paper); + border: 1px solid var(--nb-hairline); + background: var(--nb-surface); padding: 16px 18px; - font-family: var(--mono); + font-family: var(--nb-mono); font-size: 13px; line-height: 1.5; - color: var(--ink); + color: var(--nb-ink); white-space: pre-wrap; word-break: break-word; } @@ -833,7 +1087,7 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { border: 1px solid oklch(0.55 0.15 25); background: oklch(0.96 0.04 25); color: oklch(0.32 0.12 25); - font-family: var(--serif); + font-family: var(--nb-sans); font-size: 15px; line-height: 1.5; } @@ -841,26 +1095,40 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { /* ── footer ─────────────────────────────────────────────────────────── */ .bp-footer { margin-top: 64px; - padding: 24px 28px 32px; - border-top: 1px solid var(--hairline); + padding: 28px 28px 36px; + border-top: 1px solid var(--nb-hairline); display: flex; justify-content: space-between; align-items: center; - font-family: var(--mono); + font-family: var(--nb-mono); font-size: 10px; letter-spacing: 0.12em; - color: var(--ink-3); + color: var(--nb-ink-3); text-transform: uppercase; flex-wrap: wrap; - gap: 8px; + gap: 16px; } .bp-footer a { - color: var(--ink-3); + color: var(--nb-ink-3); text-decoration: none; } .bp-footer a:hover { - color: var(--ink); + color: var(--nb-pen); +} + +/* The "— composed by you" signature in the footer. Caveat, rotated + slightly, blue ink. The only other use of Caveat besides the hero + doodle. */ +.bp-footer-sig { + font-family: var(--nb-script); + font-weight: 600; + font-size: 22px; + color: var(--nb-pen); + text-transform: none; + letter-spacing: 0; + transform: rotate(-2deg); + display: inline-block; } /* ── responsive ─────────────────────────────────────────────────────── */ @@ -872,7 +1140,12 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { padding: 32px 18px 64px; } .bp-h1 { - font-size: 44px; + font-size: 38px; + } + .bp-hero-doodle { + width: 90px; + height: 70px; + opacity: 0.7; } .bp-track-summary { grid-template-columns: 1fr; @@ -889,3 +1162,22 @@ html[data-lang="de"] :is(.lang-en, .lang-ru) { align-items: flex-start; } } + +/* ── print: drop the dot-grid and the decorative blue accents ──────── */ +@media print { + body { + background-image: none; + } + .bp-header, + .bp-footer, + .bp-hero-doodle, + .bp-generate::before { + display: none; + } + .bp-cta { + background: transparent; + color: var(--nb-ink); + border: 1px solid var(--nb-ink); + box-shadow: none; + } +} diff --git a/web/static/fonts/Caveat-VF.cyrillic.woff2 b/web/static/fonts/Caveat-VF.cyrillic.woff2 new file mode 100644 index 0000000..390184c Binary files /dev/null and b/web/static/fonts/Caveat-VF.cyrillic.woff2 differ diff --git a/web/static/fonts/Caveat-VF.latin-ext.woff2 b/web/static/fonts/Caveat-VF.latin-ext.woff2 new file mode 100644 index 0000000..33ce75e Binary files /dev/null and b/web/static/fonts/Caveat-VF.latin-ext.woff2 differ diff --git a/web/static/fonts/Caveat-VF.latin.woff2 b/web/static/fonts/Caveat-VF.latin.woff2 new file mode 100644 index 0000000..836dbda Binary files /dev/null and b/web/static/fonts/Caveat-VF.latin.woff2 differ diff --git a/web/static/fonts/LICENSE.md b/web/static/fonts/LICENSE.md index ff69888..6751e02 100644 --- a/web/static/fonts/LICENSE.md +++ b/web/static/fonts/LICENSE.md @@ -1,22 +1,41 @@ -# Editorial style font licenses +# Font licenses -Both font families bundled in this directory ship under the +All font families bundled in this directory ship under the **SIL Open Font License 1.1**. Copying the WOFF2 files into the repo and embedding them as base64 in the rendered HTML is explicitly permitted by the license. -| File family | Foundry | Source | License | -|---|---|---|---| -| `SourceSerif4-Italic-VF.*.woff2`, `SourceSerif4-Roman-VF.*.woff2` | Frank Grießhammer / Adobe | https://github.com/adobe-fonts/source-serif / Google Fonts | OFL 1.1 | -| `JetBrainsMono-VF.*.woff2` | JetBrains | https://github.com/JetBrains/JetBrainsMono | OFL 1.1 | +| File family | Used by | Foundry | Source | License | +|---|---|---|---|---| +| `SourceSerif4-Italic-VF.*.woff2`, `SourceSerif4-Roman-VF.*.woff2` | Letter (output) | Frank Grießhammer / Adobe | https://github.com/adobe-fonts/source-serif / Google Fonts | OFL 1.1 | +| `JetBrainsMono-VF.*.woff2` | Letter (output), Notebook (builder) | JetBrains | https://github.com/JetBrains/JetBrainsMono | OFL 1.1 | +| `Onest-VF.*.woff2` | Notebook (builder) | Rosetta Type / Martin Vácha | https://github.com/martinvacha/onest / Google Fonts | OFL 1.1 | +| `Caveat-VF.*.woff2` | Notebook (builder) | Pablo Impallari | https://github.com/impallari/Caveat / Google Fonts | OFL 1.1 | The WOFF2 files in this directory are the subset builds served by -Google Fonts (latin + cyrillic). They were chosen, instead of the -Newsreader family named in the original design brief, because -Newsreader on Google Fonts does not include a Cyrillic subset — -Russian readers (a primary audience for this project) would have -fallen back to system serif. Source Serif 4 shares the same -optical-size variable axis and italic + roman pair as Newsreader, -and ships full Cyrillic + Latin coverage. +Google Fonts (latin + cyrillic + latin-ext). Subsets are split so the +browser activates only what it needs. + +## Substitutions from the original design brief + +**Letter (output renderer):** the brief specified Newsreader. Newsreader +on Google Fonts does not include a Cyrillic subset — Russian readers +(a primary audience for this project) would have fallen back to system +serif. Source Serif 4 shares the same optical-size variable axis and +italic + roman pair as Newsreader, and ships full Cyrillic + Latin +coverage. + +**Notebook (builder UI):** the brief specified Bricolage Grotesque +(variable display) and Hanken Grotesk (body). Neither has a basic +Cyrillic subset on Google Fonts — same problem as Newsreader. Onest +(by Rosetta Type) covers both roles: it is a wide-range variable font +(wght 300..900), ships full Cyrillic + Latin coverage, and has the +organic, slightly-warm letterforms that match the workshop register +the Notebook is going for. One file plays both the display and body +roles via weight. + +Caveat is used identically to the brief: handwritten Cyrillic-capable +script, two appearances only — the "your day" doodle annotation in the +hero and the "— composed by you" signature in the footer. Full OFL text: https://openfontlicense.org/open-font-license-official-text/ diff --git a/web/static/fonts/Onest-VF.cyrillic.woff2 b/web/static/fonts/Onest-VF.cyrillic.woff2 new file mode 100644 index 0000000..02d31b9 Binary files /dev/null and b/web/static/fonts/Onest-VF.cyrillic.woff2 differ diff --git a/web/static/fonts/Onest-VF.latin-ext.woff2 b/web/static/fonts/Onest-VF.latin-ext.woff2 new file mode 100644 index 0000000..19eac69 Binary files /dev/null and b/web/static/fonts/Onest-VF.latin-ext.woff2 differ diff --git a/web/static/fonts/Onest-VF.latin.woff2 b/web/static/fonts/Onest-VF.latin.woff2 new file mode 100644 index 0000000..96d0804 Binary files /dev/null and b/web/static/fonts/Onest-VF.latin.woff2 differ diff --git a/web/templates/builder_base.html.j2 b/web/templates/builder_base.html.j2 index 3eb956d..28d9267 100644 --- a/web/templates/builder_base.html.j2 +++ b/web/templates/builder_base.html.j2 @@ -58,13 +58,22 @@
- {# Inline route logo — matches the design proposal. #} + {# Inline route logo — fountain-pen blue ink loop, matches the + hero doodle's visual register. #} TRAILSTORY + {# Hand-noted "builder" tag next to the logo — small italic mono + in fountain-pen blue, slight rotation. Signals workshop vs + publication. #} +