Render the web site at 110% by default

The operator reads the site at 110% browser zoom; make that the default
scale. Tailwind v4 utilities are rem-based, so `html { font-size:110% }`
scales type, spacing, columns and controls together the way zoom does,
while media queries keep firing at the same viewport widths.

Things that did not scale with the root size and had to move:
- the `lg` reader grid is now 15rem + gap-x-8 (was 16rem + gap-x-10) so
  the TOC sidebar and the 68ch article still fit at 1024-1100px;
- the TOC reveal margin in app.js is 1.5rem of the computed root size
  instead of a fixed 24px, and the run funnel SVG height is `h-3`;
- long unbreakable tokens on the phone-width dashboard (settings group
  ids and headings, config warnings, run warnings) now wrap with
  `overflow-wrap:anywhere`, and the feed candidates table sits in the
  same `.scroll-x` wrapper as the other dashboard tables. Overview and
  feeds already overflowed a 390px viewport before this change; settings
  started to. All three fit now.

Verified with before/after screenshots of 27 pages at 390-1440px on the
seeded dev database: no page body scrolls horizontally, the reader
layout, TOC sidebar and dashboard match the old 110%-zoom rendering.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXYGBPoHZSDSfE5WcJ9bvj
This commit is contained in:
2026-09-09 04:50:08 +00:00
co-authored by Claude Fable 5.1
parent b02e1c7b8c
commit c707f432f2
11 changed files with 17 additions and 15 deletions
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -138,12 +138,13 @@ if (tocPanel) {
const current = tocPanel.querySelector(".toc-link[aria-current]");
if (!current || tocPanel.scrollHeight <= tocPanel.clientHeight) return;
const stickyHeader = tocPanel.querySelector(":scope > div");
const topMargin = (stickyHeader && stickyHeader.offsetHeight > 0 ? stickyHeader.offsetHeight : 0) + 24;
const revealMargin = 1.5 * parseFloat(getComputedStyle(document.documentElement).fontSize);
const topMargin = (stickyHeader && stickyHeader.offsetHeight > 0 ? stickyHeader.offsetHeight : 0) + revealMargin;
const panelRect = tocPanel.getBoundingClientRect();
const currentRect = current.getBoundingClientRect();
const top = tocPanel.scrollTop + currentRect.top - panelRect.top;
const visibleTop = tocPanel.scrollTop + topMargin;
const visibleBottom = tocPanel.scrollTop + tocPanel.clientHeight - 24;
const visibleBottom = tocPanel.scrollTop + tocPanel.clientHeight - revealMargin;
if (top >= visibleTop && top + currentRect.height <= visibleBottom) return;
const target = Math.max(0, top - Math.max(topMargin, (tocPanel.clientHeight - currentRect.height) / 2));
tocPanel.scrollTo({ top: target, behavior: reducedMotion.matches ? "auto" : "smooth" });
+2 -1
View File
@@ -114,7 +114,8 @@
@layer base {
[hidden] { display:none !important; }
html { @apply bg-paper text-ink antialiased; }
/* Match the site's preferred 110% browser-zoom reading scale by default. */
html { font-size:110%; @apply bg-paper text-ink antialiased; }
body { @apply m-0 min-h-screen bg-paper font-serif text-[1.0625rem] leading-[1.65] text-ink; font-optical-sizing:auto; }
::selection { background:color-mix(in oklab, var(--accent) 24%, transparent); }
a { @apply text-accent decoration-rule underline-offset-4 transition-colors duration-150 hover:text-accent-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-paper; }
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "layout.html" %}{% block ears %}<span class="sm:hidden">{{ toc.short_date }}</span><span class="hidden sm:inline">{{ toc.display_date }} · No. {{ toc.issue_number }}</span>{% endblock %}{% block content %}<div class="mx-auto max-w-7xl lg:grid lg:grid-cols-[16rem_minmax(0,1fr)] lg:gap-x-10 lg:px-6 xl:grid-cols-[18rem_minmax(0,1fr)] xl:gap-x-14">{% include "_toc.html" %}
{% extends "layout.html" %}{% block ears %}<span class="sm:hidden">{{ toc.short_date }}</span><span class="hidden sm:inline">{{ toc.display_date }} · No. {{ toc.issue_number }}</span>{% endblock %}{% block content %}<div class="mx-auto max-w-7xl lg:grid lg:grid-cols-[15rem_minmax(0,1fr)] lg:gap-x-8 lg:px-6 xl:grid-cols-[18rem_minmax(0,1fr)] xl:gap-x-14">{% include "_toc.html" %}
<article class="reader-page mx-auto mt-10 w-full max-w-[68ch] px-4 sm:px-6 lg:mt-12 lg:px-0" data-toc-scroll>
<header><p class="font-sans text-[0.72rem] uppercase tracking-[0.12em] text-muted">Article</p><h1 class="mt-3 text-4xl font-semibold leading-[1.1] tracking-[-0.01em] sm:text-5xl"><a class="text-ink no-underline hover:text-accent" href="{{ source_url }}">{{ title }}</a></h1>{% match byline %}{% when Some with (byline) %}<p class="mt-5 font-sans text-sm font-medium text-ink-2">{{ byline }}</p>{% when None %}{% endmatch %}<p class="mt-1 font-sans text-sm text-muted">{{ meta_line }}</p>{% match why %}{% when Some with (why) %}<p class="mt-6 border-l-2 border-accent pl-3 italic text-ink-2">Why it's here: {{ why }}</p>{% when None %}{% endmatch %}{% match understanding %}{% when Some with (line) %}<p class="mt-4 font-sans text-sm text-muted">{{ line }}</p>{% when None %}{% endmatch %}{% match social_line %}{% when Some with (social) %}<p class="mt-4 font-sans text-sm text-muted">{{ social }}</p>{% when None %}{% endmatch %}{% match summary %}{% when Some with (summary) %}<p class="mt-7 text-xl italic leading-relaxed text-ink-2">{{ summary }}</p>{% when None %}{% endmatch %}{% if excerpt_only %}<p class="notice mt-6">Excerpt only — continue reading at the original site.</p>{% endif %}</header>
<div class="prose-body mt-10 border-t border-rule pt-8">{{ body_html|safe }}</div>
+1 -1
View File
@@ -1,3 +1,3 @@
{% extends "layout.html" %}{% block ears %}<span class="sm:hidden">{{ toc.short_date }}</span><span class="hidden sm:inline">{{ toc.display_date }} · No. {{ toc.issue_number }}</span>{% endblock %}{% block content %}<div class="mx-auto max-w-7xl lg:grid lg:grid-cols-[16rem_minmax(0,1fr)] lg:gap-x-10 lg:px-6 xl:grid-cols-[18rem_minmax(0,1fr)] xl:gap-x-14">{% include "_toc.html" %}
{% extends "layout.html" %}{% block ears %}<span class="sm:hidden">{{ toc.short_date }}</span><span class="hidden sm:inline">{{ toc.display_date }} · No. {{ toc.issue_number }}</span>{% endblock %}{% block content %}<div class="mx-auto max-w-7xl lg:grid lg:grid-cols-[15rem_minmax(0,1fr)] lg:gap-x-8 lg:px-6 xl:grid-cols-[18rem_minmax(0,1fr)] xl:gap-x-14">{% include "_toc.html" %}
<article class="reader-page mx-auto mt-10 w-full max-w-[68ch] px-4 sm:px-6 lg:mt-12 lg:px-0"><header class="mb-9"><p class="font-sans text-[0.72rem] uppercase tracking-[0.12em] text-muted">Edition notes</p><h1 class="mt-2 text-4xl font-semibold leading-[1.1] tracking-[-0.01em] sm:text-5xl">Behind the paper</h1></header><section class="border-y border-rule py-6"><p>{{ summary_line }}</p><p class="mt-3">{{ admitted_line }}</p><p class="mt-3">{{ learned_line }}</p></section><section class="mt-10"><h2 class="border-t border-rule pt-2 font-sans text-[0.72rem] font-semibold uppercase tracking-[0.12em] text-muted">Near misses</h2><p class="mt-5 italic text-ink-2">Highest utility not selected.</p>{% if near_misses.is_empty() %}<p class="mt-4 text-muted">None recorded for this run.</p>{% else %}<ol class="mt-5 list-decimal space-y-3 pl-6">{% for near_miss in near_misses %}<li>{% if page.is_admin() %}<a class="text-ink decoration-rule hover:decoration-accent" href="/dashboard/articles/{{ near_miss.article_id }}">{{ near_miss.line }}</a>{% else %}{{ near_miss.line }}{% endif %}</li>{% endfor %}</ol>{% endif %}</section><p class="mt-10 border-l-2 border-rule pl-4 italic text-ink-2">{{ models_line }}</p><p class="mt-10 border-t border-rule pt-5 font-sans text-sm"><a href="{{ issue_href }}">← Back to this issue</a></p></article>
</div>{% endblock %}
+2 -2
View File
@@ -6,7 +6,7 @@
<nav class="tabs" aria-label="Candidate status"><a href="/dashboard/feeds"{% if status == "candidate" %} class="active" aria-current="page"{% endif %}>Candidates ({{ candidate_count }})</a> <a href="/dashboard/feeds?status=added"{% if status == "added" %} class="active" aria-current="page"{% endif %}>Added ({{ added_count }})</a> <a href="/dashboard/feeds?status=dismissed"{% if status == "dismissed" %} class="active" aria-current="page"{% endif %}>Dismissed ({{ dismissed_count }})</a></nav>
{% if let Some(error) = categories_error %}<p class="notice">{{ error }}</p>{% endif %}
{% include "dashboard/_pager.html" %}
<table class="table-fixed">
<div class="scroll-x"><table class="table-fixed">
<thead><tr><th class="num w-14">Score</th><th>Feed</th><th>Why</th><th class="num w-20">Articles</th><th class="w-20">Seen</th><th class="w-60 lg:w-84"><span class="sr-only">Actions</span></th></tr></thead>
<tbody>{% for row in rows %}<tr>
<td class="num tabular-nums">{{ row.score }}</td>
@@ -16,6 +16,6 @@
<td class="cell-tight text-xs text-muted"><span title="First seen {{ row.first_seen }} · last seen {{ row.last_seen }}">{{ row.seen }}</span></td>
<td>{% if status == "candidate" %}<div class="flex flex-wrap items-start gap-2"><form class="form-inline flex-nowrap" method="post" action="/dashboard/feeds/{{ row.id }}/add"><select class="min-w-0 max-w-36 truncate" name="category_id" aria-label="Category for {{ row.label }}"{% if categories.is_empty() %} disabled{% endif %}>{% for category in categories %}<option value="{{ category.id }}"{% if category.id == selected_category %} selected{% endif %}>{{ category.title }}</option>{% endfor %}</select><button class="btn-primary" type="submit"{% if categories.is_empty() %} disabled title="Miniflux categories are unavailable"{% endif %}>Add</button></form><form method="post" action="/dashboard/feeds/{{ row.id }}/dismiss"><button class="btn" type="submit">Dismiss</button></form></div>{% else %}<span class="muted text-xs">{{ row.decided_at }}{% if let Some(href) = row.miniflux_href %} · <a href="{{ href }}" rel="noopener" target="_blank">in Miniflux</a>{% endif %}</span>{% endif %}</td>
</tr>{% endfor %}{% if rows.is_empty() %}<tr><td colspan="6" class="text-muted">No feed candidates with this status. The discovery stage adds them during a run; <code>daily-epub feeds discover</code> backfills from recent articles.</td></tr>{% endif %}</tbody>
</table>
</table></div>
{% include "dashboard/_pager.html" %}
</section>{% endblock %}
+1 -1
View File
@@ -42,7 +42,7 @@
<p class="text-sm"><a href="/dashboard/jobs">All jobs</a></p>
</section>
{% if !config_warnings.is_empty() %}<section class="card"><h2>Config on disk</h2>
<ul class="warnings error list-disc pl-5">{% for line in config_warnings %}<li>{{ line }}</li>{% endfor %}</ul>
<ul class="warnings error list-disc pl-5 [overflow-wrap:anywhere]">{% for line in config_warnings %}<li>{{ line }}</li>{% endfor %}</ul>
<p class="text-sm"><a href="/dashboard/settings">Settings</a></p>
</section>{% endif %}
</div>
+2 -2
View File
@@ -18,7 +18,7 @@
<thead><tr><th>stage</th><th>share</th><th class="num">reached</th><th class="num">stopped here</th><th>why</th></tr></thead>
<tbody>{% for stage in funnel %}<tr>
<td class="cell-tight align-middle"><span class="badge {{ stage.stage }}">{{ stage.stage }}</span></td>
<td class="funnel-cell"><div class="funnel"><svg width="{{ stage.percent }}%" height="12" role="img" aria-label="{{ stage.reached }} reached"></svg></div></td>
<td class="funnel-cell"><div class="funnel"><svg class="h-3" width="{{ stage.percent }}%" role="img" aria-label="{{ stage.reached }} reached"></svg></div></td>
<td class="num align-middle">{{ stage.reached }}</td>
<td class="num align-middle text-muted">{{ stage.stopped }}</td>
<td class="reasons cell-wrap align-middle text-muted">{% for reason in stage.reasons %}{{ reason.reason }} <span class="tabular-nums text-ink">{{ reason.count }}</span>{% if !loop.last %} · {% endif %}{% endfor %}</td>
@@ -34,7 +34,7 @@
</dl></section>
<section class="card"><h2>Timings</h2><div class="scroll-x"><table><thead><tr><th>stage</th><th class="num">seconds</th></tr></thead><tbody>{% for line in timings %}<tr><td>{{ line.stage }}</td><td class="num">{{ line.seconds }}</td></tr>{% endfor %}<tr><th>total</th><th class="num">{{ timings_total }}</th></tr></tbody></table></div></section>
<section class="card"><h2>Provider usage</h2><div class="scroll-x"><table><thead><tr><th>provider</th><th class="num">in</th><th class="num">cached</th><th class="num">cache write</th><th class="num">out</th><th class="num">cost</th></tr></thead><tbody>{% for line in providers %}<tr><td>{{ line.provider }}</td><td class="num">{{ line.input }}</td><td class="num">{{ line.cached }}</td><td class="num">{{ line.cache_write }}</td><td class="num">{{ line.output }}</td><td class="num">{{ line.cost }}</td></tr>{% endfor %}</tbody></table></div></section>
<section class="card" id="warnings"><h2>Warnings</h2>{% if warnings.is_empty() %}<p class="muted">None.</p>{% else %}<ul class="list-disc pl-5 text-warn">{% for warning in warnings %}<li>{{ warning }}</li>{% endfor %}</ul>{% endif %}</section>
<section class="card" id="warnings"><h2>Warnings</h2>{% if warnings.is_empty() %}<p class="muted">None.</p>{% else %}<ul class="list-disc pl-5 text-warn [overflow-wrap:anywhere]">{% for warning in warnings %}<li>{{ warning }}</li>{% endfor %}</ul>{% endif %}</section>
<section class="card"><h2>Feeds (top {{ feeds.len() }})</h2><div><table class="table-fixed"><thead><tr><th>feed</th><th class="num w-20">entries</th></tr></thead><tbody>{% for line in feeds %}<tr><td class="truncate" title="{{ line.name }}">{{ line.name }}</td><td class="num w-20">{{ line.count }}</td></tr>{% endfor %}</tbody></table></div></section>
</div>{% else %}<p class="muted text-sm">This run has no stored report; only the funnel and candidates are available.</p>{% endif %}
+2 -2
View File
@@ -9,8 +9,8 @@
<form method="post" action="/dashboard/settings" class="settings-form" id="settings-form">
{% for group in groups %}<section class="card scroll-mt-4" id="{{ group.id }}">
<div>
<p class="page-eyebrow font-mono normal-case tracking-normal">{{ group.id }}</p>
<h2 class="mt-0.5">{{ group.title }}</h2>
<p class="page-eyebrow font-mono normal-case tracking-normal [overflow-wrap:anywhere]">{{ group.id }}</p>
<h2 class="mt-0.5 [overflow-wrap:anywhere]">{{ group.title }}</h2>
</div>
{% match group.provider %}{% when Some with (provider) %}<p class="meta text-xs">Key: <code>{{ crate::config::ProviderConfig::api_key_env_var(provider.name) }}</code> in the env file. {% if provider.removable() %}Not named by any <code>[llm]</code> role. <button type="submit" form="remove-{{ provider.name }}" class="danger">Remove provider</button>{% else if provider.built_in %}Built in: declared by the defaults, so it can be edited and left unreferenced but not removed.{% else %}Named by <code>llm.{{ provider.roles() }}</code>; reassign the role before removing it.{% endif %}</p>{% when None %}{% endmatch %}
{% if group.is_weights() %}<p class="meta text-xs">Weights need not sum to 1; they are renormalized over the signals present for each article.</p>{% endif %}
+1 -1
View File
@@ -1,5 +1,5 @@
{% extends "layout.html" %}{% block ears %}<span class="sm:hidden">{{ toc.short_date }}</span><span class="hidden sm:inline">{{ toc.display_date }} · No. {{ toc.issue_number }}</span>{% endblock %}{% block content %}
<div class="mx-auto max-w-7xl lg:grid lg:grid-cols-[16rem_minmax(0,1fr)] lg:gap-x-10 lg:px-6 xl:grid-cols-[18rem_minmax(0,1fr)] xl:gap-x-14">{% include "_toc.html" %}
<div class="mx-auto max-w-7xl lg:grid lg:grid-cols-[15rem_minmax(0,1fr)] lg:gap-x-8 lg:px-6 xl:grid-cols-[18rem_minmax(0,1fr)] xl:gap-x-14">{% include "_toc.html" %}
<article class="reader-page mx-auto mt-10 w-full max-w-[68ch] px-4 sm:px-6 lg:mt-12 lg:px-0">
<header class="mb-8 text-center"><p class="font-sans text-[0.72rem] uppercase tracking-[0.12em] text-muted">{{ display_date }} · No. {{ issue_number }}</p><p class="mt-2 font-sans text-sm text-muted">{{ stats_line }}</p></header>
<section aria-labelledby="brief-heading" data-toc-entry="/issues/{{ date }}"><h1 id="brief-heading" class="reader-section-heading">The Brief</h1><div class="editorial prose-body mt-5 [&>p:first-child]:first-letter:float-left [&>p:first-child]:first-letter:mr-2 [&>p:first-child]:first-letter:mt-1 [&>p:first-child]:first-letter:font-serif [&>p:first-child]:first-letter:text-[4.6rem] [&>p:first-child]:first-letter:font-semibold [&>p:first-child]:first-letter:leading-[0.72]">{{ front_page_html|safe }}</div></section>
+1 -1
View File
@@ -1,3 +1,3 @@
{% extends "layout.html" %}{% block ears %}<span class="sm:hidden">{{ toc.short_date }}</span><span class="hidden sm:inline">{{ toc.display_date }} · No. {{ toc.issue_number }}</span>{% endblock %}{% block content %}<div class="mx-auto max-w-7xl lg:grid lg:grid-cols-[16rem_minmax(0,1fr)] lg:gap-x-10 lg:px-6 xl:grid-cols-[18rem_minmax(0,1fr)] xl:gap-x-14">{% include "_toc.html" %}
{% extends "layout.html" %}{% block ears %}<span class="sm:hidden">{{ toc.short_date }}</span><span class="hidden sm:inline">{{ toc.display_date }} · No. {{ toc.issue_number }}</span>{% endblock %}{% block content %}<div class="mx-auto max-w-7xl lg:grid lg:grid-cols-[15rem_minmax(0,1fr)] lg:gap-x-8 lg:px-6 xl:grid-cols-[18rem_minmax(0,1fr)] xl:gap-x-14">{% include "_toc.html" %}
<article class="reader-page mx-auto mt-10 w-full max-w-[68ch] px-4 sm:px-6 lg:mt-12 lg:px-0"><header class="mb-9">{% match display_date %}{% when Some with (date) %}<p class="font-sans text-[0.72rem] uppercase tracking-[0.12em] text-muted">{{ date }}</p>{% when None %}{% endmatch %}<h1 class="mt-2 text-4xl font-semibold leading-[1.1] tracking-[-0.01em] sm:text-5xl">World Briefing</h1></header><div class="prose-body border-t border-rule pt-7">{{ body_html|safe }}</div><p class="mt-10 border-t border-rule pt-5 font-sans text-sm"><a href="{{ issue_href }}">← Back to this issue</a></p></article>
</div>{% endblock %}