The bug: 15px inputs trip iOS auto-zoom

On mobile, tapping any text field zoomed the page in and made the game awkward to use. That's iOS Safari's built-in behavior, not a layout bug: focusing a text control whose font-size is under 16px zooms the viewport to it.

Revisionist's text controls land at 15px — and not from an explicit size, but by inheritance. The body sets the editorial 15px:

The page's 15px base (line 61).

assets/css/main.css · 368 lines
assets/css/main.css368 lines · CSS
⋯ 56 lines hidden (lines 1–56)
1/**
2 * Main CSS — Revisionist.
3 * Tailwind + Nuxt UI, then the "Spine Console" design tokens: an editorial-minimal
4 * read-model aesthetic. Structure comes from hairlines + whitespace + type, never
5 * from cards / shadows / gradient fills. Semantic color lives in 9px dots + signed
6 * numbers, not box fills.
7 */
8 
9@import "tailwindcss";
10@import "@nuxt/ui";
11 
12/* ── Design tokens ─────────────────────────────────────────────────────── */
13/* Warm "ledger / letterpress" palette: aged paper, sepia ink, hairlines like faint
14 rules, and a single terracotta accent (sealing-wax) for everything interactive.
15 Semantic color is warmed too so a swing reads as ink, never an error state — the
16 accent (bright terracotta) stays distinct from negative valence (deep oxblood). */
17:root {
18 --rv-fg: #2b241c; /* sepia ink — ~12:1 on the cream ground */
19 --rv-mut: #685c49; /* readable quiet — ~5.6:1 */
20 --rv-faint: #7d6f57; /* the contrast floor for readable text — ~4.6:1 */
21 --rv-hair: #c9b99c; /* decorative-only (dot rings, ticks); never load-bearing */
22 --rv-line: #e6dcc8; /* sepia hairline */
23 --rv-bg: #faf6ee; /* warm paper */
24 --rv-tint: #f1ebdd; /* a shade deeper paper */
25 --rv-accent: #b1532e; /* terracotta / sealing wax — the one interactive accent */
26 --rv-ok: #466632; /* muted olive-forest */
27 --rv-warn: #8a5a16; /* gold-ochre */
28 --rv-bad: #7c2d26; /* deep oxblood (distinct from the brighter accent) */
29 --rv-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
30 --rv-serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, ui-serif, serif;
31 
32 /* ── Motion vocabulary ─────────────────────────────────────────────── */
33 /* One timing language so every animation feels cut from the same cloth:
34 three durations (micro / standard / expressive) and two curves — a gentle
35 settle and the struck-token overshoot the dice & seal already use. */
36 --rv-dur-1: 140ms; /* micro — hovers, presses, color/position shifts */
37 --rv-dur-2: 280ms; /* standard — reveals, fades, fills */
38 --rv-dur-3: 520ms; /* expressive — the dice, the chronicle, node arrivals */
39 --rv-ease: cubic-bezier(.2, .7, .3, 1); /* settle (ease-out) */
40 --rv-ease-spring: cubic-bezier(.2, 1.4, .4, 1); /* the struck-token overshoot */
42 
43.dark {
44 --rv-fg: #ece4d6; /* warm off-white ink */
45 --rv-mut: #b3a892;
46 --rv-faint: #978c77; /* ~6:1 on the warm near-black */
47 --rv-hair: #4a4133;
48 --rv-line: #2f2820;
49 --rv-bg: #14110d; /* warm near-black (brown undertone) */
50 --rv-tint: #1c1813;
51 --rv-accent: #db7a4f; /* lifted terracotta */
52 --rv-ok: #74b35f;
53 --rv-warn: #d8a34d;
54 --rv-bad: #e08a72;
56 
57body {
58 background: var(--rv-bg);
59 color: var(--rv-fg);
60 font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
61 font-size: 15px;
62 line-height: 1.55;
⋯ 305 lines hidden (lines 64–368)
64 
65/* Browser-level chrome, warmed to match the page: text selection + scrollbars. */
66::selection { background: color-mix(in srgb, var(--rv-accent) 24%, transparent); }
67* { scrollbar-width: thin; scrollbar-color: var(--rv-hair) transparent; }
68::-webkit-scrollbar { width: 11px; height: 11px; }
69::-webkit-scrollbar-track { background: transparent; }
70::-webkit-scrollbar-thumb { background: var(--rv-hair); border-radius: 999px; border: 3px solid var(--rv-bg); }
71::-webkit-scrollbar-thumb:hover { background: var(--rv-faint); }
72 
73/* ── Token helper classes (pair with Tailwind layout utilities) ─────────── */
74.rv-fg { color: var(--rv-fg); }
75.rv-muted { color: var(--rv-mut); }
76.rv-hair-c { color: var(--rv-hair); }
77.rv-hover:hover { background: var(--rv-tint); }
78.rv-hover-fg { color: var(--rv-mut); }
79.rv-hover-fg:hover { color: var(--rv-fg); }
80/* First-turn quieting of the empty read-model, so the eye lands on the dock. */
81.rv-dim { opacity: 0.5; transition: opacity var(--rv-dur-2) var(--rv-ease); }
82@media (prefers-reduced-motion: reduce) { .rv-dim { transition: none; } }
83.rv-faint { color: var(--rv-faint); }
84.rv-accent { color: var(--rv-accent); }
85.rv-bg { background: var(--rv-bg); }
86.rv-tint { background: var(--rv-tint); }
87.rv-mono { font-family: var(--rv-mono); }
88 
89.rv-ok { color: var(--rv-ok); }
90.rv-warn { color: var(--rv-warn); }
91.rv-bad { color: var(--rv-bad); }
92 
93/* Pair with Tailwind border / border-t / border-b / border-r / border-l. */
94.rv-line { border-color: var(--rv-line); }
95 
96/* A "section" header = an 11px uppercase muted label + a 1px underline. The card. */
97/* A text-style modifier only — does NOT force display, so `flex`/`hidden`/`md:inline`
98 utilities on the same element still win (a prior `display:block` here silently
99 broke the spine header's flex layout and the objective era-tag's responsive hide). */
100.rv-label {
101 font-size: 11px;
102 text-transform: uppercase;
103 letter-spacing: 0.1em;
104 font-weight: 600;
105 color: var(--rv-mut);
107.rv-label--rule {
108 display: block;
109 padding-bottom: 6px;
110 border-bottom: 1px solid var(--rv-line);
111 margin-bottom: 12px;
113 
114/* 9px status / valence dots — the symbol that replaces every colored fill. */
115.rv-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; flex: none; }
116.rv-dot--ok { background: var(--rv-ok); }
117.rv-dot--warn { background: var(--rv-warn); }
118.rv-dot--bad { background: var(--rv-bad); }
119.rv-dot--mut { background: var(--rv-faint); }
120.rv-dot--accent { background: var(--rv-accent); }
121.rv-dot--hollow { background: transparent; border: 1.5px solid var(--rv-hair); }
122 
123/* One serif voice — titles + Chronicle prose ONLY (never UI labels or numerals). */
124.rv-serif { font-family: var(--rv-serif); }
125 
126/* Inputs: 1px hairline, ~2px radius, accent focus outline. No shadow, no chrome. */
127.rv-input {
128 width: 100%;
129 background: var(--rv-bg);
130 color: var(--rv-fg);
131 border: 1px solid var(--rv-line);
132 border-radius: 2px;
133 padding: 9px 11px;
134 font: inherit;
135 /* Engraved double-rule — the composer reads as a framed note (echoes the die). */
136 box-shadow: inset 0 0 0 3px var(--rv-bg), inset 0 0 0 4px var(--rv-line);
138.rv-input::placeholder { color: var(--rv-faint); }
139.rv-input:focus,
140.rv-input:focus-visible {
141 outline: 2px solid var(--rv-accent);
142 outline-offset: -1px;
143 border-color: transparent;
145 
146/* Buttons: text-forward, hairline; primary fills with the one accent. */
147.rv-btn {
148 font: inherit;
149 font-weight: 600;
150 color: var(--rv-fg);
151 background: transparent;
152 border: 1px solid var(--rv-line);
153 border-radius: 2px;
154 padding: 6px 14px;
155 cursor: pointer;
156 transition: border-color var(--rv-dur-1) var(--rv-ease), color var(--rv-dur-1) var(--rv-ease),
157 background var(--rv-dur-1) var(--rv-ease), opacity var(--rv-dur-1) var(--rv-ease),
158 transform var(--rv-dur-1) var(--rv-ease);
160.rv-btn:hover:not(:disabled) { border-color: var(--rv-fg); }
161.rv-btn:active:not(:disabled) { transform: translateY(0.5px) scale(0.975); }
162.rv-btn:focus-visible { outline: 2px solid var(--rv-accent); outline-offset: 1px; }
163.rv-btn:disabled { opacity: 0.5; cursor: not-allowed; }
164.rv-btn--primary {
165 color: #fff;
166 background: var(--rv-accent);
167 border-color: var(--rv-accent);
169.dark .rv-btn--primary { color: #0d0d0d; }
170.rv-btn--primary:hover:not(:disabled) { filter: brightness(1.08); border-color: var(--rv-accent); }
171/* Disabled primary reads as a flat, inert neutral — NOT a dimmed accent — so the
172 enabled→disabled jump is unmistakable and the resting CTA never looks faded. */
173.rv-btn--primary:disabled {
174 color: var(--rv-faint);
175 background: transparent;
176 border-color: var(--rv-line);
177 opacity: 1;
179 
180/* A real, on-brand focus ring on every bespoke interactive element (the .rv-input
181 handles its own). One accent; never the suppressed default. */
182button:focus-visible,
183a:focus-visible,
184summary:focus-visible,
185[role="button"]:focus-visible,
186[tabindex]:focus-visible {
187 outline: 2px solid var(--rv-accent);
188 outline-offset: 2px;
189 border-radius: 3px;
191 
192/* Invisible 44px hit area for the editorial-tiny icon controls (padding, not size). */
193.rv-tap {
194 display: inline-flex;
195 align-items: center;
196 justify-content: center;
197 min-width: 40px;
198 min-height: 40px;
200 
201/* Ruled "fill-in" field — a single rule like a blank on a ledger form / letter.
202 Used for the short text inputs; the multi-line composer keeps a boxed surface. */
203.rv-field {
204 width: 100%;
205 background: transparent;
206 color: var(--rv-fg);
207 border: 0;
208 border-bottom: 2px solid var(--rv-hair);
209 border-radius: 0;
210 padding: 6px 2px;
211 font: inherit;
212 transition: border-color var(--rv-dur-1) var(--rv-ease), background-color var(--rv-dur-1) var(--rv-ease);
214.rv-field::placeholder { color: var(--rv-faint); }
215.rv-field:focus,
216.rv-field:focus-visible {
217 outline: none;
218 border-bottom-color: var(--rv-accent);
219 background: var(--rv-tint);
221 
222/* Mobile Safari zooms the viewport when a focused text control's font-size is
223 under 16px (both .rv-field and .rv-input inherit the body's 15px). Pin them to
224 16px on touch devices so tapping a field no longer zooms the page in — without
225 touching the viewport meta (disabling pinch-zoom would be an a11y regression).
226 Desktop keeps the smaller editorial size. */
227@media (pointer: coarse) {
228 .rv-field,
229 .rv-input {
230 font-size: 16px;
231 }
233 
234/* A mounted card-stock surface — a filing card with an engraved inner rule. */
235.rv-card {
236 background: var(--rv-tint);
237 border: 1px solid var(--rv-line);
238 border-radius: 3px;
239 box-shadow: inset 0 0 0 3px var(--rv-tint), inset 0 0 0 4px var(--rv-line);
241 
242/* An illuminated initial for the Chronicle prose — the one ornament a hand-written
243 history book earns. Applies only to the first paragraph of a telling. */
244[data-testid="chronicle-body"] p:first-of-type::first-letter,
245[data-testid="end-chronicle-body"] p:first-of-type::first-letter {
246 font-family: var(--rv-serif);
247 font-size: 2.8em;
248 line-height: 0.78;
249 float: left;
250 margin: 0.04em 0.09em 0 0;
251 color: var(--rv-accent);
252 font-weight: 600;
254 
255/* The lifetime slider as a timeline playhead: a thin ruled track + a terracotta
256 marker bar (not a default round handle). */
257input[type="range"] {
258 -webkit-appearance: none;
259 appearance: none;
260 width: 100%;
261 height: 18px;
262 background: transparent;
263 cursor: pointer;
265input[type="range"]::-webkit-slider-runnable-track { height: 2px; background: var(--rv-line); }
266input[type="range"]::-moz-range-track { height: 2px; background: var(--rv-line); }
267input[type="range"]::-webkit-slider-thumb {
268 -webkit-appearance: none; appearance: none;
269 width: 5px; height: 16px; margin-top: -7px;
270 background: var(--rv-accent); border: 0; border-radius: 1.5px;
272input[type="range"]::-moz-range-thumb {
273 width: 5px; height: 16px; background: var(--rv-accent); border: 0; border-radius: 1.5px;
275input[type="range"]:focus-visible { outline: 2px solid var(--rv-accent); outline-offset: 4px; }
276 
277/* Thin tracks: the only "bars" (progress, char ammo). Hairline, never a pill. */
278.rv-track { height: 6px; background: var(--rv-line); border-radius: 999px; overflow: hidden; }
279.rv-track > .rv-fill { height: 100%; background: var(--rv-fg); border-radius: 999px; transition: width 0.4s var(--rv-ease); }
280 
281/* Collapsible rails (progressive disclosure) — ▸/▾ markers, hairline-topped. */
282.rv-rail { border-top: 1px solid var(--rv-line); padding-top: 10px; }
283.rv-summary { cursor: pointer; list-style: none; display: flex; align-items: center; gap: 8px; }
284.rv-summary::-webkit-details-marker { display: none; }
285.rv-summary::before { content: "\25B8"; color: var(--rv-faint); font-size: 11px; line-height: 1; }
286details[open] > .rv-summary::before { content: "\25BE"; }
287/* A forced-open rail (desktop Chronicle): the summary is a static label, not a toggle. */
288.rv-summary--static { cursor: default; pointer-events: none; }
289.rv-summary--static::before { display: none; }
290 
291/* ── Motion & state primitives ─────────────────────────────────────────── */
292 
293/* Tactile press — non-button interactive surfaces (chips, nodes, tabs) give a
294 little under the finger, the same as .rv-btn. Pair with the press target. */
295.rv-press { transition: transform var(--rv-dur-1) var(--rv-ease), background var(--rv-dur-1) var(--rv-ease), border-color var(--rv-dur-1) var(--rv-ease), color var(--rv-dur-1) var(--rv-ease); }
296.rv-press:active { transform: scale(0.97); }
297 
298/* The one spinner — a struck ring, for every "consulting the record…" wait.
299 Sizes to its font (em), inherits ink, so it drops into any text run. */
300.rv-spinner {
301 display: inline-block;
302 width: 0.9em; height: 0.9em;
303 border: 2px solid currentColor;
304 border-bottom-color: transparent;
305 border-radius: 50%;
306 animation: rv-spin 0.7s linear infinite;
307 vertical-align: -0.12em;
309@keyframes rv-spin { to { transform: rotate(360deg); } }
310 
311/* Pending shimmer — a slow ink wash over a block the system is still composing
312 (skeleton lines while the chronicler writes). */
313.rv-shimmer {
314 background: linear-gradient(90deg, var(--rv-line) 20%, var(--rv-hair) 40%, var(--rv-line) 60%);
315 background-size: 200% 100%;
316 animation: rv-shimmer 1.5s ease-in-out infinite;
317 border-radius: 2px;
319@keyframes rv-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
320 
321/* Waiting mode — a thin indeterminate sweep under the masthead while a turn is
322 being resolved: the whole board reads as "history is being rewritten." */
323.rv-loading-bar { position: relative; height: 2px; overflow: hidden; background: var(--rv-line); }
324.rv-loading-bar::after {
325 content: "";
326 position: absolute; inset: 0;
327 background: var(--rv-accent);
328 transform-origin: left;
329 animation: rv-load-sweep 1.15s var(--rv-ease) infinite;
331@keyframes rv-load-sweep {
332 0% { transform: translateX(-100%) scaleX(0.35); }
333 55% { transform: translateX(40%) scaleX(0.6); }
334 100% { transform: translateX(160%) scaleX(0.35); }
336 
337/* A quiet inline hint — teaches without nagging; a faint aside led by a small
338 accent pointer. */
339.rv-hint { font-size: 11px; line-height: 1.4; color: var(--rv-faint); }
340.rv-hint::before { content: "\203A\00a0\00a0"; color: var(--rv-accent); font-weight: 700; }
341 
342/* Entrances. panel-in is transform-only so it never fights .rv-dim's opacity, and
343 it replays on display:none→block (the phone tab switch). fade-in is the soft
344 reveal for whole views (board, end screen). */
345.rv-panel { animation: rv-panel-in var(--rv-dur-2) var(--rv-ease); }
346@keyframes rv-panel-in { from { transform: translateY(8px); } to { transform: translateY(0); } }
347.rv-fade-in { animation: rv-fade-in var(--rv-dur-3) var(--rv-ease) both; }
348@keyframes rv-fade-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
349 
350/* Value-change pop — the headline % gives a quick struck-token bump when it moves
351 (the text stays in lockstep with the store; this is purely the flourish). */
352.pct-pop { animation: rv-pct-pop var(--rv-dur-2) var(--rv-ease-spring); }
353@keyframes rv-pct-pop { 0% { transform: scale(1); } 35% { transform: scale(1.14); } 100% { transform: scale(1); } }
354 
355/* One place that honors reduced-motion for every shared primitive above. */
356@media (prefers-reduced-motion: reduce) {
357 .rv-spinner { animation-duration: 1.4s; }
358 .rv-shimmer,
359 .rv-loading-bar::after,
360 .rv-panel,
361 .rv-fade-in,
362 .pct-pop { animation: none; }
363 .rv-fade-in { opacity: 1; transform: none; }
364 .rv-press:active,
365 .rv-btn:active:not(:disabled) { transform: none; }
366 /* The bar fills jump rather than slide (the width tween is motion too). */
367 .rv-track > .rv-fill { transition: none; }

…and the field classes opt into it with font: inherit, whose shorthand carries font-size through. So .rv-field (the figure and Archive-lookup inputs) and .rv-input (the message composer) both render at 15px — one pixel under the threshold, which is all iOS needs.

font: inherit (line 211) pulls the body's 15px onto the field; .rv-input does the same.

assets/css/main.css · 368 lines
assets/css/main.css368 lines · CSS
⋯ 202 lines hidden (lines 1–202)
1/**
2 * Main CSS — Revisionist.
3 * Tailwind + Nuxt UI, then the "Spine Console" design tokens: an editorial-minimal
4 * read-model aesthetic. Structure comes from hairlines + whitespace + type, never
5 * from cards / shadows / gradient fills. Semantic color lives in 9px dots + signed
6 * numbers, not box fills.
7 */
8 
9@import "tailwindcss";
10@import "@nuxt/ui";
11 
12/* ── Design tokens ─────────────────────────────────────────────────────── */
13/* Warm "ledger / letterpress" palette: aged paper, sepia ink, hairlines like faint
14 rules, and a single terracotta accent (sealing-wax) for everything interactive.
15 Semantic color is warmed too so a swing reads as ink, never an error state — the
16 accent (bright terracotta) stays distinct from negative valence (deep oxblood). */
17:root {
18 --rv-fg: #2b241c; /* sepia ink — ~12:1 on the cream ground */
19 --rv-mut: #685c49; /* readable quiet — ~5.6:1 */
20 --rv-faint: #7d6f57; /* the contrast floor for readable text — ~4.6:1 */
21 --rv-hair: #c9b99c; /* decorative-only (dot rings, ticks); never load-bearing */
22 --rv-line: #e6dcc8; /* sepia hairline */
23 --rv-bg: #faf6ee; /* warm paper */
24 --rv-tint: #f1ebdd; /* a shade deeper paper */
25 --rv-accent: #b1532e; /* terracotta / sealing wax — the one interactive accent */
26 --rv-ok: #466632; /* muted olive-forest */
27 --rv-warn: #8a5a16; /* gold-ochre */
28 --rv-bad: #7c2d26; /* deep oxblood (distinct from the brighter accent) */
29 --rv-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
30 --rv-serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, ui-serif, serif;
31 
32 /* ── Motion vocabulary ─────────────────────────────────────────────── */
33 /* One timing language so every animation feels cut from the same cloth:
34 three durations (micro / standard / expressive) and two curves — a gentle
35 settle and the struck-token overshoot the dice & seal already use. */
36 --rv-dur-1: 140ms; /* micro — hovers, presses, color/position shifts */
37 --rv-dur-2: 280ms; /* standard — reveals, fades, fills */
38 --rv-dur-3: 520ms; /* expressive — the dice, the chronicle, node arrivals */
39 --rv-ease: cubic-bezier(.2, .7, .3, 1); /* settle (ease-out) */
40 --rv-ease-spring: cubic-bezier(.2, 1.4, .4, 1); /* the struck-token overshoot */
42 
43.dark {
44 --rv-fg: #ece4d6; /* warm off-white ink */
45 --rv-mut: #b3a892;
46 --rv-faint: #978c77; /* ~6:1 on the warm near-black */
47 --rv-hair: #4a4133;
48 --rv-line: #2f2820;
49 --rv-bg: #14110d; /* warm near-black (brown undertone) */
50 --rv-tint: #1c1813;
51 --rv-accent: #db7a4f; /* lifted terracotta */
52 --rv-ok: #74b35f;
53 --rv-warn: #d8a34d;
54 --rv-bad: #e08a72;
56 
57body {
58 background: var(--rv-bg);
59 color: var(--rv-fg);
60 font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
61 font-size: 15px;
62 line-height: 1.55;
64 
65/* Browser-level chrome, warmed to match the page: text selection + scrollbars. */
66::selection { background: color-mix(in srgb, var(--rv-accent) 24%, transparent); }
67* { scrollbar-width: thin; scrollbar-color: var(--rv-hair) transparent; }
68::-webkit-scrollbar { width: 11px; height: 11px; }
69::-webkit-scrollbar-track { background: transparent; }
70::-webkit-scrollbar-thumb { background: var(--rv-hair); border-radius: 999px; border: 3px solid var(--rv-bg); }
71::-webkit-scrollbar-thumb:hover { background: var(--rv-faint); }
72 
73/* ── Token helper classes (pair with Tailwind layout utilities) ─────────── */
74.rv-fg { color: var(--rv-fg); }
75.rv-muted { color: var(--rv-mut); }
76.rv-hair-c { color: var(--rv-hair); }
77.rv-hover:hover { background: var(--rv-tint); }
78.rv-hover-fg { color: var(--rv-mut); }
79.rv-hover-fg:hover { color: var(--rv-fg); }
80/* First-turn quieting of the empty read-model, so the eye lands on the dock. */
81.rv-dim { opacity: 0.5; transition: opacity var(--rv-dur-2) var(--rv-ease); }
82@media (prefers-reduced-motion: reduce) { .rv-dim { transition: none; } }
83.rv-faint { color: var(--rv-faint); }
84.rv-accent { color: var(--rv-accent); }
85.rv-bg { background: var(--rv-bg); }
86.rv-tint { background: var(--rv-tint); }
87.rv-mono { font-family: var(--rv-mono); }
88 
89.rv-ok { color: var(--rv-ok); }
90.rv-warn { color: var(--rv-warn); }
91.rv-bad { color: var(--rv-bad); }
92 
93/* Pair with Tailwind border / border-t / border-b / border-r / border-l. */
94.rv-line { border-color: var(--rv-line); }
95 
96/* A "section" header = an 11px uppercase muted label + a 1px underline. The card. */
97/* A text-style modifier only — does NOT force display, so `flex`/`hidden`/`md:inline`
98 utilities on the same element still win (a prior `display:block` here silently
99 broke the spine header's flex layout and the objective era-tag's responsive hide). */
100.rv-label {
101 font-size: 11px;
102 text-transform: uppercase;
103 letter-spacing: 0.1em;
104 font-weight: 600;
105 color: var(--rv-mut);
107.rv-label--rule {
108 display: block;
109 padding-bottom: 6px;
110 border-bottom: 1px solid var(--rv-line);
111 margin-bottom: 12px;
113 
114/* 9px status / valence dots — the symbol that replaces every colored fill. */
115.rv-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; flex: none; }
116.rv-dot--ok { background: var(--rv-ok); }
117.rv-dot--warn { background: var(--rv-warn); }
118.rv-dot--bad { background: var(--rv-bad); }
119.rv-dot--mut { background: var(--rv-faint); }
120.rv-dot--accent { background: var(--rv-accent); }
121.rv-dot--hollow { background: transparent; border: 1.5px solid var(--rv-hair); }
122 
123/* One serif voice — titles + Chronicle prose ONLY (never UI labels or numerals). */
124.rv-serif { font-family: var(--rv-serif); }
125 
126/* Inputs: 1px hairline, ~2px radius, accent focus outline. No shadow, no chrome. */
127.rv-input {
128 width: 100%;
129 background: var(--rv-bg);
130 color: var(--rv-fg);
131 border: 1px solid var(--rv-line);
132 border-radius: 2px;
133 padding: 9px 11px;
134 font: inherit;
135 /* Engraved double-rule — the composer reads as a framed note (echoes the die). */
136 box-shadow: inset 0 0 0 3px var(--rv-bg), inset 0 0 0 4px var(--rv-line);
138.rv-input::placeholder { color: var(--rv-faint); }
139.rv-input:focus,
140.rv-input:focus-visible {
141 outline: 2px solid var(--rv-accent);
142 outline-offset: -1px;
143 border-color: transparent;
145 
146/* Buttons: text-forward, hairline; primary fills with the one accent. */
147.rv-btn {
148 font: inherit;
149 font-weight: 600;
150 color: var(--rv-fg);
151 background: transparent;
152 border: 1px solid var(--rv-line);
153 border-radius: 2px;
154 padding: 6px 14px;
155 cursor: pointer;
156 transition: border-color var(--rv-dur-1) var(--rv-ease), color var(--rv-dur-1) var(--rv-ease),
157 background var(--rv-dur-1) var(--rv-ease), opacity var(--rv-dur-1) var(--rv-ease),
158 transform var(--rv-dur-1) var(--rv-ease);
160.rv-btn:hover:not(:disabled) { border-color: var(--rv-fg); }
161.rv-btn:active:not(:disabled) { transform: translateY(0.5px) scale(0.975); }
162.rv-btn:focus-visible { outline: 2px solid var(--rv-accent); outline-offset: 1px; }
163.rv-btn:disabled { opacity: 0.5; cursor: not-allowed; }
164.rv-btn--primary {
165 color: #fff;
166 background: var(--rv-accent);
167 border-color: var(--rv-accent);
169.dark .rv-btn--primary { color: #0d0d0d; }
170.rv-btn--primary:hover:not(:disabled) { filter: brightness(1.08); border-color: var(--rv-accent); }
171/* Disabled primary reads as a flat, inert neutral — NOT a dimmed accent — so the
172 enabled→disabled jump is unmistakable and the resting CTA never looks faded. */
173.rv-btn--primary:disabled {
174 color: var(--rv-faint);
175 background: transparent;
176 border-color: var(--rv-line);
177 opacity: 1;
179 
180/* A real, on-brand focus ring on every bespoke interactive element (the .rv-input
181 handles its own). One accent; never the suppressed default. */
182button:focus-visible,
183a:focus-visible,
184summary:focus-visible,
185[role="button"]:focus-visible,
186[tabindex]:focus-visible {
187 outline: 2px solid var(--rv-accent);
188 outline-offset: 2px;
189 border-radius: 3px;
191 
192/* Invisible 44px hit area for the editorial-tiny icon controls (padding, not size). */
193.rv-tap {
194 display: inline-flex;
195 align-items: center;
196 justify-content: center;
197 min-width: 40px;
198 min-height: 40px;
200 
201/* Ruled "fill-in" field — a single rule like a blank on a ledger form / letter.
202 Used for the short text inputs; the multi-line composer keeps a boxed surface. */
203.rv-field {
204 width: 100%;
205 background: transparent;
206 color: var(--rv-fg);
207 border: 0;
208 border-bottom: 2px solid var(--rv-hair);
209 border-radius: 0;
210 padding: 6px 2px;
211 font: inherit;
212 transition: border-color var(--rv-dur-1) var(--rv-ease), background-color var(--rv-dur-1) var(--rv-ease);
⋯ 155 lines hidden (lines 214–368)
214.rv-field::placeholder { color: var(--rv-faint); }
215.rv-field:focus,
216.rv-field:focus-visible {
217 outline: none;
218 border-bottom-color: var(--rv-accent);
219 background: var(--rv-tint);
221 
222/* Mobile Safari zooms the viewport when a focused text control's font-size is
223 under 16px (both .rv-field and .rv-input inherit the body's 15px). Pin them to
224 16px on touch devices so tapping a field no longer zooms the page in — without
225 touching the viewport meta (disabling pinch-zoom would be an a11y regression).
226 Desktop keeps the smaller editorial size. */
227@media (pointer: coarse) {
228 .rv-field,
229 .rv-input {
230 font-size: 16px;
231 }
233 
234/* A mounted card-stock surface — a filing card with an engraved inner rule. */
235.rv-card {
236 background: var(--rv-tint);
237 border: 1px solid var(--rv-line);
238 border-radius: 3px;
239 box-shadow: inset 0 0 0 3px var(--rv-tint), inset 0 0 0 4px var(--rv-line);
241 
242/* An illuminated initial for the Chronicle prose — the one ornament a hand-written
243 history book earns. Applies only to the first paragraph of a telling. */
244[data-testid="chronicle-body"] p:first-of-type::first-letter,
245[data-testid="end-chronicle-body"] p:first-of-type::first-letter {
246 font-family: var(--rv-serif);
247 font-size: 2.8em;
248 line-height: 0.78;
249 float: left;
250 margin: 0.04em 0.09em 0 0;
251 color: var(--rv-accent);
252 font-weight: 600;
254 
255/* The lifetime slider as a timeline playhead: a thin ruled track + a terracotta
256 marker bar (not a default round handle). */
257input[type="range"] {
258 -webkit-appearance: none;
259 appearance: none;
260 width: 100%;
261 height: 18px;
262 background: transparent;
263 cursor: pointer;
265input[type="range"]::-webkit-slider-runnable-track { height: 2px; background: var(--rv-line); }
266input[type="range"]::-moz-range-track { height: 2px; background: var(--rv-line); }
267input[type="range"]::-webkit-slider-thumb {
268 -webkit-appearance: none; appearance: none;
269 width: 5px; height: 16px; margin-top: -7px;
270 background: var(--rv-accent); border: 0; border-radius: 1.5px;
272input[type="range"]::-moz-range-thumb {
273 width: 5px; height: 16px; background: var(--rv-accent); border: 0; border-radius: 1.5px;
275input[type="range"]:focus-visible { outline: 2px solid var(--rv-accent); outline-offset: 4px; }
276 
277/* Thin tracks: the only "bars" (progress, char ammo). Hairline, never a pill. */
278.rv-track { height: 6px; background: var(--rv-line); border-radius: 999px; overflow: hidden; }
279.rv-track > .rv-fill { height: 100%; background: var(--rv-fg); border-radius: 999px; transition: width 0.4s var(--rv-ease); }
280 
281/* Collapsible rails (progressive disclosure) — ▸/▾ markers, hairline-topped. */
282.rv-rail { border-top: 1px solid var(--rv-line); padding-top: 10px; }
283.rv-summary { cursor: pointer; list-style: none; display: flex; align-items: center; gap: 8px; }
284.rv-summary::-webkit-details-marker { display: none; }
285.rv-summary::before { content: "\25B8"; color: var(--rv-faint); font-size: 11px; line-height: 1; }
286details[open] > .rv-summary::before { content: "\25BE"; }
287/* A forced-open rail (desktop Chronicle): the summary is a static label, not a toggle. */
288.rv-summary--static { cursor: default; pointer-events: none; }
289.rv-summary--static::before { display: none; }
290 
291/* ── Motion & state primitives ─────────────────────────────────────────── */
292 
293/* Tactile press — non-button interactive surfaces (chips, nodes, tabs) give a
294 little under the finger, the same as .rv-btn. Pair with the press target. */
295.rv-press { transition: transform var(--rv-dur-1) var(--rv-ease), background var(--rv-dur-1) var(--rv-ease), border-color var(--rv-dur-1) var(--rv-ease), color var(--rv-dur-1) var(--rv-ease); }
296.rv-press:active { transform: scale(0.97); }
297 
298/* The one spinner — a struck ring, for every "consulting the record…" wait.
299 Sizes to its font (em), inherits ink, so it drops into any text run. */
300.rv-spinner {
301 display: inline-block;
302 width: 0.9em; height: 0.9em;
303 border: 2px solid currentColor;
304 border-bottom-color: transparent;
305 border-radius: 50%;
306 animation: rv-spin 0.7s linear infinite;
307 vertical-align: -0.12em;
309@keyframes rv-spin { to { transform: rotate(360deg); } }
310 
311/* Pending shimmer — a slow ink wash over a block the system is still composing
312 (skeleton lines while the chronicler writes). */
313.rv-shimmer {
314 background: linear-gradient(90deg, var(--rv-line) 20%, var(--rv-hair) 40%, var(--rv-line) 60%);
315 background-size: 200% 100%;
316 animation: rv-shimmer 1.5s ease-in-out infinite;
317 border-radius: 2px;
319@keyframes rv-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
320 
321/* Waiting mode — a thin indeterminate sweep under the masthead while a turn is
322 being resolved: the whole board reads as "history is being rewritten." */
323.rv-loading-bar { position: relative; height: 2px; overflow: hidden; background: var(--rv-line); }
324.rv-loading-bar::after {
325 content: "";
326 position: absolute; inset: 0;
327 background: var(--rv-accent);
328 transform-origin: left;
329 animation: rv-load-sweep 1.15s var(--rv-ease) infinite;
331@keyframes rv-load-sweep {
332 0% { transform: translateX(-100%) scaleX(0.35); }
333 55% { transform: translateX(40%) scaleX(0.6); }
334 100% { transform: translateX(160%) scaleX(0.35); }
336 
337/* A quiet inline hint — teaches without nagging; a faint aside led by a small
338 accent pointer. */
339.rv-hint { font-size: 11px; line-height: 1.4; color: var(--rv-faint); }
340.rv-hint::before { content: "\203A\00a0\00a0"; color: var(--rv-accent); font-weight: 700; }
341 
342/* Entrances. panel-in is transform-only so it never fights .rv-dim's opacity, and
343 it replays on display:none→block (the phone tab switch). fade-in is the soft
344 reveal for whole views (board, end screen). */
345.rv-panel { animation: rv-panel-in var(--rv-dur-2) var(--rv-ease); }
346@keyframes rv-panel-in { from { transform: translateY(8px); } to { transform: translateY(0); } }
347.rv-fade-in { animation: rv-fade-in var(--rv-dur-3) var(--rv-ease) both; }
348@keyframes rv-fade-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
349 
350/* Value-change pop — the headline % gives a quick struck-token bump when it moves
351 (the text stays in lockstep with the store; this is purely the flourish). */
352.pct-pop { animation: rv-pct-pop var(--rv-dur-2) var(--rv-ease-spring); }
353@keyframes rv-pct-pop { 0% { transform: scale(1); } 35% { transform: scale(1.14); } 100% { transform: scale(1); } }
354 
355/* One place that honors reduced-motion for every shared primitive above. */
356@media (prefers-reduced-motion: reduce) {
357 .rv-spinner { animation-duration: 1.4s; }
358 .rv-shimmer,
359 .rv-loading-bar::after,
360 .rv-panel,
361 .rv-fade-in,
362 .pct-pop { animation: none; }
363 .rv-fade-in { opacity: 1; transform: none; }
364 .rv-press:active,
365 .rv-btn:active:not(:disabled) { transform: none; }
366 /* The bar fills jump rather than slide (the width tween is motion too). */
367 .rv-track > .rv-fill { transition: none; }

The fix: 16px on touch, nothing else

One rule clears it: pin the two text-control classes to 16px, but only on touch devices, so the desktop editorial size is left alone.

16px on coarse-pointer devices — covers both field classes.

assets/css/main.css · 368 lines
assets/css/main.css368 lines · CSS
⋯ 221 lines hidden (lines 1–221)
1/**
2 * Main CSS — Revisionist.
3 * Tailwind + Nuxt UI, then the "Spine Console" design tokens: an editorial-minimal
4 * read-model aesthetic. Structure comes from hairlines + whitespace + type, never
5 * from cards / shadows / gradient fills. Semantic color lives in 9px dots + signed
6 * numbers, not box fills.
7 */
8 
9@import "tailwindcss";
10@import "@nuxt/ui";
11 
12/* ── Design tokens ─────────────────────────────────────────────────────── */
13/* Warm "ledger / letterpress" palette: aged paper, sepia ink, hairlines like faint
14 rules, and a single terracotta accent (sealing-wax) for everything interactive.
15 Semantic color is warmed too so a swing reads as ink, never an error state — the
16 accent (bright terracotta) stays distinct from negative valence (deep oxblood). */
17:root {
18 --rv-fg: #2b241c; /* sepia ink — ~12:1 on the cream ground */
19 --rv-mut: #685c49; /* readable quiet — ~5.6:1 */
20 --rv-faint: #7d6f57; /* the contrast floor for readable text — ~4.6:1 */
21 --rv-hair: #c9b99c; /* decorative-only (dot rings, ticks); never load-bearing */
22 --rv-line: #e6dcc8; /* sepia hairline */
23 --rv-bg: #faf6ee; /* warm paper */
24 --rv-tint: #f1ebdd; /* a shade deeper paper */
25 --rv-accent: #b1532e; /* terracotta / sealing wax — the one interactive accent */
26 --rv-ok: #466632; /* muted olive-forest */
27 --rv-warn: #8a5a16; /* gold-ochre */
28 --rv-bad: #7c2d26; /* deep oxblood (distinct from the brighter accent) */
29 --rv-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
30 --rv-serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, ui-serif, serif;
31 
32 /* ── Motion vocabulary ─────────────────────────────────────────────── */
33 /* One timing language so every animation feels cut from the same cloth:
34 three durations (micro / standard / expressive) and two curves — a gentle
35 settle and the struck-token overshoot the dice & seal already use. */
36 --rv-dur-1: 140ms; /* micro — hovers, presses, color/position shifts */
37 --rv-dur-2: 280ms; /* standard — reveals, fades, fills */
38 --rv-dur-3: 520ms; /* expressive — the dice, the chronicle, node arrivals */
39 --rv-ease: cubic-bezier(.2, .7, .3, 1); /* settle (ease-out) */
40 --rv-ease-spring: cubic-bezier(.2, 1.4, .4, 1); /* the struck-token overshoot */
42 
43.dark {
44 --rv-fg: #ece4d6; /* warm off-white ink */
45 --rv-mut: #b3a892;
46 --rv-faint: #978c77; /* ~6:1 on the warm near-black */
47 --rv-hair: #4a4133;
48 --rv-line: #2f2820;
49 --rv-bg: #14110d; /* warm near-black (brown undertone) */
50 --rv-tint: #1c1813;
51 --rv-accent: #db7a4f; /* lifted terracotta */
52 --rv-ok: #74b35f;
53 --rv-warn: #d8a34d;
54 --rv-bad: #e08a72;
56 
57body {
58 background: var(--rv-bg);
59 color: var(--rv-fg);
60 font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
61 font-size: 15px;
62 line-height: 1.55;
64 
65/* Browser-level chrome, warmed to match the page: text selection + scrollbars. */
66::selection { background: color-mix(in srgb, var(--rv-accent) 24%, transparent); }
67* { scrollbar-width: thin; scrollbar-color: var(--rv-hair) transparent; }
68::-webkit-scrollbar { width: 11px; height: 11px; }
69::-webkit-scrollbar-track { background: transparent; }
70::-webkit-scrollbar-thumb { background: var(--rv-hair); border-radius: 999px; border: 3px solid var(--rv-bg); }
71::-webkit-scrollbar-thumb:hover { background: var(--rv-faint); }
72 
73/* ── Token helper classes (pair with Tailwind layout utilities) ─────────── */
74.rv-fg { color: var(--rv-fg); }
75.rv-muted { color: var(--rv-mut); }
76.rv-hair-c { color: var(--rv-hair); }
77.rv-hover:hover { background: var(--rv-tint); }
78.rv-hover-fg { color: var(--rv-mut); }
79.rv-hover-fg:hover { color: var(--rv-fg); }
80/* First-turn quieting of the empty read-model, so the eye lands on the dock. */
81.rv-dim { opacity: 0.5; transition: opacity var(--rv-dur-2) var(--rv-ease); }
82@media (prefers-reduced-motion: reduce) { .rv-dim { transition: none; } }
83.rv-faint { color: var(--rv-faint); }
84.rv-accent { color: var(--rv-accent); }
85.rv-bg { background: var(--rv-bg); }
86.rv-tint { background: var(--rv-tint); }
87.rv-mono { font-family: var(--rv-mono); }
88 
89.rv-ok { color: var(--rv-ok); }
90.rv-warn { color: var(--rv-warn); }
91.rv-bad { color: var(--rv-bad); }
92 
93/* Pair with Tailwind border / border-t / border-b / border-r / border-l. */
94.rv-line { border-color: var(--rv-line); }
95 
96/* A "section" header = an 11px uppercase muted label + a 1px underline. The card. */
97/* A text-style modifier only — does NOT force display, so `flex`/`hidden`/`md:inline`
98 utilities on the same element still win (a prior `display:block` here silently
99 broke the spine header's flex layout and the objective era-tag's responsive hide). */
100.rv-label {
101 font-size: 11px;
102 text-transform: uppercase;
103 letter-spacing: 0.1em;
104 font-weight: 600;
105 color: var(--rv-mut);
107.rv-label--rule {
108 display: block;
109 padding-bottom: 6px;
110 border-bottom: 1px solid var(--rv-line);
111 margin-bottom: 12px;
113 
114/* 9px status / valence dots — the symbol that replaces every colored fill. */
115.rv-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; flex: none; }
116.rv-dot--ok { background: var(--rv-ok); }
117.rv-dot--warn { background: var(--rv-warn); }
118.rv-dot--bad { background: var(--rv-bad); }
119.rv-dot--mut { background: var(--rv-faint); }
120.rv-dot--accent { background: var(--rv-accent); }
121.rv-dot--hollow { background: transparent; border: 1.5px solid var(--rv-hair); }
122 
123/* One serif voice — titles + Chronicle prose ONLY (never UI labels or numerals). */
124.rv-serif { font-family: var(--rv-serif); }
125 
126/* Inputs: 1px hairline, ~2px radius, accent focus outline. No shadow, no chrome. */
127.rv-input {
128 width: 100%;
129 background: var(--rv-bg);
130 color: var(--rv-fg);
131 border: 1px solid var(--rv-line);
132 border-radius: 2px;
133 padding: 9px 11px;
134 font: inherit;
135 /* Engraved double-rule — the composer reads as a framed note (echoes the die). */
136 box-shadow: inset 0 0 0 3px var(--rv-bg), inset 0 0 0 4px var(--rv-line);
138.rv-input::placeholder { color: var(--rv-faint); }
139.rv-input:focus,
140.rv-input:focus-visible {
141 outline: 2px solid var(--rv-accent);
142 outline-offset: -1px;
143 border-color: transparent;
145 
146/* Buttons: text-forward, hairline; primary fills with the one accent. */
147.rv-btn {
148 font: inherit;
149 font-weight: 600;
150 color: var(--rv-fg);
151 background: transparent;
152 border: 1px solid var(--rv-line);
153 border-radius: 2px;
154 padding: 6px 14px;
155 cursor: pointer;
156 transition: border-color var(--rv-dur-1) var(--rv-ease), color var(--rv-dur-1) var(--rv-ease),
157 background var(--rv-dur-1) var(--rv-ease), opacity var(--rv-dur-1) var(--rv-ease),
158 transform var(--rv-dur-1) var(--rv-ease);
160.rv-btn:hover:not(:disabled) { border-color: var(--rv-fg); }
161.rv-btn:active:not(:disabled) { transform: translateY(0.5px) scale(0.975); }
162.rv-btn:focus-visible { outline: 2px solid var(--rv-accent); outline-offset: 1px; }
163.rv-btn:disabled { opacity: 0.5; cursor: not-allowed; }
164.rv-btn--primary {
165 color: #fff;
166 background: var(--rv-accent);
167 border-color: var(--rv-accent);
169.dark .rv-btn--primary { color: #0d0d0d; }
170.rv-btn--primary:hover:not(:disabled) { filter: brightness(1.08); border-color: var(--rv-accent); }
171/* Disabled primary reads as a flat, inert neutral — NOT a dimmed accent — so the
172 enabled→disabled jump is unmistakable and the resting CTA never looks faded. */
173.rv-btn--primary:disabled {
174 color: var(--rv-faint);
175 background: transparent;
176 border-color: var(--rv-line);
177 opacity: 1;
179 
180/* A real, on-brand focus ring on every bespoke interactive element (the .rv-input
181 handles its own). One accent; never the suppressed default. */
182button:focus-visible,
183a:focus-visible,
184summary:focus-visible,
185[role="button"]:focus-visible,
186[tabindex]:focus-visible {
187 outline: 2px solid var(--rv-accent);
188 outline-offset: 2px;
189 border-radius: 3px;
191 
192/* Invisible 44px hit area for the editorial-tiny icon controls (padding, not size). */
193.rv-tap {
194 display: inline-flex;
195 align-items: center;
196 justify-content: center;
197 min-width: 40px;
198 min-height: 40px;
200 
201/* Ruled "fill-in" field — a single rule like a blank on a ledger form / letter.
202 Used for the short text inputs; the multi-line composer keeps a boxed surface. */
203.rv-field {
204 width: 100%;
205 background: transparent;
206 color: var(--rv-fg);
207 border: 0;
208 border-bottom: 2px solid var(--rv-hair);
209 border-radius: 0;
210 padding: 6px 2px;
211 font: inherit;
212 transition: border-color var(--rv-dur-1) var(--rv-ease), background-color var(--rv-dur-1) var(--rv-ease);
214.rv-field::placeholder { color: var(--rv-faint); }
215.rv-field:focus,
216.rv-field:focus-visible {
217 outline: none;
218 border-bottom-color: var(--rv-accent);
219 background: var(--rv-tint);
221 
222/* Mobile Safari zooms the viewport when a focused text control's font-size is
223 under 16px (both .rv-field and .rv-input inherit the body's 15px). Pin them to
224 16px on touch devices so tapping a field no longer zooms the page in — without
225 touching the viewport meta (disabling pinch-zoom would be an a11y regression).
226 Desktop keeps the smaller editorial size. */
227@media (pointer: coarse) {
228 .rv-field,
229 .rv-input {
230 font-size: 16px;
231 }
⋯ 136 lines hidden (lines 233–368)
233 
234/* A mounted card-stock surface — a filing card with an engraved inner rule. */
235.rv-card {
236 background: var(--rv-tint);
237 border: 1px solid var(--rv-line);
238 border-radius: 3px;
239 box-shadow: inset 0 0 0 3px var(--rv-tint), inset 0 0 0 4px var(--rv-line);
241 
242/* An illuminated initial for the Chronicle prose — the one ornament a hand-written
243 history book earns. Applies only to the first paragraph of a telling. */
244[data-testid="chronicle-body"] p:first-of-type::first-letter,
245[data-testid="end-chronicle-body"] p:first-of-type::first-letter {
246 font-family: var(--rv-serif);
247 font-size: 2.8em;
248 line-height: 0.78;
249 float: left;
250 margin: 0.04em 0.09em 0 0;
251 color: var(--rv-accent);
252 font-weight: 600;
254 
255/* The lifetime slider as a timeline playhead: a thin ruled track + a terracotta
256 marker bar (not a default round handle). */
257input[type="range"] {
258 -webkit-appearance: none;
259 appearance: none;
260 width: 100%;
261 height: 18px;
262 background: transparent;
263 cursor: pointer;
265input[type="range"]::-webkit-slider-runnable-track { height: 2px; background: var(--rv-line); }
266input[type="range"]::-moz-range-track { height: 2px; background: var(--rv-line); }
267input[type="range"]::-webkit-slider-thumb {
268 -webkit-appearance: none; appearance: none;
269 width: 5px; height: 16px; margin-top: -7px;
270 background: var(--rv-accent); border: 0; border-radius: 1.5px;
272input[type="range"]::-moz-range-thumb {
273 width: 5px; height: 16px; background: var(--rv-accent); border: 0; border-radius: 1.5px;
275input[type="range"]:focus-visible { outline: 2px solid var(--rv-accent); outline-offset: 4px; }
276 
277/* Thin tracks: the only "bars" (progress, char ammo). Hairline, never a pill. */
278.rv-track { height: 6px; background: var(--rv-line); border-radius: 999px; overflow: hidden; }
279.rv-track > .rv-fill { height: 100%; background: var(--rv-fg); border-radius: 999px; transition: width 0.4s var(--rv-ease); }
280 
281/* Collapsible rails (progressive disclosure) — ▸/▾ markers, hairline-topped. */
282.rv-rail { border-top: 1px solid var(--rv-line); padding-top: 10px; }
283.rv-summary { cursor: pointer; list-style: none; display: flex; align-items: center; gap: 8px; }
284.rv-summary::-webkit-details-marker { display: none; }
285.rv-summary::before { content: "\25B8"; color: var(--rv-faint); font-size: 11px; line-height: 1; }
286details[open] > .rv-summary::before { content: "\25BE"; }
287/* A forced-open rail (desktop Chronicle): the summary is a static label, not a toggle. */
288.rv-summary--static { cursor: default; pointer-events: none; }
289.rv-summary--static::before { display: none; }
290 
291/* ── Motion & state primitives ─────────────────────────────────────────── */
292 
293/* Tactile press — non-button interactive surfaces (chips, nodes, tabs) give a
294 little under the finger, the same as .rv-btn. Pair with the press target. */
295.rv-press { transition: transform var(--rv-dur-1) var(--rv-ease), background var(--rv-dur-1) var(--rv-ease), border-color var(--rv-dur-1) var(--rv-ease), color var(--rv-dur-1) var(--rv-ease); }
296.rv-press:active { transform: scale(0.97); }
297 
298/* The one spinner — a struck ring, for every "consulting the record…" wait.
299 Sizes to its font (em), inherits ink, so it drops into any text run. */
300.rv-spinner {
301 display: inline-block;
302 width: 0.9em; height: 0.9em;
303 border: 2px solid currentColor;
304 border-bottom-color: transparent;
305 border-radius: 50%;
306 animation: rv-spin 0.7s linear infinite;
307 vertical-align: -0.12em;
309@keyframes rv-spin { to { transform: rotate(360deg); } }
310 
311/* Pending shimmer — a slow ink wash over a block the system is still composing
312 (skeleton lines while the chronicler writes). */
313.rv-shimmer {
314 background: linear-gradient(90deg, var(--rv-line) 20%, var(--rv-hair) 40%, var(--rv-line) 60%);
315 background-size: 200% 100%;
316 animation: rv-shimmer 1.5s ease-in-out infinite;
317 border-radius: 2px;
319@keyframes rv-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
320 
321/* Waiting mode — a thin indeterminate sweep under the masthead while a turn is
322 being resolved: the whole board reads as "history is being rewritten." */
323.rv-loading-bar { position: relative; height: 2px; overflow: hidden; background: var(--rv-line); }
324.rv-loading-bar::after {
325 content: "";
326 position: absolute; inset: 0;
327 background: var(--rv-accent);
328 transform-origin: left;
329 animation: rv-load-sweep 1.15s var(--rv-ease) infinite;
331@keyframes rv-load-sweep {
332 0% { transform: translateX(-100%) scaleX(0.35); }
333 55% { transform: translateX(40%) scaleX(0.6); }
334 100% { transform: translateX(160%) scaleX(0.35); }
336 
337/* A quiet inline hint — teaches without nagging; a faint aside led by a small
338 accent pointer. */
339.rv-hint { font-size: 11px; line-height: 1.4; color: var(--rv-faint); }
340.rv-hint::before { content: "\203A\00a0\00a0"; color: var(--rv-accent); font-weight: 700; }
341 
342/* Entrances. panel-in is transform-only so it never fights .rv-dim's opacity, and
343 it replays on display:none→block (the phone tab switch). fade-in is the soft
344 reveal for whole views (board, end screen). */
345.rv-panel { animation: rv-panel-in var(--rv-dur-2) var(--rv-ease); }
346@keyframes rv-panel-in { from { transform: translateY(8px); } to { transform: translateY(0); } }
347.rv-fade-in { animation: rv-fade-in var(--rv-dur-3) var(--rv-ease) both; }
348@keyframes rv-fade-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
349 
350/* Value-change pop — the headline % gives a quick struck-token bump when it moves
351 (the text stays in lockstep with the store; this is purely the flourish). */
352.pct-pop { animation: rv-pct-pop var(--rv-dur-2) var(--rv-ease-spring); }
353@keyframes rv-pct-pop { 0% { transform: scale(1); } 35% { transform: scale(1.14); } 100% { transform: scale(1); } }
354 
355/* One place that honors reduced-motion for every shared primitive above. */
356@media (prefers-reduced-motion: reduce) {
357 .rv-spinner { animation-duration: 1.4s; }
358 .rv-shimmer,
359 .rv-loading-bar::after,
360 .rv-panel,
361 .rv-fade-in,
362 .pct-pop { animation: none; }
363 .rv-fade-in { opacity: 1; transform: none; }
364 .rv-press:active,
365 .rv-btn:active:not(:disabled) { transform: none; }
366 /* The bar fills jump rather than slide (the width tween is motion too). */
367 .rv-track > .rv-fill { transition: none; }