remarque-tokens 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENT_RULES.md CHANGED
@@ -141,6 +141,8 @@ project/
141
141
  ├── tokens-core.css # CORE tier: type scale, spacing, widths, radius, motion, prose machinery. NEVER override
142
142
  ├── tokens-palette.css # PALETTE tier: font slots, colors, accent, reading measure. Override freely, then run the audit
143
143
  ├── prose.css # .remarque-prose long-form styling (aggregated by tokens.css; own subpath for opt-out)
144
+ ├── essay.css # Optional Essay module: sidenotes + sticky TOC rail (own subpath, NOT aggregated — import explicitly)
145
+ ├── print.css # Print stylesheet (own subpath, NOT aggregated — import explicitly)
144
146
  ├── theme.css # Tailwind v4 adapter (@theme inline) — import after tailwindcss + tokens
145
147
  ├── tailwind.config.js # Tailwind v3 ONLY — v4 projects use theme.css instead
146
148
  ├── public/
@@ -200,6 +202,7 @@ Before considering any implementation complete, verify:
200
202
  - [ ] No scroll-triggered or entrance animations exist
201
203
  - [ ] Every image has a 1px border and mono caption
202
204
  - [ ] Every page maps to an archetype (Essay, Dossier, Notebook, Landing, Reference/Docs, Changelog, Gallery)
205
+ - [ ] If the Essay uses sidenotes/a TOC rail (`remarque-tokens/essay`): `.remarque-sidenote-ref`/`.remarque-sidenote` alternate in strict DOM order, the rail never intrudes into `.remarque-prose`'s own measure, and the page renders correctly with `essay.css`'s `@media` block deleted (the narrow-viewport/no-JS fallback)
203
206
  - [ ] Mobile version is roomy — not a compressed desktop layout
204
207
  - [ ] Mobile nav links have ≥44px touch targets
205
208
  - [ ] No pure white or pure black backgrounds
package/CHANGELOG.md CHANGED
@@ -4,7 +4,58 @@ All notable changes to `remarque-tokens` are documented here. Token value
4
4
  changes always state the design rationale — downstream sites pin against
5
5
  these entries when syncing.
6
6
 
7
- ## 0.14.0 — 2026-07-23
7
+ ## 0.15.0 — 2026-07-23
8
+
9
+ Essay module — sidenotes + sticky TOC rail, graduated from the flagship (closes #52).
10
+
11
+ ### Added
12
+ - **`essay.css`** (new subpath: `remarque-tokens/essay`, `remarque-tokens/essay.css`) —
13
+ optional Essay-archetype module, not aggregated into `tokens.css` or `prose.css`. Adds:
14
+ - `.remarque-sidenote-ref` / `.remarque-sidenote` — footnotes as in-flow margin notes.
15
+ Small-print inline block below `80rem` (`--color-border-bold`, the note's only visual
16
+ separator at that width); floats into the left gutter above it via a Tufte-style
17
+ negative-margin technique (`clear: left` stacks consecutive notes, no JS). Numbered
18
+ with CSS counters (`counter-reset` on `.remarque-prose`) rather than an authored number
19
+ — this package ships no build step, unlike the flagship's rehype transform. A repeat
20
+ citation of an already-noted footnote uses `.remarque-sidenote-ref--repeat` (styled
21
+ identically, doesn't advance the shared counter).
22
+ - `.remarque-footnotes` — a same-voice fallback for consumers without a DOM-reordering
23
+ build step: styles GFM's default end-of-document footnotes section instead of relocating
24
+ notes inline.
25
+ - `.remarque-toc-rail` — `position: sticky` right rail at `>= 80rem`, plain inline
26
+ collapsible `<details>` below it, same markup at every width (mono meta voice, small
27
+ caps rather than `text-transform: uppercase` — the flagship predates that rule).
28
+ - `.remarque-essay` — the opt-in 3-column grid shell (gutter / reading column / TOC
29
+ rail) that places the above two. Center track is `minmax(0, var(--content-reading))`,
30
+ identical to plain `.content-reading` centering; the grid never sets `max-width` or
31
+ `margin-inline` on its children, so the reading-column measure is unaffected with or
32
+ without this module.
33
+ - Single breakpoint, `>= 80rem` (1280px) — re-derived from `--content-standard` /
34
+ `--content-reading` / the core spacing scale rather than asserting the flagship's
35
+ literal rem measurements. Deleting the `@media` block leaves the correct
36
+ narrow-viewport/no-JS presentation, not a degraded one.
37
+ - `grid-row: 1 / span 999` on the rail, not `1 / -1` — ships with the flagship's
38
+ same-day grid-inflation fix already applied (`-1` resolves to row 1 with no explicit
39
+ `grid-template-rows`, inflating it to the rail's own height).
40
+ - **`print.css`** — forces `.remarque-sidenote` back to inline mode and `.remarque-essay`
41
+ back to block flow under `@media print`, making good on this file's existing "when a
42
+ sidenotes module lands" comment. (`.remarque-toc-rail` was already caught by the
43
+ existing `nav` / `[class*="toc-"]` print-hiding rules — no change needed there.)
44
+ - **REMARQUE.md "Essay Module"** — markup contract, when-to-use guidance, the
45
+ breakpoint-behavior table, and the grid-inflation provenance note.
46
+ - **AGENT_RULES.md** — File Structure Convention lists `essay.css` (and, newly, `print.css`,
47
+ which the convention had never mentioned); Quality Checklist gains an Essay-module line.
48
+ - **Demo site (`site/`)** — `writing/typography-as-interface` (the Essay archetype's VR
49
+ reference page) now exercises both sidenotes and the TOC rail. VR baselines updated:
50
+ `essay-{light,dark}-{desktop,mobile}.png` (4 files). New computed-style assertion:
51
+ the TOC rail's bounding box never overlaps `.remarque-prose`'s reading column at the
52
+ `1280px` desktop viewport.
53
+
54
+ ### Documentation
55
+ - **README.md "Graduation"** — adds this release to the "Historical examples" list as
56
+ the worked example of the "optional module" destination.
57
+
58
+
8
59
 
9
60
  Triage bundle — prose/`.content-reading` pairing documented, `--motion-easing` bridged into the Tailwind v4 utility layer, USWDS/WCAG source citations added to the accessibility section (closes #23, closes #31, closes #32).
10
61
 
package/README.md CHANGED
@@ -139,7 +139,9 @@ exemplar of the third: sanctioned personality that would be diluted by generaliz
139
139
 
140
140
  **Process (single-maintainer scale, no ceremony):** open an issue with before/after
141
141
  screenshots and the token-purity check; record the decision in the changelog. Historical
142
- examples: measure compensation and the Gallery archetype graduated; the theme deck was
142
+ examples: measure compensation and the Gallery archetype graduated; the essay
143
+ sidenotes/sticky-TOC-rail pair graduated as an optional module
144
+ ([#52](https://github.com/williamzujkowski/remarque/issues/52)); the theme deck was
143
145
  re-scoped on the way up ([#56](https://github.com/williamzujkowski/remarque/issues/56));
144
146
  the zine layer deliberately stayed local.
145
147
 
package/REMARQUE.md CHANGED
@@ -496,7 +496,8 @@ Long-form writing. The core content type.
496
496
  - Low-noise footer navigation (previous/next)
497
497
 
498
498
  **Optionally includes:**
499
- - Side table of contents on desktop (outside reading column)
499
+ - Side table of contents on desktop (outside reading column) — see "Essay Module" below for the sanctioned sticky-rail implementation
500
+ - Sidenotes/footnotes as in-flow margin notes — same module, same rail-vs-gutter layout
500
501
 
501
502
  **Never includes:**
502
503
  - Sidebar content within the reading column
@@ -626,6 +627,153 @@ Cover-grid and catalog pages — a page whose primary content unit is a grid of
626
627
 
627
628
  ---
628
629
 
630
+ ## Essay Module
631
+
632
+ An optional pair of Essay-archetype features — margin notes and a sticky
633
+ table of contents — shipped as their own subpath, `remarque-tokens/essay`
634
+ (`essay.css`), rather than aggregated into `tokens.css` or `prose.css`.
635
+ Not every Essay wants either; both are opt-in.
636
+
637
+ **Provenance.** Graduated from williamzujkowski.github.io's
638
+ `rehype-sidenotes.mjs` + `global.css` sidenote/sticky-TOC implementation,
639
+ ratified for upstreaming in [issue #52](https://github.com/williamzujkowski/remarque/issues/52)
640
+ per the "Graduation" checklist in the package README — a footnote
641
+ vocabulary Remarque otherwise entirely lacked (this spec, until now,
642
+ permitted a side TOC at line ~499 without specifying anything about it).
643
+ No site-specific value was copied; every dimension below is re-derived
644
+ from tokens this package already ships (see `essay.css`'s header comment
645
+ for the value-by-value mapping).
646
+
647
+ ### Markup contract
648
+
649
+ ```html
650
+ <article class="remarque-essay">
651
+ <header class="content-reading">
652
+ <h1 class="text-display font-display">Essay Title</h1>
653
+ </header>
654
+
655
+ <nav class="remarque-toc-rail" aria-label="Table of contents">
656
+ <details open>
657
+ <summary>On this page</summary>
658
+ <ul>
659
+ <li><a href="#section-one">Section One</a></li>
660
+ <li><a href="#section-two">Section Two</a></li>
661
+ </ul>
662
+ </details>
663
+ </nav>
664
+
665
+ <div class="remarque-prose content-reading">
666
+ <h2 id="section-one">Section One</h2>
667
+ <p>
668
+ A claim worth a citation<a href="#note-1" id="note-1-ref"
669
+ class="remarque-sidenote-ref" aria-describedby="note-1"></a>.
670
+ </p>
671
+ <aside class="remarque-sidenote" id="note-1" role="note">
672
+ The note text, in real DOM order right after the paragraph that
673
+ cites it — not collected into an end-of-document section.
674
+ </aside>
675
+ </div>
676
+
677
+ <footer class="content-reading">...</footer>
678
+ </article>
679
+ ```
680
+
681
+ Requirements the CSS depends on:
682
+
683
+ - `.remarque-sidenote-ref` and its matching `.remarque-sidenote` MUST
684
+ alternate in strict DOM order (ref, then note, then the next ref, then
685
+ its note...) — the module numbers them with CSS counters
686
+ (`counter-reset` on `.remarque-prose`), not an authored number, so an
687
+ out-of-order pair mislabels every note after it.
688
+ - A footnote cited more than once: only the FIRST reference gets a
689
+ `.remarque-sidenote` after it; later citations of the same note use
690
+ `.remarque-sidenote-ref--repeat` (styled identically, does not advance
691
+ the counter) so the shared numbering doesn't skip.
692
+ - `role="note"` on every `.remarque-sidenote` — `<aside>`'s implicit role
693
+ is `complementary`, a landmark; an essay with several notes would
694
+ otherwise scatter that many unlabeled landmark regions through the
695
+ page. `aria-describedby` on the reference (pointing at the note's `id`)
696
+ is optional but recommended.
697
+ - `.remarque-toc-rail` wraps a plain `<details open>` — the SAME markup
698
+ at every viewport width, no JS breakpoint listener. `open` by default
699
+ is deliberate (see "Breakpoint behavior" below).
700
+
701
+ **No build step required, and none shipped.** This module doesn't ship or
702
+ depend on a rehype/remark transform — the contract above is satisfied by
703
+ hand-authored HTML exactly as well as by a build-time relocation step. If
704
+ your pipeline doesn't relocate GFM footnote definitions into DOM order
705
+ (most don't, out of the box), style the default end-of-document
706
+ `<section data-footnotes>` output with `.remarque-footnotes` instead —
707
+ same mono/meta voice, without the DOM-reordering accessibility upgrade:
708
+
709
+ ```html
710
+ <section class="remarque-footnotes">
711
+ <h2>Notes</h2>
712
+ <ol>
713
+ <li id="note-1">The note text. <a href="#note-1-ref">&#8617;</a></li>
714
+ </ol>
715
+ </section>
716
+ ```
717
+
718
+ Use `.remarque-sidenote` whenever you can relocate notes into reading
719
+ order — that relocation is the actual accessibility win this module
720
+ exists for. `.remarque-footnotes` is a same-voice fallback, not a lesser
721
+ tier of support, for the common case where you can't (yet).
722
+
723
+ ### When to use
724
+
725
+ Essay archetype only (sidenotes/a TOC rail are meaningless outside
726
+ long-form prose). Reach for sidenotes on essays with real citations,
727
+ asides, or sourcing that would otherwise interrupt the sentence; reach
728
+ for the TOC rail on any essay long enough that "where am I" becomes a
729
+ real question — roughly the same length threshold Reference/Docs pages
730
+ already assume. Both are independent opt-ins: an essay can use one, the
731
+ other, both, or neither without violating the archetype.
732
+
733
+ ### Breakpoint behavior
734
+
735
+ Gated on a single `>= 80rem` (1280px) `@media` query — no other
736
+ breakpoint, no JS:
737
+
738
+ | Viewport | Sidenotes | TOC |
739
+ |---|---|---|
740
+ | `< 80rem` (narrow) | Small-print block, in-flow, right after the citing paragraph — bordered with `--color-border-bold` (a 3:1 functional border, since it's the note's only visual separator at this width) | Inline collapsible `<details>` in the reading column, native disclosure triangle |
741
+ | `>= 80rem` (wide) | Floats into the LEFT margin gutter via a Tufte-style negative-margin technique, `clear: left` stacks consecutive notes, border relaxes to decorative `--color-border` (whitespace is now the real separator) | `position: sticky` in the RIGHT rail, `.remarque-essay` becomes a 3-column grid (gutter / reading column / rail) |
742
+
743
+ The rail and margin notes always live OUTSIDE `--content-reading` — that
744
+ is the entire point of the module. The center grid track is
745
+ `minmax(0, var(--content-reading))`, identical to the plain
746
+ `max-width: content-reading; margin-inline: auto` centering used
747
+ everywhere else in Remarque; a TOC rail or a floated sidenote can widen
748
+ the page shell, never the reading column itself. `.remarque-essay`'s grid
749
+ placement rule only ever sets `grid-column` on its children — it never
750
+ touches `max-width`/`margin-inline`, so the reading-column assertion
751
+ this spec's Enforcement Checklist already covers holds unmodified with
752
+ or without this module.
753
+
754
+ Delete `essay.css`'s single `@media (min-width: 80rem)` block and the
755
+ page still renders correctly — that IS the narrow-viewport presentation,
756
+ not a degraded one. No JavaScript is required at either width. (The
757
+ flagship implementation runs a small script to force `<details>` open on
758
+ every breakpoint crossing; this module holds to "no JS required" and
759
+ instead specifies `open` by default in the markup — a reader who
760
+ manually collapses the TOC below the breakpoint and then widens their
761
+ window keeps it collapsed until clicked again, a fully static cost this
762
+ module accepts in exchange for not requiring a script tag.)
763
+
764
+ ### Provenance note: the grid-inflation lesson
765
+
766
+ `.remarque-essay`'s TOC rail spans `grid-row: 1 / span 999`, not
767
+ `1 / -1`. With no explicit `grid-template-rows` (as here), `-1` resolves
768
+ to "the last explicit row line" — which, with zero explicit rows, is
769
+ row 1. The flagship implementation shipped exactly that bug: the rail's
770
+ own height inflated row 1, pushing the whole essay down and leaving a
771
+ visible gap between the header and the first paragraph on wide
772
+ viewports, fixed same-day. This module ships with the fix already
773
+ applied so the next consumer doesn't rediscover it.
774
+
775
+ ---
776
+
629
777
  ## Signature Moves
630
778
 
631
779
  These are the repeatable visual tells that make a Remarque site recognizable:
package/essay.css ADDED
@@ -0,0 +1,429 @@
1
+ /*
2
+ * Remarque — Essay Module (sidenotes + sticky TOC rail)
3
+ * ───────────────────────────────────────────────────────
4
+ * Optional Essay-archetype module: footnotes become in-flow margin notes
5
+ * (Tufte/gwern-style), and an "on this page" table of contents pins to a
6
+ * sticky rail alongside the reading column. Both are progressive
7
+ * enhancements over a plain Essay page — CSS-only, no JavaScript required
8
+ * at either breakpoint.
9
+ *
10
+ * PROVENANCE — graduated, not invented here. This module re-expresses, in
11
+ * Remarque's own token vocabulary, a design first built and shipped on
12
+ * williamzujkowski.github.io (`astro-site/src/lib/rehype-sidenotes.mjs` +
13
+ * `global.css`'s "Sidenotes"/"Sticky TOC side rail" blocks). Ratified for
14
+ * upstreaming in issue #52. No site-specific value (the flagship's literal
15
+ * `11.5rem`/`13rem`/`14rem`/`1280px` measurements) was copied — every
16
+ * dimension below is re-derived from tokens already in tokens-core.css /
17
+ * tokens-palette.css. See REMARQUE.md's "Essay Module" section for the
18
+ * full markup contract, and this file's inline comments for exactly which
19
+ * site value maps to which token and why a few mechanics changed shape
20
+ * (CSS counters instead of a build-time numbering step; `<details open>`
21
+ * by default instead of a JS breakpoint listener) on the way upstream.
22
+ *
23
+ * Own subpath (remarque-tokens/essay, remarque-tokens/essay.css) — NOT
24
+ * included by the tokens.css aggregator, and not required by prose.css:
25
+ * plenty of Essay pages want neither sidenotes nor a TOC rail. Import
26
+ * explicitly, after prose.css:
27
+ * @import 'remarque-tokens/prose.css';
28
+ * @import 'remarque-tokens/essay.css';
29
+ * (string form — see AGENT_RULES.md Pitfall #6.)
30
+ *
31
+ * ─── MARKUP CONTRACT (summary — full version in REMARQUE.md) ──────────
32
+ *
33
+ * <article class="remarque-essay">
34
+ * <header class="content-reading">...</header>
35
+ *
36
+ * <nav class="remarque-toc-rail" aria-label="Table of contents">
37
+ * <details open>
38
+ * <summary>On this page</summary>
39
+ * <ul>
40
+ * <li><a href="#section-one">Section One</a></li>
41
+ * </ul>
42
+ * </details>
43
+ * </nav>
44
+ *
45
+ * <div class="remarque-prose content-reading">
46
+ * <p>
47
+ * A claim worth a citation<a href="#note-1" id="note-1-ref"
48
+ * class="remarque-sidenote-ref" aria-describedby="note-1"></a>.
49
+ * </p>
50
+ * <aside class="remarque-sidenote" id="note-1" role="note">
51
+ * The note text, in real DOM order right after the paragraph that
52
+ * cites it — not collected into an end-of-document section.
53
+ * </aside>
54
+ * </div>
55
+ *
56
+ * <footer class="content-reading">...</footer>
57
+ * </article>
58
+ *
59
+ * DOM order is the accessibility requirement, not a presentational nicety:
60
+ * a screen reader hits a sidenote's content exactly where its reference
61
+ * occurs. `.remarque-sidenote-ref` and its matching `.remarque-sidenote`
62
+ * MUST alternate in strict DOM order (ref, then note, then the next ref,
63
+ * then its note...) — the counter mechanics below depend on it. `role=
64
+ * "note"` keeps a page with several sidenotes from scattering that many
65
+ * unlabeled `complementary` landmarks through the document (`<aside>`'s
66
+ * implicit role) — the same fix the flagship's rehype plugin documents.
67
+ *
68
+ * This module is deliberately silent on HOW that markup gets produced. A
69
+ * build-time transform (the flagship's rehype-sidenotes.mjs is one worked
70
+ * example, not a dependency of this package) or hand-authored HTML both
71
+ * satisfy the contract equally — see REMARQUE.md for the no-build-step
72
+ * fallback (`.remarque-footnotes`) if you don't have one.
73
+ *
74
+ * ─── BREAKPOINT ─────────────────────────────────────────────────────────
75
+ * Gated on >= 80rem (1280px, at the platform default 16px root) viewport
76
+ * width — the same threshold the flagship ratified, re-derived here from
77
+ * this package's own tokens instead of asserted as a bare magic number:
78
+ * `--content-standard` (72rem) is the widest a Remarque page's content
79
+ * ever grows, and the rail/gutter tracks below need `--space-11`+
80
+ * `--space-12` (10-12rem) and `--space-10` (8rem) of clearance on top of
81
+ * `--content-reading` (46rem default) plus two `--space-6` gaps — call it
82
+ * roughly 70-76rem of content width before both tracks have their
83
+ * documented minimum room, plus headroom for a scrollbar and window
84
+ * chrome. 80rem clears that with margin to spare across every approved
85
+ * body-face/measure pairing (see REMARQUE.md's Measure Compensation
86
+ * table) without needing a `--breakpoint-*` token this package doesn't
87
+ * otherwise have. Below it: sidenotes render as small-print inline blocks
88
+ * exactly where the plain document flow already places them, and the TOC
89
+ * is a collapsible `<details>` in the reading column — this is also the
90
+ * ONLY rendering a consumer who skips this module's `@media` block ever
91
+ * sees, so nothing here is required for the page to work.
92
+ */
93
+
94
+ /* ─── Sidenotes — default / narrow-viewport mode ────────────────────────
95
+ * `.remarque-sidenote` renders as a bordered small-print block right in
96
+ * the text flow by default. This IS the narrow-viewport fallback the
97
+ * spec calls for — zero extra markup, zero JS: the note just isn't
98
+ * floated into a margin yet.
99
+ *
100
+ * Numbering is generated, not authored: `counter-reset` on
101
+ * `.remarque-prose`, `counter-increment` on `.remarque-sidenote-ref` only,
102
+ * and `.remarque-sidenote` simply READING that same counter (no increment
103
+ * of its own — see the comment on that rule) keep the visible number in
104
+ * sync across the reference and its note with no number typed into
105
+ * either element — the flagship's rehype plugin injects a literal
106
+ * `<span class="sidenote-number">1</span>`/text node at build time; this
107
+ * package ships no build step, so CSS counters do the same job for a
108
+ * pure-CSS module. A repeat citation of an already-noted footnote should
109
+ * use `.remarque-sidenote-ref--repeat` (no `.remarque-sidenote` follows
110
+ * it) so the shared counter isn't advanced for a note that was already
111
+ * printed — mirrors the flagship's "only the first reference gets a
112
+ * sidenote" rule.
113
+ */
114
+ .remarque-prose {
115
+ counter-reset: remarque-sidenote;
116
+ }
117
+
118
+ .remarque-sidenote-ref {
119
+ counter-increment: remarque-sidenote;
120
+ font-family: var(--font-mono);
121
+ font-size: var(--text-meta);
122
+ /* USWDS floor rationale: --text-micro is reserved for timestamps/fine
123
+ print only (tokens-core.css, REMARQUE.md "Font Size Floors") — the
124
+ flagship's own marker used that size, but this module holds to
125
+ Remarque's stricter 14px floor for anything read as content. */
126
+ line-height: 0;
127
+ vertical-align: super;
128
+ color: var(--color-accent);
129
+ text-decoration: none;
130
+ }
131
+
132
+ .remarque-sidenote-ref::after {
133
+ content: counter(remarque-sidenote);
134
+ }
135
+
136
+ .remarque-sidenote-ref:hover {
137
+ color: var(--color-accent-hover);
138
+ text-decoration: underline;
139
+ }
140
+
141
+ .remarque-sidenote-ref--repeat {
142
+ counter-increment: none;
143
+ }
144
+
145
+ .remarque-sidenote {
146
+ /* No counter-increment here: `.remarque-sidenote-ref` already advanced
147
+ `remarque-sidenote` immediately before this element in DOM order —
148
+ incrementing again would double-count every pair (ref shows N, the
149
+ note that follows it would show N+1 instead of N). This element only
150
+ READS the counter its ref just set. */
151
+ display: block;
152
+ margin-block: var(--space-3);
153
+ padding-inline-start: var(--space-3);
154
+ /* border-bold: this is the note's ONLY visual separator from running
155
+ prose at narrow widths, so it must clear the 3:1 functional-border
156
+ minimum (WCAG 1.4.11) — same reasoning as .remarque-prose blockquote. */
157
+ border-inline-start: var(--border-width) var(--border-style) var(--color-border-bold);
158
+ font-family: var(--font-mono);
159
+ font-size: var(--text-meta);
160
+ line-height: var(--leading-meta);
161
+ color: var(--color-fg-muted);
162
+ }
163
+
164
+ /* The generated number precedes whatever content the note holds. If that
165
+ content is a single run of inline text (no wrapping <p>), the number
166
+ sits inline with the opening words — the flagship's "1 Note text..."
167
+ pairing. A `<p>`-wrapped note (multi-paragraph notes need it) instead
168
+ reads as a small label above the paragraph — a pure-CSS module can't
169
+ reach inside an arbitrary child the way a build-time transform can, so
170
+ this is the honest degradation: prefer bare text for single-line notes. */
171
+ .remarque-sidenote::before {
172
+ content: counter(remarque-sidenote) "\2002";
173
+ color: var(--color-accent);
174
+ font-weight: var(--weight-semibold);
175
+ }
176
+
177
+ .remarque-sidenote :where(p) {
178
+ margin: 0;
179
+ }
180
+
181
+ .remarque-sidenote :where(p) + :where(p) {
182
+ margin-top: var(--space-2);
183
+ }
184
+
185
+ /* ─── Sidenotes — wide-viewport mode: float into the left margin ────────
186
+ * Classic Tufte CSS technique: `.remarque-sidenote` isn't a grid child
187
+ * itself (it's nested inside `.remarque-prose`, which IS grid-column 2 —
188
+ * see `.remarque-essay` below) so grid placement doesn't apply to it
189
+ * directly. Instead it floats left with a negative inline-start margin
190
+ * exactly wide enough to clear its own box plus one column-gap unit,
191
+ * landing in the grid's column-1 gutter. `clear: left` stacks consecutive
192
+ * notes without any JS-computed offset.
193
+ *
194
+ * Width (`--space-10`, 8rem) and pull-out distance (that width plus
195
+ * `--space-6`, the same gap the grid below uses between tracks) are both
196
+ * drawn from the core spacing scale rather than asserting a bespoke
197
+ * sidebar-width value — this is the token-vocabulary re-expression of the
198
+ * flagship's hand-measured `11.5rem`/`13rem` pair.
199
+ */
200
+ @media (min-width: 80rem) {
201
+ .remarque-sidenote {
202
+ float: left;
203
+ clear: left;
204
+ width: var(--space-10);
205
+ margin-inline-start: calc(-1 * (var(--space-10) + var(--space-6)));
206
+ margin-block: var(--space-1) var(--space-4);
207
+ padding-inline-start: 0;
208
+ padding-inline-end: var(--space-2);
209
+ border-inline-start: none;
210
+ /* Decorative-only at this width (the gutter's own whitespace is the
211
+ real separator now) — `--color-border`, not `-bold`, matching the
212
+ flagship's own choice to relax the border once it's no longer the
213
+ sole boundary cue. */
214
+ border-inline-end: var(--border-width) var(--border-style) var(--color-border);
215
+ text-align: start;
216
+ }
217
+ }
218
+
219
+ /* ─── Footnotes fallback (no relocation build step) ─────────────────────
220
+ * `.remarque-footnotes` styles a traditional end-of-document footnotes
221
+ * section — e.g. GFM/mdast-util-to-hast's default
222
+ * `<section data-footnotes><h2>Footnotes</h2><ol>...</ol></section>`
223
+ * output — in the same meta voice as `.remarque-sidenote`, for a consumer
224
+ * who hasn't built (or doesn't want) a DOM-reordering transform. It is a
225
+ * same-voice fallback, not a lesser feature: use `.remarque-sidenote`
226
+ * whenever you can relocate notes into DOM reading order (the real a11y
227
+ * upgrade this module exists for); reach for `.remarque-footnotes` only
228
+ * when that isn't available yet.
229
+ *
230
+ * <section class="remarque-footnotes">
231
+ * <h2>Notes</h2>
232
+ * <ol>
233
+ * <li id="note-1">The note text. <a href="#note-1-ref">&#8617;</a></li>
234
+ * </ol>
235
+ * </section>
236
+ */
237
+ .remarque-footnotes {
238
+ margin-top: var(--space-7);
239
+ padding-top: var(--space-3);
240
+ border-top: var(--border-width) var(--border-style) var(--color-border);
241
+ font-family: var(--font-mono);
242
+ font-size: var(--text-meta);
243
+ line-height: var(--leading-meta);
244
+ color: var(--color-fg-muted);
245
+ }
246
+
247
+ .remarque-footnotes h2 {
248
+ font-family: var(--font-mono);
249
+ font-size: var(--text-meta);
250
+ letter-spacing: var(--tracking-caps);
251
+ font-variant-caps: all-small-caps;
252
+ color: var(--color-muted);
253
+ margin: 0 0 var(--space-3);
254
+ }
255
+
256
+ .remarque-footnotes ol {
257
+ padding-inline-start: var(--space-5);
258
+ margin: 0;
259
+ }
260
+
261
+ .remarque-footnotes li + li {
262
+ margin-top: var(--space-2);
263
+ }
264
+
265
+ .remarque-footnotes a {
266
+ color: var(--color-accent);
267
+ text-decoration: none;
268
+ }
269
+
270
+ .remarque-footnotes a:hover {
271
+ color: var(--color-accent-hover);
272
+ text-decoration: underline;
273
+ }
274
+
275
+ /* ─── Sticky TOC rail ────────────────────────────────────────────────────
276
+ * `.remarque-toc-rail` wraps a plain collapsible `<details>` — the SAME
277
+ * markup at every viewport, no JS breakpoint listener. Author it
278
+ * `<details open>` by default: below the rail breakpoint this is an
279
+ * inline collapsible block a reader can close to save space (native
280
+ * disclosure behavior, unchanged); at/above it, it reads as an
281
+ * already-expanded sticky rail. The flagship instead runs a small inline
282
+ * script that force-opens the native `open` attribute on every breakpoint
283
+ * crossing — this module holds to "no JS required," so it authors the
284
+ * open state up front instead of syncing it at runtime. The one
285
+ * degradation this trades away: a reader who manually collapses the TOC
286
+ * below the breakpoint and then widens their window keeps it collapsed
287
+ * until they click it again — a fully static, CSS-only cost worth paying
288
+ * for not requiring a script tag.
289
+ *
290
+ * Marker: reuses the same quiet mono disclosure triangle as
291
+ * `.remarque-prose details summary::before` (prose.css) rather than
292
+ * asking consumers to embed an inline SVG chevron, as the flagship's
293
+ * TableOfContents.astro does.
294
+ */
295
+ .remarque-toc-rail {
296
+ margin-block: var(--space-5);
297
+ }
298
+
299
+ .remarque-toc-rail details {
300
+ border: none;
301
+ padding: 0;
302
+ }
303
+
304
+ .remarque-toc-rail summary {
305
+ cursor: pointer;
306
+ list-style: none;
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: space-between;
310
+ gap: var(--space-2);
311
+ padding-block: var(--space-2);
312
+ border-bottom: var(--border-width) var(--border-style) var(--color-border);
313
+ /* Mono meta voice — same declarations as .text-label (tokens-core.css),
314
+ repeated rather than shared because .text-label isn't a mixin: true
315
+ small caps, never text-transform: uppercase (REMARQUE.md "Small
316
+ Caps"). The flagship predates that rule and used `text-transform:
317
+ uppercase`; this is the one place the re-expression corrects the
318
+ source rather than just relocating it. */
319
+ font-family: var(--font-mono);
320
+ font-size: var(--text-meta);
321
+ font-variant-caps: all-small-caps;
322
+ letter-spacing: var(--tracking-caps);
323
+ color: var(--color-muted);
324
+ transition: color var(--motion-fast) var(--motion-easing);
325
+ }
326
+
327
+ .remarque-toc-rail summary::-webkit-details-marker {
328
+ display: none;
329
+ }
330
+
331
+ .remarque-toc-rail summary::before {
332
+ content: "\25B8";
333
+ display: inline-block;
334
+ color: var(--color-muted);
335
+ transition: transform var(--motion-fast) var(--motion-easing);
336
+ }
337
+
338
+ .remarque-toc-rail details[open] summary::before {
339
+ transform: rotate(90deg);
340
+ }
341
+
342
+ .remarque-toc-rail summary:hover {
343
+ color: var(--color-fg);
344
+ }
345
+
346
+ .remarque-toc-rail ul {
347
+ list-style: none;
348
+ padding: 0;
349
+ margin: var(--space-3) 0 0;
350
+ }
351
+
352
+ .remarque-toc-rail li + li {
353
+ margin-top: var(--space-1);
354
+ }
355
+
356
+ .remarque-toc-rail a {
357
+ font-family: var(--font-mono);
358
+ font-size: var(--text-meta);
359
+ color: var(--color-fg-muted);
360
+ text-decoration: none;
361
+ transition: color var(--motion-fast) var(--motion-easing);
362
+ }
363
+
364
+ .remarque-toc-rail a:hover {
365
+ color: var(--color-fg);
366
+ }
367
+
368
+ /* ─── Essay grid shell — wide-viewport rail layout ───────────────────────
369
+ * `.remarque-essay` is the opt-in grid container an Essay page's <article>
370
+ * adds to place a sticky TOC rail beside the reading column, mirroring
371
+ * this module's sidenote gutter on the OTHER side (right rail = TOC, left
372
+ * gutter = sidenotes — never the same side, so the two mechanisms never
373
+ * fight over space). Below the breakpoint it is a no-op: every child
374
+ * keeps whatever block-flow centering it already had from `.content-
375
+ * reading` / `.remarque-prose` — this rule only exists inside the
376
+ * `@media` block.
377
+ *
378
+ * Track widths: the center track is `minmax(0, var(--content-reading))` —
379
+ * IDENTICAL to the plain `max-width: content-reading; margin-inline:
380
+ * auto` centering used everywhere else (same visual column, just a grid
381
+ * track instead of block flow), so nothing here changes the reading
382
+ * measure the "prose measure" computed-style check already covers. The
383
+ * right track (`minmax(--space-11, --space-12)`, 10-12rem) is this
384
+ * module's re-expression of the flagship's bespoke `minmax(12rem, 14rem)`
385
+ * rail — same shape (a comfortable range, not a fixed width), core
386
+ * spacing tokens instead of asserted rem literals.
387
+ *
388
+ * Children are placed by exclusion (`:not(.remarque-toc-rail)` -> column
389
+ * 2) rather than an explicit tag list, deliberately: nothing about this
390
+ * rule touches `max-width` or `margin-inline` on any child, so each
391
+ * element's own `.content-reading` / `.remarque-prose` centering keeps
392
+ * working exactly as it does outside a grid — the grid track is already
393
+ * sized to match it.
394
+ */
395
+ @media (min-width: 80rem) {
396
+ .remarque-essay {
397
+ display: grid;
398
+ grid-template-columns:
399
+ minmax(0, 1fr)
400
+ minmax(0, var(--content-reading))
401
+ minmax(var(--space-11), var(--space-12));
402
+ column-gap: var(--space-6);
403
+ }
404
+
405
+ .remarque-essay > :not(.remarque-toc-rail) {
406
+ grid-column: 2;
407
+ }
408
+
409
+ .remarque-essay > .remarque-toc-rail {
410
+ grid-column: 3;
411
+ /* `1 / -1` resolves to row 1 ONLY when the grid has no explicit
412
+ `grid-template-rows` (as here) — `-1` means "the last explicit
413
+ row line," which with zero explicit rows IS row 1. That inflates
414
+ row 1 to the rail's own height instead of spanning the real
415
+ content rows, shoving everything after it down a gap the height
416
+ of the TOC. (Exactly the bug the flagship shipped and fixed,
417
+ 2026-07-20 — captured here so this module doesn't reintroduce
418
+ it.) A generous explicit span covers any Essay's block count;
419
+ empty implicit rows stay 0-height. */
420
+ grid-row: 1 / span 999;
421
+ align-self: start;
422
+ justify-self: start;
423
+ position: sticky;
424
+ top: var(--space-6);
425
+ width: 100%;
426
+ max-height: calc(100vh - var(--space-8));
427
+ overflow-y: auto;
428
+ }
429
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remarque-tokens",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "engines": {
5
5
  "node": ">=18"
6
6
  },
@@ -68,6 +68,8 @@
68
68
  "./prose.css": "./prose.css",
69
69
  "./print": "./print.css",
70
70
  "./print.css": "./print.css",
71
+ "./essay": "./essay.css",
72
+ "./essay.css": "./essay.css",
71
73
  "./agent-rules": "./AGENT_RULES.md",
72
74
  "./spec": "./REMARQUE.md",
73
75
  "./package.json": "./package.json"
@@ -92,6 +94,7 @@
92
94
  "scripts/tokens-json.mjs",
93
95
  "prose.css",
94
96
  "print.css",
97
+ "essay.css",
95
98
  "scripts/lib/css-tokens.mjs"
96
99
  ]
97
100
  }
package/print.css CHANGED
@@ -18,10 +18,11 @@
18
18
  * markup — a site with different class names should add its own
19
19
  * @media print rule for anything this file doesn't catch.
20
20
  *
21
- * Sidenotes: this package does not yet ship a sidenotes module. When
22
- * one lands, its narrow-viewport "inline" mode is exactly the right
23
- * print behaviorprint should never attempt the wide-margin
24
- * side-rail layout a sidenotes module might use on desktop screens.
21
+ * Sidenotes/TOC rail (remarque-tokens/essay.css, issue #52): the "Force
22
+ * the narrow-viewport mode" rule below is this file making good on the
23
+ * promise abovethe essay module's own default (non-`@media`) CSS
24
+ * already IS the narrow-viewport inline mode, so this rule only needs to
25
+ * cancel the `>= 80rem` float, not author a print-specific layout.
25
26
  */
26
27
 
27
28
  @page {
@@ -84,17 +85,43 @@
84
85
  display: none !important;
85
86
  }
86
87
 
88
+ /* ─── Sidenotes: force the narrow-viewport (inline) mode ───
89
+ * essay.css's `>= 80rem` float/negative-margin rule has no viewport to
90
+ * key off of on paper, and some print engines DO evaluate desktop-width
91
+ * `@media (min-width)` queries against the page box. Cancel it
92
+ * explicitly rather than relying on that not happening — a floated
93
+ * margin note next to a 2cm-margin page is exactly the layout the
94
+ * comment at the top of this file promises never to attempt. */
95
+ .remarque-sidenote {
96
+ float: none !important;
97
+ width: auto !important;
98
+ margin-inline-start: 0 !important;
99
+ margin-block: var(--space-3) !important;
100
+ padding-inline-start: var(--space-3) !important;
101
+ padding-inline-end: 0 !important;
102
+ border-inline-start: var(--border-width) var(--border-style) var(--color-border-bold) !important;
103
+ border-inline-end: none !important;
104
+ }
105
+
87
106
  /* ─── Full-width reading column ────────────────────────────
88
107
  * The page itself is already narrow at 2cm margins — don't also
89
- * constrain content to the on-screen reading measure. */
108
+ * constrain content to the on-screen reading measure. Also collapses
109
+ * `.remarque-essay` back to plain block flow — same defensive reasoning
110
+ * as the sidenote float-cancel above, for the grid rather than the
111
+ * float half of essay.css's wide-viewport rule. */
90
112
  .content-reading,
91
113
  .content-standard,
92
114
  .content-wide,
115
+ .remarque-essay,
93
116
  main {
94
117
  max-width: 100% !important;
95
118
  margin-inline: 0 !important;
96
119
  }
97
120
 
121
+ .remarque-essay {
122
+ display: block !important;
123
+ }
124
+
98
125
  body {
99
126
  font-size: 11pt;
100
127
  line-height: 1.5;
package/tokens.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * Remarque design tokens — GENERATED from tokens.json by
3
- * scripts/tokens-json.mjs (v0.14.0). Do not edit — the CSS
3
+ * scripts/tokens-json.mjs (v0.15.0). Do not edit — the CSS
4
4
  * (tokens-core.css + tokens-palette.css) is the source of truth;
5
5
  * tokens.json is the intermediate machine-readable form this file is
6
6
  * generated from. Regenerate with: node scripts/tokens-json.mjs
package/tokens.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$description": "Remarque design tokens — GENERATED from tokens-core.css + tokens-palette.css by scripts/tokens-json.mjs. Do not edit; the CSS is the source of truth.",
3
3
  "$extensions": {
4
4
  "remarque": {
5
- "version": "0.14.0",
5
+ "version": "0.15.0",
6
6
  "tiers": {
7
7
  "core": "immutable identity — overriding forks the system",
8
8
  "palette": "sanctioned personalization surface — override freely, then run remarque-audit"