@bongos/core 1.19.449 → 1.19.451

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.
@@ -1,417 +1,109 @@
1
- /* primer.css — the primer reader page (V3.R13-followup #406; reworked for the
2
- app-shell redesign, task 1723). Long-form reading layout that pairs with
3
- /builders/style.css: it only adds the primer-specific reading shell and
4
- derives every color from the shared design tokens (--surface, --border,
5
- --accent, …) so instance theme packs reskin it for free. */
6
-
7
- body.primer-body { background: var(--bg); }
8
-
9
- /* ---------- Reading-progress bar ----------
10
- Fixed just below the sticky top bar (56px). Spans the viewport but sits
11
- BELOW the sidebar in the z ladder (sidebar z-40, topbar z-30) so on desktop
12
- the opaque sidebar covers its left edge and it reads as the content's
13
- progress, not app chrome. */
14
-
15
- .primer-progress {
16
- position: fixed;
17
- top: var(--topbar-h);
18
- inset-inline: 0;
19
- height: 3px;
20
- background: color-mix(in srgb, var(--accent) 14%, transparent);
21
- z-index: 25;
22
- pointer-events: none;
23
- }
24
- .primer-progress__bar {
25
- height: 100%;
26
- width: 0;
27
- background: var(--accent);
28
- transition: width 100ms linear;
29
- }
30
-
31
- /* ---------- Two-column reading layout ---------- */
32
-
33
- .primer-layout {
34
- display: grid;
35
- grid-template-columns: 240px minmax(0, 1fr);
36
- gap: 40px;
37
- align-items: start;
38
- padding-bottom: 24px;
39
- }
40
-
41
- /* ---------- Sticky table of contents (left rail) ---------- */
42
-
43
- .primer-toc {
44
- position: sticky;
45
- top: calc(var(--topbar-h) + 16px);
46
- align-self: start;
47
- }
48
- .primer-toc__toggle {
49
- display: none;
50
- width: 100%;
51
- padding: 9px 14px;
52
- background: var(--surface);
53
- border: 1px solid var(--border-strong);
54
- border-radius: var(--radius-sm);
55
- font-family: var(--font-body);
56
- font-size: 13.5px;
57
- font-weight: 600;
58
- color: var(--ink);
59
- cursor: pointer;
60
- justify-content: space-between;
61
- align-items: center;
62
- }
63
- .primer-toc__chevron {
64
- transition: transform 200ms;
65
- }
66
- .primer-toc__toggle[aria-expanded="true"] .primer-toc__chevron {
67
- transform: rotate(180deg);
68
- }
69
- .primer-toc__inner {
70
- background: var(--surface);
71
- border: 1px solid var(--border);
72
- border-radius: var(--radius);
73
- padding: 16px 16px 18px;
74
- }
75
- .primer-toc__heading {
76
- margin: 0 0 10px;
77
- font-size: 11.5px;
78
- font-weight: 600;
79
- letter-spacing: 0.06em;
80
- text-transform: uppercase;
81
- color: var(--ink-faint);
82
- border-bottom: 1px solid var(--border);
83
- padding-bottom: 8px;
84
- }
85
- .primer-toc__list {
86
- list-style: none;
87
- margin: 0;
88
- padding: 0;
89
- counter-reset: toc;
90
- }
91
- .primer-toc__list li {
92
- margin: 0;
93
- padding: 4px 0;
94
- counter-increment: toc;
95
- line-height: 1.4;
96
- }
97
- .primer-toc__list a {
98
- /* Two-column grid: a fixed-width numeral gutter wide enough for the
99
- longest Roman numeral in any primer (VIII = 3 letters), then the
100
- section title flows in column 2. Grid keeps the two columns
101
- independent so long numerals never overlap the title. */
102
- display: grid;
103
- grid-template-columns: 2.4em 1fr;
104
- column-gap: 0.4em;
105
- font-size: 13.5px;
106
- color: var(--ink-soft);
107
- transition: color 150ms;
108
- }
109
- .primer-toc__list a::before {
110
- content: counter(toc, upper-roman) ".";
111
- font-size: 12px;
112
- color: var(--ink-faint);
113
- letter-spacing: 0.03em;
114
- text-align: right;
115
- /* Align the period baseline with the title's first line. */
116
- line-height: 1.4;
117
- }
118
- .primer-toc__list a:hover {
119
- color: var(--ink);
120
- text-decoration: none;
121
- }
122
- .primer-toc__list a.is-active {
123
- color: var(--accent-text);
124
- font-weight: 600;
125
- }
126
- .primer-toc__list a.is-active::before {
127
- color: var(--accent-text);
128
- }
129
- .primer-toc__placeholder {
130
- color: var(--ink-faint);
131
- padding: 4px 0;
132
- }
133
-
134
- /* ---------- Article column ---------- */
135
-
136
- .primer-article {
137
- /* Reading-optimized line length. Roughly 60-72ch at 17px body. */
138
- max-width: 72ch;
139
- font-size: 1.05rem;
140
- line-height: 1.7;
141
- color: var(--ink);
142
- }
143
-
144
- .primer-loading {
145
- color: var(--ink-soft);
146
- padding: 40px 0;
147
- text-align: center;
148
- }
149
-
150
- .primer-article h1 {
151
- /* The primer's own H1 — suppressed; the top bar already names the page. */
152
- display: none;
153
- }
154
-
155
- .primer-article h2 {
156
- font-family: var(--font-display);
157
- font-weight: 700;
158
- font-size: 1.45rem;
159
- letter-spacing: -0.01em;
160
- color: var(--ink);
161
- margin: 2.4em 0 0.7em;
162
- padding-bottom: 0.45em;
163
- border-bottom: 1px solid var(--border);
164
- /* Anchor jumps must land below the sticky top bar. */
165
- scroll-margin-top: calc(var(--topbar-h) + 16px);
166
- }
167
- .primer-article h2:first-of-type {
168
- margin-top: 0.4em;
169
- }
170
-
171
- .primer-article h3 {
1
+ /* primer.css — what the PRIMER has that the other reading rooms do not
2
+ (V3.R13-followup #406; rebuilt into the chrome world by task 1003306).
3
+
4
+ The room itself — the reading measure and rhythm, the table of contents, the
5
+ progress hairline, the framed figure, the skeleton and the failure note — is
6
+ the shared archetype in room.css, which this page loads first. What is left
7
+ here is the acknowledge gate: the primer is the one document in the hall a
8
+ builder must confirm they have read, and no other room has one.
9
+
10
+ Three things this sheet used to carry that are gone rather than moved:
11
+ * `body.primer-body { background: var(--bg) }` — the shell paints the
12
+ ground now; a page repainting it is a second copy of a value that already
13
+ has an owner. The class stays on <body> because it is a page hook, not a
14
+ paint.
15
+ * `max-width: 72ch` on the article, under a comment reading "Roughly 60-72ch
16
+ at 17px body". A `ch` is the font's ZERO glyph, not a character, and
17
+ Manrope's zero is wide: it rendered about 90. The measure is the shell's
18
+ measured --measure, and it lives in room.css with the prose.
19
+ * A hand-written `z-index: 25` on the progress bar. It is --z-progress now,
20
+ named in the shell's ladder beside the topbar it has to sit under.
21
+
22
+ No compat alias (--surface / --surface-2 / --border / --border-strong /
23
+ --accent-weak / --accent-text) appears here. `--line` is a WIDTH; the
24
+ hairline's colour is --rule / --rule-soft. */
25
+
26
+ /* ---- the acknowledge gate ------------------------------------------------
27
+ Its own card under the article, at the same width, so the end of the document
28
+ and the thing to do about it read as two beats of one column. */
29
+
30
+ .primer-ack { margin: var(--sp-sm) 0 var(--sp-xl); padding: var(--room-pad); }
31
+ .primer-ack__h {
32
+ margin: 0 0 14px;
172
33
  font-family: var(--font-display);
173
- font-weight: 600;
174
- font-size: 1.1rem;
175
- color: var(--ink);
176
- margin: 1.9em 0 0.55em;
177
- scroll-margin-top: calc(var(--topbar-h) + 16px);
178
- }
179
-
180
- .primer-article p {
181
- margin: 0 0 1.15em;
182
- }
183
- .primer-article p:last-child { margin-bottom: 0; }
184
-
185
- /* The subtitle line at the very top of the primer markdown. */
186
- .primer-article > p > em:only-child {
187
- font-size: 1.02rem;
188
- color: var(--ink-soft);
189
- }
190
-
191
- .primer-article a {
192
- color: var(--accent-text);
193
- border-bottom: 1px solid var(--border);
194
- }
195
- .primer-article a:hover {
196
- color: var(--accent);
197
- border-bottom-color: var(--accent);
198
- text-decoration: none;
199
- }
200
-
201
- .primer-article strong {
34
+ font-weight: 300;
35
+ font-size: 28px;
36
+ line-height: 1.1;
37
+ letter-spacing: -0.03em;
202
38
  color: var(--ink);
203
- font-weight: 600;
204
- }
205
- .primer-article em {
206
- font-style: italic;
207
- }
208
-
209
- .primer-article code {
210
- font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
211
- font-size: 0.92em;
212
- background: var(--surface-2);
213
- border: 1px solid var(--border);
214
- padding: 1px 5px;
215
- border-radius: 4px;
216
- color: var(--accent-text);
217
- }
218
-
219
- .primer-article pre {
220
- font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
221
- font-size: 0.9em;
222
- line-height: 1.55;
223
- background: var(--surface-2);
224
- border: 1px solid var(--border);
225
- border-radius: var(--radius-sm);
226
- padding: 14px 16px;
227
- margin: 1.4em 0;
228
- overflow-x: auto;
229
- color: var(--ink);
230
- }
231
- .primer-article pre code {
232
- background: none;
233
- border: none;
234
- padding: 0;
235
- color: inherit;
236
- font-size: inherit;
237
- }
238
-
239
- .primer-article ul, .primer-article ol {
240
- margin: 0 0 1.2em;
241
- padding-left: 1.4em;
242
39
  }
243
- .primer-article li {
244
- margin: 0 0 0.35em;
245
- }
246
- .primer-article li:last-child { margin-bottom: 0; }
247
-
248
- .primer-article hr {
249
- border: none;
250
- border-top: 1px solid var(--border);
251
- margin: 2.4em 0;
252
- }
253
-
254
- /* Tables (the rank ladder). Styled like .ledger but reading-tuned. */
255
- .primer-article table {
256
- width: 100%;
257
- border-collapse: collapse;
258
- margin: 1.4em 0;
259
- font-size: 0.95em;
260
- }
261
- .primer-article th, .primer-article td {
262
- text-align: left;
263
- padding: 10px 12px;
264
- border-bottom: 1px solid var(--border);
265
- vertical-align: top;
266
- }
267
- .primer-article th {
268
- font-size: 0.78em;
269
- font-weight: 600;
270
- letter-spacing: 0.05em;
271
- text-transform: uppercase;
40
+ .primer-ack__lede {
41
+ margin: 0 0 var(--sp-md);
42
+ max-width: var(--measure);
272
43
  color: var(--ink-soft);
273
- background: var(--surface-2);
274
44
  }
275
- .primer-article tr:last-child td {
276
- border-bottom: none;
277
- }
278
-
279
- /* Inlined diagrams. The figure block sits flush in the reading column with
280
- a soft card frame so the diagrams don't feel pasted on top of the body text. */
281
- .primer-figure {
282
- margin: 1.8em 0 2em;
283
- text-align: center;
284
- }
285
- .primer-figure img {
286
- max-width: 100%;
287
- height: auto;
288
- background: var(--surface);
289
- border: 1px solid var(--border);
290
- border-radius: var(--radius-sm);
291
- padding: 12px;
292
- }
293
- .primer-figure figcaption {
294
- margin-top: 10px;
295
- font-size: 12.5px;
45
+ /* The gate is a MEASUREMENT the reader can watch move, not a refusal: the meter
46
+ says how far through the article they are against the threshold, in the label
47
+ voice, with the figure tabular so it does not re-space as it counts up. */
48
+ .primer-ack__meter {
49
+ margin: 0 0 var(--sp-lg);
50
+ font-family: var(--label-face);
51
+ font-size: var(--label-size);
52
+ letter-spacing: var(--label-space);
53
+ text-transform: var(--label-case);
296
54
  color: var(--ink-faint);
297
55
  }
56
+ .primer-ack__num { color: var(--ink); font-variant-numeric: tabular-nums; }
298
57
 
299
- /* ---------- Footer ack module ---------- */
300
-
301
- .primer-ack {
302
- max-width: 760px;
303
- margin: 24px auto 40px;
304
- }
305
- .primer-ack__inner {
306
- background: var(--surface);
307
- border: 1px solid var(--border);
308
- border-radius: var(--radius);
309
- padding: 30px 28px;
310
- text-align: center;
311
- }
312
- .primer-ack__h {
313
- font-family: var(--font-display);
314
- font-weight: 600;
315
- font-size: 1.15rem;
316
- color: var(--ink);
317
- margin: 0 0 10px;
318
- }
319
- .primer-ack__lede {
320
- margin: 0 0 20px;
321
- color: var(--ink-soft);
322
- font-size: 14px;
323
- }
58
+ /* The locked state is glass, the open state is the accent — the One Warm Thing
59
+ Rule: the accent lights only what is pressable, and while the gate is shut
60
+ this is not. aria-disabled (not disabled) keeps the control in the tab order
61
+ so a keyboard reader can land on it and hear the note below say why. */
324
62
  .primer-ack__btn {
325
- display: inline-block;
326
- padding: 10px 26px;
63
+ display: inline-flex;
64
+ align-items: center;
65
+ min-height: 48px;
66
+ padding: 0 30px;
327
67
  font-family: var(--font-body);
328
- font-size: 14px;
329
- font-weight: 600;
330
- color: #fff;
331
- background: var(--accent);
332
- border: 1px solid var(--accent-deep);
333
- border-radius: var(--radius-sm);
334
- cursor: pointer;
335
- transition: background 150ms, transform 100ms;
336
- }
337
- .primer-ack__btn:hover:not(:disabled) {
338
- background: var(--accent-deep);
339
- }
340
- .primer-ack__btn:active:not(:disabled) {
341
- transform: translateY(1px);
342
- }
343
- .primer-ack__btn:disabled {
344
- background: var(--surface-2);
68
+ font-size: 15px;
345
69
  color: var(--ink-faint);
346
- border-color: var(--border);
70
+ background: var(--glass);
71
+ border: var(--line) solid var(--glass-edge);
72
+ border-radius: var(--radius-pill);
347
73
  cursor: not-allowed;
74
+ transition: color 200ms cubic-bezier(.2, .7, .3, 1), background 200ms cubic-bezier(.2, .7, .3, 1);
348
75
  }
349
- .primer-ack__note {
350
- margin: 14px 0 0;
351
- color: var(--ink-soft);
76
+ .primer-ack__btn[aria-disabled="false"] {
77
+ background: var(--accent);
78
+ border-color: var(--accent);
79
+ color: var(--accent-on);
80
+ cursor: pointer;
352
81
  }
82
+ .primer-ack__btn[aria-disabled="false"]:hover { background: var(--accent-lift); }
353
83
 
354
- /* V3.R13-followup-2 (#411): collapsed "already acked" state — the inner
355
- box shrinks since there's no big button + lede; the confirmation reads
356
- as a single line with the check mark. */
357
- .primer-ack__inner--acked {
358
- padding: 16px 22px;
359
- }
360
- .primer-ack__inner--acked .primer-ack__h {
361
- margin-bottom: 6px;
362
- font-size: 1rem;
363
- }
364
- .primer-ack__inner--acked .primer-ack__note {
365
- margin-top: 0;
366
- color: var(--ink);
84
+ .primer-ack__note {
85
+ margin: var(--sp-sm) 0 0;
86
+ max-width: var(--measure);
87
+ font-size: 13.5px;
88
+ color: var(--ink-faint);
367
89
  }
368
- .primer-ack__check {
90
+ .primer-ack__check { margin-right: 6px; color: var(--ok); font-weight: 600; }
91
+ /* The way back, once the gate is closed. It is a link in a sentence with no
92
+ vertical rhythm of its own — measured 84x18 in the acked state — so it takes
93
+ the shell's remedy for exactly that shape (.scroll__lede a in style.css):
94
+ go inline-block and buy the height back with padding the line does not pay
95
+ for. The class is this page's, so the rule is too. */
96
+ .primer-ack__note a {
369
97
  display: inline-block;
370
- margin-right: 6px;
371
- color: var(--ok);
372
- font-weight: 700;
373
- }
374
-
375
- /* ---------- Responsive (mobile) ----------
376
- Breakpoint mirrors primer.js's matchMedia('(max-width: 760px)') — keep in sync. */
377
-
378
- @media (max-width: 760px) {
379
- .primer-layout {
380
- grid-template-columns: minmax(0, 1fr);
381
- gap: 0;
382
- }
383
- .primer-toc {
384
- position: static;
385
- margin-bottom: 20px;
386
- }
387
- .primer-toc__toggle {
388
- display: flex;
389
- }
390
- .primer-toc__inner {
391
- display: none;
392
- margin-top: 6px;
393
- }
394
- .primer-toc.is-open .primer-toc__inner {
395
- display: block;
396
- }
397
- .primer-article {
398
- font-size: 1rem;
399
- line-height: 1.65;
400
- }
401
- .primer-article h2 {
402
- font-size: 1.25rem;
403
- }
404
- /* A markdown table's column count is fixed by the source, so its min-content
405
- width outruns a phone (measured: 79px of page overflow on /drachmae). Give
406
- the table its own horizontal scroller instead of letting it drag the page
407
- sideways — the .section-rail / .kit-tabs strips already set that precedent.
408
- `display: block` is what makes the table box a scrollport; the rows still
409
- lay out through the anonymous table box inside it. Nothing here is sticky,
410
- so this costs no header behaviour. */
411
- .primer-article table {
412
- display: block;
413
- width: max-content;
414
- max-width: 100%;
415
- overflow-x: auto;
416
- }
417
- }
98
+ min-width: 24px;
99
+ min-height: 24px;
100
+ padding: 3px 4px;
101
+ margin: 0 -4px;
102
+ line-height: 18px;
103
+ }
104
+
105
+ /* V3.R13-followup-2 (#411): the acked panel collapses — no big button, no
106
+ lede, just the confirmation line, so a re-read is not shouted at. */
107
+ .primer-ack--acked { padding: var(--sp-md) var(--sp-lg); }
108
+ .primer-ack--acked .primer-ack__h { margin-bottom: 6px; font-size: 20px; }
109
+ .primer-ack--acked .primer-ack__note { margin-top: 0; color: var(--ink-soft); }
@@ -9,15 +9,14 @@
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
11
  <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
12
+ <link rel="stylesheet" href="/builders/room.css">
12
13
  <link rel="stylesheet" href="/builders/primer.css?v=2026-07-02-shell1">
13
14
  <link rel="stylesheet" href="/builders/diagram-viewer.css?v=2026-06-14-diagviewer-1">
14
15
  </head>
15
- <body class="primer-body" data-page="primer" data-page-title="Primer">
16
-
17
- <!-- Reading-progress bar — fills as the reader scrolls through the article.
18
- Pinned just below the sticky top bar (z below the sidebar so it reads as
19
- the content column's progress, not app chrome). -->
20
- <div class="primer-progress" aria-hidden="true"><div class="primer-progress__bar" id="primer-progress-bar"></div></div>
16
+ <!-- `room` opts this page into the reading-room archetype's composition tokens
17
+ (room.css). They sit on the body, never in a page :root — the branding
18
+ pack's :root is spliced at the end of <head> and outranks one. -->
19
+ <body class="room primer-body" data-page="primer" data-page-title="Primer">
21
20
 
22
21
  <div class="app">
23
22
  <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
@@ -26,19 +25,34 @@
26
25
  <header class="app-topbar" id="app-topbar"></header>
27
26
  <div class="app-content">
28
27
 
29
- <div class="page-head">
28
+ <!-- Reading-progress hairline — fills as the reader scrolls the article.
29
+ Sticky under the topbar rather than fixed over the viewport: it is the
30
+ content column's progress, not app chrome, and --z-progress puts it
31
+ below the topbar in the ladder for the same reason. -->
32
+ <div class="primer-progress" aria-hidden="true"><div class="primer-progress__bar" id="primer-progress-bar"></div></div>
33
+
34
+ <!-- The reading room announces its document: the label voice on a
35
+ one-word eyebrow, then the room's name lowercase in the display face.
36
+ The board's head is deliberately the quiet inverse of this — see
37
+ .page-head--room in style.css. The page had no <h1> at all before
38
+ task 1003306, so it failed one-header-main-h1 on every render. -->
39
+ <div class="page-head page-head--room">
40
+ <p class="page-head__eyebrow">Docs</p>
41
+ <h1 class="page-head__h">The primer</h1>
30
42
  <p class="page-head__sub">Everything a new builder needs before claiming their first task. About a ten-minute read.</p>
31
43
  </div>
32
44
 
33
45
  <main id="hall-main">
34
46
  <div class="primer-layout">
35
47
 
36
- <!-- Sticky left-rail table of contents. Built from the article's h2s after
37
- the markdown renders. Mobile: collapses behind a toggle. -->
48
+ <!-- Sticky left-rail table of contents. Built from the article's h2s
49
+ after the markdown renders; the numerals are drawn by room.css's
50
+ counter, so the markup carries only titles. Mobile: collapses
51
+ behind a toggle. -->
38
52
  <aside class="primer-toc" id="primer-toc" aria-label="Sections in this primer">
39
53
  <button type="button" class="primer-toc__toggle" id="primer-toc-toggle" aria-expanded="false" aria-controls="primer-toc-list">
40
54
  <span>Sections</span>
41
- <span class="primer-toc__chevron" aria-hidden="true">▾</span>
55
+ <svg class="primer-toc__chevron" viewBox="0 0 10 6" width="10" height="6" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 1.2 5 4.8 9 1.2"/></svg>
42
56
  </button>
43
57
  <nav class="primer-toc__inner">
44
58
  <p class="primer-toc__heading" id="primer-toc-heading">In this primer</p>
@@ -48,25 +62,50 @@
48
62
  </nav>
49
63
  </aside>
50
64
 
51
- <!-- Article column — markdown rendered here. Aria role 'main' so screen
52
- readers jump straight to the reading content. -->
53
- <article class="primer-article" id="primer-article" aria-busy="true">
54
- <div class="primer-loading">Loading the primer…</div>
55
- </article>
65
+ <!-- The article column. Three panels of the same card at the same
66
+ width — skeleton, document, failure — so the column never changes
67
+ shape as the markdown resolves. -->
68
+ <div class="primer-col">
56
69
 
57
- </div>
70
+ <div class="primer-skeleton scroll" id="primer-skeleton" aria-hidden="true">
71
+ <span class="primer-sk primer-skeleton__h"></span>
72
+ <div class="primer-skeleton__p">
73
+ <span class="primer-sk"></span><span class="primer-sk"></span>
74
+ <span class="primer-sk"></span><span class="primer-sk"></span>
75
+ <span class="primer-sk"></span>
76
+ </div>
77
+ <div class="primer-skeleton__rule"></div>
78
+ <div class="primer-skeleton__p">
79
+ <span class="primer-sk"></span><span class="primer-sk"></span>
80
+ <span class="primer-sk"></span><span class="primer-sk"></span>
81
+ <span class="primer-sk"></span>
82
+ </div>
83
+ </div>
84
+
85
+ <article class="primer-article scroll" id="primer-article" aria-busy="true" hidden></article>
86
+
87
+ <div class="primer-note primer-note--error scroll" id="primer-error" hidden>
88
+ <p class="primer-note__label">The primer did not load</p>
89
+ <p class="primer-note__body">The markdown behind this page could not be fetched. Reload to try again, or read it in the repo.</p>
90
+ <p class="primer-note__acts"><a class="btn btn--ghost" href="/builders">Back to home</a></p>
91
+ </div>
92
+
93
+ <!-- Footer ack module. The button is soft-gated with aria-disabled
94
+ (NOT disabled) until the reader has scrolled past ~90% of the
95
+ article: a real disabled button leaves the tab order, so a
96
+ keyboard reader could never land on it and never heard why it
97
+ was locked. -->
98
+ <section class="primer-ack scroll" id="primer-ack" aria-labelledby="primer-ack-h" hidden>
99
+ <h2 class="primer-ack__h" id="primer-ack-h">Acknowledge the primer</h2>
100
+ <p class="primer-ack__lede" id="primer-ack-lede">Read to the end and the button below unlocks. It is an honour system — nothing here checks your comprehension.</p>
101
+ <p class="primer-ack__meter" id="primer-ack-meter">Read <span class="primer-ack__num">0%</span> of the 90% needed</p>
102
+ <button type="button" class="primer-ack__btn" id="primer-ack-btn" aria-disabled="true" aria-describedby="primer-ack-note-locked">I have read it</button>
103
+ <p class="primer-ack__note" id="primer-ack-note-locked">Locked until you reach the end of the article above.</p>
104
+ <p class="primer-ack__note" id="primer-ack-note" hidden></p>
105
+ </section>
58
106
 
59
- <!-- Footer ack module — sits below the article. Button is soft-gated (disabled
60
- class) until the reader has scrolled past ~90% of the content. The honor-
61
- system signal: "I have read it" only enables once they've actually read it. -->
62
- <section class="primer-ack" id="primer-ack" hidden>
63
- <div class="primer-ack__inner">
64
- <h2 class="primer-ack__h">Acknowledge the primer</h2>
65
- <p class="primer-ack__lede" id="primer-ack-lede">Scroll through the primer above. Once you've reached the end, the button below activates.</p>
66
- <button type="button" class="primer-ack__btn" id="primer-ack-btn" disabled>I have read it</button>
67
- <p class="primer-ack__note" id="primer-ack-note" hidden>Already acknowledged — you're all set. <a href="/">Back to home</a>.</p>
68
107
  </div>
69
- </section>
108
+ </div>
70
109
  </main>
71
110
 
72
111
  <footer class="app-foot" id="app-foot"></footer>
@@ -79,6 +118,9 @@
79
118
  <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
80
119
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
81
120
  <script src="/builders/palette.js" defer></script>
121
+ <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
122
+ it rather than carrying a second copy of the grammar. -->
123
+ <script src="/builders/md-reader.js" defer></script>
82
124
  <script src="/builders/primer.js?v=2026-05-29-primer-1" defer></script>
83
125
  <script src="/builders/diagram-viewer.js?v=2026-06-14-diagviewer-1" defer></script>
84
126
  <script src="/builders/copy-on-click.js?v=2026-06-01-copy-1" defer></script>