Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions templates/styles/editorial.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -566,10 +566,16 @@
<span class="de">{{ narrative.subtitle.de }}</span>
</p>
{% if meta.location or meta.date %}
{# Byline + dateline. The byline reads as a small editorial note
clarifying provenance — that the prose is reconstructed from
the seed text, the GPX track, and the photos — not a real-time
diary. Conditional on at least one of location/date so the
no-meta render stays clean. #}
<div class="meta">
{% if meta.location %}{{ meta.location | upper }}{% endif %}
{% if meta.location and meta.date %} &middot; {% endif %}
{% if meta.date %}{{ meta.date }}{% endif %}
<span class="en">Written from notes, photographs, and a recorded path.</span>
<span class="ru">Написано по заметкам, фотографиям и записанному треку.</span>
<span class="de">Aus Notizen, Fotos und einem aufgezeichneten Pfad geschrieben.</span>
<span aria-hidden="true"> &middot; </span>{% if meta.location %}{{ meta.location | upper }}{% endif %}{% if meta.location and meta.date %} &middot; {% endif %}{% if meta.date %}{{ meta.date }}{% endif %}
</div>
{% endif %}
<hr class="rule">
Expand Down
133 changes: 127 additions & 6 deletions templates/styles/encyclopedia.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -287,17 +356,49 @@
</svg>

<article>
{# ADR-014 / Phase 4: see editorial.html.j2 for the per-sentence
provenance treatment. This style uses the flat fallback until
Phase 4.1 ports it. #}
{# Reading tools — Notes (audit) toggle. column-span: all keeps
the row above the two-column body rather than getting trapped
inside the left column. Vocabulary mirrors editorial.html.j2. #}
<div class="article-tools" role="group" aria-label="Show writing notes">
<button
type="button"
id="notesBtn"
aria-pressed="false"
aria-label="Show or hide per-sentence writing notes"
>Notes</button>
</div>

{# ADR-014 / Phase 4: sentence-level provenance, opt-in.
Each sentence becomes a <span data-prov> so the rendered HTML
carries the grounding info. Off by default — `body.audit`
opts in (see the article-tools button above). Tooltips render
via the custom ::after data-tip pattern. #}
<div class="en">
{% for p in flat_paragraphs.en %}<p>{{ p }}</p>{% endfor %}
{% for paragraph in narrative.paragraphs %}
<p>
{% for sentence in paragraph -%}
<span class="sent" data-prov="{{ sentence.provenance.source }}" data-tip="{{ sentence.provenance.source }}: {{ sentence.provenance.reference }}">{{ sentence.text.en }}</span>{% if not loop.last %} {% endif -%}
{% endfor %}
</p>
{% endfor %}
</div>
<div class="ru">
{% for p in flat_paragraphs.ru %}<p>{{ p }}</p>{% endfor %}
{% for paragraph in narrative.paragraphs %}
<p>
{% for sentence in paragraph -%}
<span class="sent" data-prov="{{ sentence.provenance.source }}" data-tip="{{ sentence.provenance.source }}: {{ sentence.provenance.reference }}">{{ sentence.text.ru }}</span>{% if not loop.last %} {% endif -%}
{% endfor %}
</p>
{% endfor %}
</div>
<div class="de">
{% for p in flat_paragraphs.de %}<p>{{ p }}</p>{% endfor %}
{% for paragraph in narrative.paragraphs %}
<p>
{% for sentence in paragraph -%}
<span class="sent" data-prov="{{ sentence.provenance.source }}" data-tip="{{ sentence.provenance.source }}: {{ sentence.provenance.reference }}">{{ sentence.text.de }}</span>{% if not loop.last %} {% endif -%}
{% endfor %}
</p>
{% endfor %}
</div>

<blockquote>
Expand Down Expand Up @@ -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;
Expand Down
146 changes: 138 additions & 8 deletions templates/styles/log.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -236,20 +314,52 @@
L 100,30"/>
</svg>

{# 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. #}
<div class="article-tools" role="group" aria-label="Show writing notes">
<button
type="button"
id="notesBtn"
aria-pressed="false"
aria-label="Show or hide per-sentence writing notes"
>Notes</button>
</div>

<article>
{# 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 <span data-prov> 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. #}
<div class="en">
{% for p in flat_paragraphs.en %}<p>{{ p }}</p>{% endfor %}
{% for paragraph in narrative.paragraphs %}
<p>
{% for sentence in paragraph -%}
<span class="sent" data-prov="{{ sentence.provenance.source }}" data-tip="{{ sentence.provenance.source }}: {{ sentence.provenance.reference }}">{{ sentence.text.en }}</span>{% if not loop.last %} {% endif -%}
{% endfor %}
</p>
{% endfor %}
</div>
<div class="ru">
{% for p in flat_paragraphs.ru %}<p>{{ p }}</p>{% endfor %}
{% for paragraph in narrative.paragraphs %}
<p>
{% for sentence in paragraph -%}
<span class="sent" data-prov="{{ sentence.provenance.source }}" data-tip="{{ sentence.provenance.source }}: {{ sentence.provenance.reference }}">{{ sentence.text.ru }}</span>{% if not loop.last %} {% endif -%}
{% endfor %}
</p>
{% endfor %}
</div>
<div class="de">
{% for p in flat_paragraphs.de %}<p>{{ p }}</p>{% endfor %}
{% for paragraph in narrative.paragraphs %}
<p>
{% for sentence in paragraph -%}
<span class="sent" data-prov="{{ sentence.provenance.source }}" data-tip="{{ sentence.provenance.source }}: {{ sentence.provenance.reference }}">{{ sentence.text.de }}</span>{% if not loop.last %} {% endif -%}
{% endfor %}
</p>
{% endfor %}
</div>

<blockquote>
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion tests/golden/test-render-editorial.html
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,10 @@ <h1 class="display">
<span class="de">Ein Morgen über dem Wolkenmeer</span>
</p>
<div class="meta">
BAVARIAN ALPS &middot; 2025-08-15 </div>
<span class="en">Written from notes, photographs, and a recorded path.</span>
<span class="ru">Написано по заметкам, фотографиям и записанному треку.</span>
<span class="de">Aus Notizen, Fotos und einem aufgezeichneten Pfad geschrieben.</span>
<span aria-hidden="true"> &middot; </span>BAVARIAN ALPS &middot; 2025-08-15 </div>
<hr class="rule">
</header>

Expand Down
Loading
Loading