@airmark/quiver 0.33.0 → 0.35.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.
Files changed (54) hide show
  1. package/README.md +18 -12
  2. package/package.json +7 -7
  3. package/quills/af4141/0.1.0/Quill.yaml +3 -12
  4. package/quills/af4141/0.1.0/example.md +53 -0
  5. package/quills/afmc_moa/0.0.1/Quill.yaml +29 -45
  6. package/quills/afmc_moa/0.0.1/example.md +114 -0
  7. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/config.typ +1 -1
  8. package/quills/classic_resume/{0.0.1 → 0.1.0}/Quill.yaml +88 -168
  9. package/quills/classic_resume/0.1.0/example.md +111 -0
  10. package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/src/components.typ +77 -35
  11. package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/src/config.typ +23 -14
  12. package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/src/layout.typ +37 -7
  13. package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/src/lib.typ +1 -0
  14. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/util.typ +50 -0
  15. package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/typst.toml +0 -5
  16. package/quills/classic_resume/{0.0.1 → 0.1.0}/plate.typ +63 -56
  17. package/quills/cyber_career_plan/0.1.0/Quill.yaml +328 -0
  18. package/quills/cyber_career_plan/0.1.0/example.md +55 -0
  19. package/quills/cyber_career_plan/0.1.0/plate.typ +681 -0
  20. package/quills/daf1206/0.1.0/Quill.yaml +14 -16
  21. package/quills/daf1206/0.1.0/example.md +55 -0
  22. package/quills/daf4392/0.1.0/Quill.yaml +1 -6
  23. package/quills/daf4392/0.1.0/example.md +51 -0
  24. package/quills/usaf_letter/0.1.0/Quill.yaml +7 -42
  25. package/quills/usaf_letter/0.1.0/example.md +35 -0
  26. package/quills/{usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE → usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/OFL.txt} +2 -2
  27. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/SpectralSC-Italic.ttf +0 -0
  28. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/SpectralSC-Regular.ttf +0 -0
  29. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/frontmatter.typ +1 -1
  30. package/quills/usaf_letter/0.1.0/plate.typ +1 -4
  31. package/quills/usaf_memo/0.2.0/Quill.yaml +7 -40
  32. package/quills/usaf_memo/0.2.0/example.md +45 -0
  33. package/quills/usaf_memo/0.3.0/Quill.yaml +13 -50
  34. package/quills/usaf_memo/0.3.0/example.md +48 -0
  35. package/quills/{usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/Cinzel/LICENSE → usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/OFL.txt} +2 -2
  36. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/SpectralSC-Italic.ttf +0 -0
  37. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/SpectralSC-Regular.ttf +0 -0
  38. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +1 -1
  39. package/quills/usaf_memo/0.3.0/plate.typ +1 -4
  40. package/templates/deros_extension.md +40 -0
  41. package/templates/templates.json +7 -0
  42. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/util.typ +0 -32
  43. package/quills/cyber_ribbon_chart/0.0.1/Quill.yaml +0 -320
  44. package/quills/cyber_ribbon_chart/0.0.1/plate.typ +0 -428
  45. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
  46. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
  47. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
  48. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
  49. /package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/LICENSE +0 -0
  50. /package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Bold.ttf +0 -0
  51. /package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-BoldItalic.ttf +0 -0
  52. /package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Italic.ttf +0 -0
  53. /package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Regular.ttf +0 -0
  54. /package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/fonts/EBGaramond/OFL.txt +0 -0
@@ -1,428 +0,0 @@
1
- #import "@local/quillmark-helper:0.1.0": data
2
-
3
- #let ink = luma(12%)
4
- #let mute = luma(45%)
5
- #let faint = luma(62%)
6
-
7
- #set page(width: 11in, height: 8.5in, margin: 0.45in)
8
- #set text(font: "Figtree", size: 8pt, fill: ink)
9
-
10
- #set document(
11
- title: "Ribbon Chart — " + data.name,
12
- author: data.name,
13
- )
14
-
15
- // A `plaintext` field lowers to content padded with a space on each side, which
16
- // would print inside a bold run and defeat an emptiness test.
17
- #let trim(v) = {
18
- if type(v) != content { return v }
19
- let kids = v.at("children", default: none)
20
- if kids == none { return v }
21
- let pad(c) = c == [ ] or c.func() == parbreak
22
- while kids.len() > 0 and pad(kids.first()) { kids = kids.slice(1) }
23
- while kids.len() > 0 and pad(kids.last()) { kids = kids.slice(0, -1) }
24
- kids.sum(default: [])
25
- }
26
- #let blank(v) = { let t = trim(v); t == [] or t == "" }
27
-
28
- // The bundled fallback face carries no geometric glyphs — no ✓, ■, □, ●, ▸ — so
29
- // a mark is drawn rather than set. Held against open is a value contrast, which
30
- // is what survives the photocopier these are read from.
31
- #let held-mark = box(baseline: 0.5pt, rect(width: 4.6pt, height: 4.6pt, fill: ink, stroke: 0.5pt + ink))
32
- #let open-mark = box(baseline: 0.5pt, rect(width: 4.6pt, height: 4.6pt, fill: none, stroke: 0.5pt + faint))
33
-
34
- #let section-rule(title) = block(spacing: 0pt, below: 5pt)[
35
- #text(size: 6.6pt, weight: 700, tracking: 0.7pt)[#upper(title)]
36
- #v(2pt)
37
- #line(length: 100%, stroke: 0.6pt + ink)
38
- ]
39
-
40
- #let fact(label, value) = block(spacing: 0pt)[
41
- #text(size: 5.8pt, weight: 600, tracking: 0.4pt, fill: mute)[#upper(label)]
42
- #v(1.5pt)
43
- #text(size: 8.5pt, weight: 500)[#if value == none { text(fill: faint)[—] } else { value }]
44
- ]
45
-
46
- #let show-date(d) = if d == none { none } else { d.display("[day padding:none] [month repr:short] [year]") }
47
-
48
- // ─── the ladder ──────────────────────────────────────────────────────────────
49
- // Every 17X board and school gate, as an offset from the year group, counted
50
- // from the adjusted year group where one is set because that is the year a
51
- // board counts from. The tier is how heavily it prints: a board is a date you
52
- // are ranked against, a look is one of a numbered series, a course is neither.
53
- #let ladder = (
54
- (3, "Cyber 200", "course"),
55
- (4, "SOS window", "course"),
56
- (8, "Maj board", "board"),
57
- (8, "IDE 1st look", "look"),
58
- (9, "Cyber 300", "course"),
59
- (9, "IDE 2nd look", "look"),
60
- (10, "IDE 3rd look", "look"),
61
- (11, "IDE final look", "look"),
62
- (12, "Lt Col board", "board"),
63
- (14, "SDE 1st look", "look"),
64
- (15, "SDE 2nd look", "look"),
65
- (16, "SDE 3rd look", "look"),
66
- (17, "SDE final look", "look"),
67
- )
68
-
69
- // ─── inputs ──────────────────────────────────────────────────────────────────
70
- #let comm-yg = data.commissioning_yg
71
- #let adj-yg = data.adjusted_yg
72
- #let board-yg = if adj-yg != 0 { adj-yg } else { comm-yg }
73
- #let start-year = data.timeline_start_year
74
- #let years = calc.max(1, data.timeline_years)
75
- #let cols = years * 2
76
- #let narrow = years > 12
77
- #let band-height = 0.52in
78
- #let chip-size = if narrow { 5.6pt } else { 6.2pt }
79
- #let chip-inset = if narrow { 2pt } else { 3.5pt }
80
-
81
- // Three weights of gate, by what each one is: a board ranks you against a year
82
- // group on a date, a look is one of a numbered series, a course is a seat.
83
- #let chip(label, tier) = {
84
- if tier == "board" {
85
- box(fill: ink, inset: (x: chip-inset, y: 2pt))[#text(size: chip-size, weight: 700, fill: white)[#label]]
86
- } else if tier == "look" {
87
- box(fill: none, stroke: 0.5pt + ink, inset: (x: chip-inset, y: 2pt))[#text(size: chip-size, weight: 600)[#label]]
88
- } else {
89
- box(inset: (x: 0.5pt, y: 2pt))[#text(size: chip-size, fill: mute)[#label]]
90
- }
91
- }
92
-
93
- // One tint per vector, darkest first, so the shade says which row this is.
94
- // Adjacent tours part by a gap and an edge instead, and at the third rank the
95
- // edge is doing all the work — which is what a photocopy leaves anyway.
96
- #let rank-tints = (luma(86%), luma(92%), luma(96%))
97
-
98
-
99
- // ─── vectors ─────────────────────────────────────────────────────────────────
100
- #let vectors = data.vectors.enumerate().map(((i, v)) => (
101
- label: if blank(v.label) { [Vector #(i + 1)] } else { trim(v.label) },
102
- focus: trim(v.focus),
103
- tours: v.tours.map(t => (
104
- title: trim(t.title),
105
- duration: float(t.duration),
106
- vml: t.vml,
107
- school: t.school,
108
- )),
109
- ))
110
-
111
- // A half-year column is H1 (Jan–Jun, even) or H2 (Jul–Dec, odd), and a VML
112
- // cycle picks the phase a tour may start on. Where that means waiting, the wait
113
- // is added to the tour before it rather than left as a hole: you hold a job
114
- // until you move, and an empty half-year on a career chart reads as
115
- // unemployment. Only the first tour can be preceded by nothing, and its indent
116
- // is the half-year before a summer move.
117
- //
118
- // Nothing is dropped. A tour the window cuts keeps a torn edge and says which
119
- // year it runs past; one starting beyond the window is counted on its row.
120
- #let place(tours) = {
121
- let cursor = 0
122
- let out = ()
123
- for t in tours {
124
- let phase = if t.vml == "Winter" { 0 } else { 1 }
125
- let start = cursor
126
- if calc.rem(start, 2) != phase { start += 1 }
127
- if start > cursor and out.len() > 0 {
128
- let prev = out.last()
129
- prev.span += start - cursor
130
- prev.held = true
131
- out.at(out.len() - 1) = prev
132
- }
133
- out.push((
134
- tour: t,
135
- start: start,
136
- span: calc.max(1, int(calc.round(t.duration * 2))),
137
- held: false,
138
- ))
139
- cursor = out.last().start + out.last().span
140
- }
141
- out.map(p => if p.start >= cols { (..p, beyond: true) } else {
142
- (..p, beyond: false, drawn: calc.min(p.span, cols - p.start), clipped: p.start + p.span > cols)
143
- })
144
- }
145
-
146
- // The band is ruled to an even height, so a narrow block cannot grow to fit its
147
- // title: the title sets to the block instead. That is the ceiling `title`
148
- // warns about in Quill.yaml — about twenty characters per year of length.
149
- #let title-size(span) = if span <= 1 { 6.4pt } else if span == 2 { 7.2pt } else { 8pt }
150
-
151
- // Half-columns back to years, so a stretched block prints the length it draws
152
- // rather than the length it was given.
153
- #let span-label(span) = {
154
- let years = span / 2
155
- if calc.rem(span, 2) == 0 { [#int(years) yr] } else { [#years yr] }
156
- }
157
-
158
- // ═══ IDENTITY ════════════════════════════════════════════════════════════════
159
- #grid(
160
- columns: (2.7in, 1fr),
161
- column-gutter: 18pt,
162
- align: bottom,
163
- [
164
- #text(size: 17pt, weight: 800, tracking: -0.25pt)[#data.name]
165
- #if not blank(data.duty_title) [
166
- #v(2pt)
167
- #text(size: 8pt, weight: 500, fill: mute)[#data.duty_title]
168
- ]
169
- ],
170
- grid(
171
- columns: (1fr,) * 5,
172
- column-gutter: 12pt,
173
- align: top,
174
- fact("Commissioning YG", [#comm-yg]),
175
- fact("Adjusted YG", if adj-yg != 0 { [#adj-yg] } else { none }),
176
- fact("Date of rank", show-date(data.date_of_rank)),
177
- fact("Arrived station", show-date(data.date_arrived_station)),
178
- fact("Advanced degree", if blank(data.advanced_degree) { none } else { data.advanced_degree }),
179
- ),
180
- )
181
-
182
- #v(6pt)
183
- #line(length: 100%, stroke: 1.3pt + ink)
184
- #v(9pt)
185
-
186
- // ═══ TIMELINE ════════════════════════════════════════════════════════════════
187
- #let in-window(cy) = cy >= start-year and cy < start-year + years
188
- #let ahead = ladder.filter(m => board-yg + m.at(0) >= start-year + years)
189
-
190
- #block(spacing: 0pt)[
191
- #grid(columns: (1fr, auto), align: (left + bottom, right + bottom),
192
- text(size: 6.6pt, weight: 700, tracking: 0.7pt)[#upper("Assignment vectors & development timeline")],
193
- text(size: 6.2pt, fill: mute)[
194
- CY #start-year–#(start-year + years - 1) · counted from YG #board-yg
195
- #if ahead.len() > 0 [ · #ahead.len() later milestone#if ahead.len() > 1 [s] beyond #(start-year + years - 1)]
196
- ],
197
- )
198
- #v(2pt)
199
- #line(length: 100%, stroke: 0.6pt + ink)
200
- ]
201
- #v(5pt)
202
-
203
- #let rows = ()
204
-
205
- // year header
206
- #rows.push(table.cell(stroke: none)[])
207
- #for i in range(years) {
208
- rows.push(table.cell(colspan: 2, stroke: (bottom: 0.7pt + ink), inset: (bottom: 2.5pt))[
209
- #text(size: 8pt, weight: 700)[#(start-year + i)]
210
- #linebreak()
211
- #text(size: 5.8pt, fill: mute)[YG+#(start-year + i - board-yg)]
212
- ])
213
- }
214
-
215
- // milestone row
216
- #rows.push(table.cell(align: left + horizon, stroke: none)[
217
- #text(size: 6.4pt, weight: 600, fill: mute)[Eligibility]
218
- ])
219
- #for i in range(years) {
220
- let cy = start-year + i
221
- let hits = ladder.filter(m => board-yg + m.at(0) == cy)
222
- rows.push(table.cell(colspan: 2, stroke: none, inset: (x: 1.5pt, y: 3pt))[
223
- #for m in hits [
224
- #block(spacing: 2pt, breakable: false)[#chip(m.at(1), m.at(2))]
225
- ]
226
- ])
227
- }
228
-
229
- // one row per vector
230
- #for (rank, v) in vectors.enumerate() {
231
- let placed = place(v.tours)
232
- let beyond = placed.filter(p => p.beyond)
233
- let top-rule = (top: 0.4pt + luma(80%))
234
- let tint = rank-tints.at(calc.min(rank, rank-tints.len() - 1))
235
-
236
- rows.push(table.cell(align: left + horizon, stroke: top-rule, inset: (right: 5pt, y: 3pt))[
237
- #text(size: 8.5pt, weight: 700)[#v.label]
238
- #if not blank(v.focus) or beyond.len() > 0 [
239
- #linebreak()
240
- #text(size: 6pt, fill: mute)[
241
- #if not blank(v.focus) [#v.focus]
242
- #if beyond.len() > 0 [
243
- #if not blank(v.focus) [ · ]
244
- +#beyond.len() past #(start-year + years - 1)
245
- ]
246
- ]
247
- ]
248
- ])
249
-
250
- let at = 0
251
- let idx = 0
252
- for p in placed.filter(p => not p.beyond) {
253
- if p.start > at {
254
- rows.push(table.cell(colspan: p.start - at, stroke: top-rule)[])
255
- at = p.start
256
- }
257
- rows.push(table.cell(
258
- colspan: p.drawn,
259
- stroke: top-rule,
260
- inset: (x: 1pt, y: 2pt),
261
- align: left + horizon,
262
- )[
263
- // The block is drawn inside its cell rather than as the cell's fill, so
264
- // adjacent tours are parted by a gap and an edge instead of by a second
265
- // shade — which is what lets the shade say which vector this is.
266
- #box(
267
- width: 100%,
268
- height: 100%,
269
- fill: if p.tour.school { none } else { tint },
270
- stroke: if p.tour.school { (paint: mute, thickness: 0.5pt, dash: (2pt, 1.5pt)) } else { 0.5pt + luma(55%) },
271
- inset: (x: 3pt, y: 2pt),
272
- baseline: 0pt,
273
- )[
274
- #set align(left + horizon)
275
- #text(size: title-size(p.drawn), weight: 600, style: if p.tour.school { "italic" } else { "normal" })[
276
- #p.tour.title
277
- ]
278
- #linebreak()
279
- #text(size: 5.8pt, fill: mute)[
280
- #span-label(p.span)#if p.held [ #sym.dagger] · #p.tour.vml#if p.clipped [ · runs past #(start-year + years - 1)]
281
- ]
282
- ]
283
- ])
284
- at = p.start + p.drawn
285
- idx += 1
286
- }
287
- if at < cols { rows.push(table.cell(colspan: cols - at, stroke: top-rule)[]) }
288
- }
289
-
290
- #table(
291
- columns: (1.2in,) + (1fr,) * cols,
292
- rows: (auto, auto) + (band-height,) * vectors.len(),
293
- stroke: none,
294
- inset: 2pt,
295
- ..rows,
296
- )
297
-
298
- // The legend names the marks this chart actually carries and no others.
299
- #let drawn-tours = vectors.map(v => place(v.tours).filter(p => not p.beyond)).flatten()
300
- #let shown-tiers = ladder.filter(m => in-window(board-yg + m.at(0))).map(m => m.at(2)).dedup()
301
- #let legend = ()
302
- #if "board" in shown-tiers { legend.push([#chip("board", "board") ranked against your year group]) }
303
- #if "look" in shown-tiers { legend.push([#chip("look", "look") one of a numbered series]) }
304
- #if drawn-tours.any(p => not p.tour.school) {
305
- legend.push([#box(width: 14pt, height: 6pt, fill: rank-tints.at(0), stroke: 0.5pt + luma(55%)) assignment])
306
- }
307
- #if drawn-tours.any(p => p.tour.school) {
308
- legend.push([#box(width: 14pt, height: 6pt, stroke: (paint: mute, thickness: 0.5pt, dash: (2pt, 1.5pt))) school])
309
- }
310
- #if drawn-tours.any(p => p.held) { legend.push([#sym.dagger held to the next cycle]) }
311
-
312
- #if legend.len() > 0 {
313
- v(3pt)
314
- text(size: 5.8pt, fill: mute, legend.join(h(6pt)))
315
- }
316
-
317
- #v(9pt)
318
-
319
- // ═══ RECORD ══════════════════════════════════════════════════════════════════
320
- // Every member arrives, held or not, in the order Quill.yaml declares it; an
321
- // unheld member's detail arrives blank whatever the document retains.
322
- #let quals = data.qualifications
323
- #let vocabulary = (
324
- ("Leadership & Command", quals.command),
325
- ("Operations", quals.operations),
326
- ("Staff & Functional", quals.staff),
327
- ("Education & PME", quals.education),
328
- )
329
-
330
- #let remarks = (
331
- ("Awards", data.awards),
332
- ("Certifications", data.certifications),
333
- ("Deployments", data.deployments),
334
- ).filter(r => r.at(1).len() > 0)
335
-
336
- // The stratifications sit beside the vocabulary; with none, there is no narrow
337
- // empty column beside it, there is no column.
338
- #let strats-column = [
339
- #section-rule("Recent stratifications")
340
- #for s in data.stratifications {
341
- block(spacing: 7pt)[
342
- #grid(columns: (0.44in, 1fr), column-gutter: 7pt, align: (right + top, left + top),
343
- text(size: 8.5pt, weight: 700)[#s.year],
344
- [
345
- #if not blank(s.rater_strat) [#text(size: 8pt)[#s.rater_strat]]
346
- #if not blank(s.rater_strat) and not blank(s.hlr_strat) [#linebreak()]
347
- #if not blank(s.hlr_strat) [#text(size: 7.4pt, fill: mute)[#s.hlr_strat]]
348
- ],
349
- )
350
- ]
351
- }
352
- ]
353
-
354
- #let vocabulary-column = [
355
- #section-rule("Qualifications & experience")
356
- #grid(
357
- columns: (1fr,) * vocabulary.len(),
358
- column-gutter: 14pt,
359
- ..vocabulary.map(group => {
360
- let (heading, members) = group
361
- [
362
- #text(size: 6.2pt, weight: 700, tracking: 0.4pt, fill: mute)[#upper(heading)]
363
- #v(3pt)
364
- #set par(leading: 0.42em)
365
- #for (_, m) in members {
366
- block(spacing: 0pt, inset: (y: 1.1pt))[
367
- #grid(columns: (7.6pt, 1fr), align: (left + top, left + top))[
368
- #if m.held { held-mark } else { open-mark }
369
- ][
370
- #text(
371
- size: 7.2pt,
372
- weight: if m.held { 600 } else { 400 },
373
- fill: if m.held { ink } else { luma(50%) },
374
- )[#m.title]
375
- #if not blank(m.detail) [
376
- #linebreak()
377
- #text(size: 6.4pt, style: "italic", fill: mute)[#trim(m.detail)]
378
- ]
379
- ]
380
- ]
381
- }
382
- ]
383
- }),
384
- )
385
- ]
386
-
387
- #if data.stratifications.len() > 0 {
388
- grid(columns: (2.45in, 1fr), column-gutter: 24pt, strats-column, vocabulary-column)
389
- } else {
390
- vocabulary-column
391
- }
392
-
393
- // Three lists side by side across the full width. Stacked in a narrow column
394
- // they cost three times the height and read as one long list.
395
- #if remarks.len() > 0 [
396
- #v(8pt)
397
- #section-rule("Awards · certifications · deployments")
398
- #grid(
399
- columns: (1fr,) * 3,
400
- column-gutter: 24pt,
401
- ..remarks.map(((label, lines)) => [
402
- #text(size: 5.8pt, weight: 600, tracking: 0.4pt, fill: mute)[#upper(label)]
403
- #v(2pt)
404
- #for l in lines [#block(spacing: 2.5pt)[#text(size: 7.6pt)[#l]]]
405
- ]),
406
- )
407
- ]
408
-
409
- // ═══ NOTES ═══════════════════════════════════════════════════════════════════
410
- // The one thing the page does that the browser tool it came from could not: a
411
- // rater writes on it during the discussion, and the sheet becomes the record of
412
- // the conversation. So the lines print whether or not anything was typed above
413
- // them.
414
- #v(9pt)
415
- #section-rule("Development notes")
416
- #let typed = data.at("$body", default: "")
417
- #if type(typed) != str [
418
- #block(spacing: 6pt)[#text(size: 7.6pt)[#typed]]
419
- ]
420
- #for _ in range(2) {
421
- v(11pt)
422
- line(length: 100%, stroke: 0.4pt + luma(80%))
423
- }
424
- #v(8pt)
425
- #text(size: 6.2pt, fill: mute)[
426
- Discussed with #box(width: 1.7in, stroke: (bottom: 0.4pt + luma(60%)))[]
427
- #h(10pt) on #box(width: 1in, stroke: (bottom: 0.4pt + luma(60%)))[]
428
- ]