@usableapp/cardds 0.7.103 → 0.7.104

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 (60) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/css/actions.css +102 -36
  3. package/css/base.css +139 -15
  4. package/css/card.css +308 -79
  5. package/css/choice.css +25 -33
  6. package/css/finish.css +28 -27
  7. package/css/forms.css +6 -14
  8. package/css/journey.css +4 -4
  9. package/css/layover.css +26 -24
  10. package/css/lists.css +8 -6
  11. package/css/media.css +13 -74
  12. package/css/numbers.css +37 -32
  13. package/css/palette.css +1 -1
  14. package/css/people.css +3 -8
  15. package/css/post.css +23 -19
  16. package/css/sheet.css +84 -56
  17. package/css/stack.css +39 -215
  18. package/css/stamp.css +41 -29
  19. package/css/step.css +1 -107
  20. package/css/sticky.css +43 -41
  21. package/css/theme.template.css +12 -0
  22. package/css/tokens.css +100 -26
  23. package/dist/actions/Chip.d.ts +1 -1
  24. package/dist/actions/Chip.js +3 -2
  25. package/dist/cardds.css +1053 -868
  26. package/dist/cards/Card.d.ts +3 -3
  27. package/dist/cards/Card.js +1 -1
  28. package/dist/cards/CardFeed.js +7 -9
  29. package/dist/catalog.json +1 -1
  30. package/dist/layover/Snackbar.js +2 -2
  31. package/dist/lists/Bubble.d.ts +1 -1
  32. package/dist/lists/Bubble.js +1 -1
  33. package/dist/lists/Row.js +5 -7
  34. package/dist/numbers/Picker.js +11 -18
  35. package/dist/people/Avatar.d.ts +1 -1
  36. package/dist/people/Avatar.js +3 -2
  37. package/dist/post/PostDesk.js +6 -9
  38. package/dist/post/PostLetter.js +4 -4
  39. package/dist/pressable.d.ts +9 -0
  40. package/dist/pressable.js +6 -0
  41. package/dist/room.d.ts +5 -0
  42. package/dist/room.js +9 -0
  43. package/dist/scaffold/Panes.js +8 -14
  44. package/dist/sheets/Sheet.d.ts +1 -1
  45. package/dist/sheets/Sheet.js +2 -3
  46. package/dist/sheets/SheetSteps.js +3 -3
  47. package/dist/sheets/StackSheet.d.ts +1 -1
  48. package/dist/sheets/sheetStepsPile.js +2 -3
  49. package/dist/sheets/stackCard.js +67 -115
  50. package/dist/state.d.ts +21 -0
  51. package/dist/state.js +28 -1
  52. package/dist/sticky/Sticky.js +3 -7
  53. package/dist/templates/Home.d.ts +1 -1
  54. package/dist/templates/Splash.js +2 -2
  55. package/dist/templates/Walkthrough.js +4 -5
  56. package/docs/reference.md +71 -9
  57. package/lib/catalog.mjs +23 -7
  58. package/lint/rules.mjs +7 -5
  59. package/lint/values.mjs +144 -0
  60. package/package.json +6 -4
package/css/stack.css CHANGED
@@ -53,22 +53,9 @@
53
53
  Lh, 2026-09-11). --_n: how many sheets the stack holds, read from the DOM
54
54
  by :has below so the closed geometry needs no script and no per-stack
55
55
  declaration. */
56
- .sheet-stack { display: grid; grid-template-rows: minmax(0, 1fr); --stack-step: 5.5rem; margin-inline: calc(-1 * var(--screen-pad)); --_n: 1; }
56
+ .sheet-stack { display: grid; grid-template-rows: minmax(0, 1fr); --stack-step: calc(var(--tap-base) * 11 / 6); margin-inline: calc(-1 * var(--screen-pad)); --_n: 1; }
57
57
  .sheet-stack > .card {
58
- grid-area: 1 / 1; /* every card shares the stack's cell; later ones offset down */
59
- border-bottom-left-radius: 0;
60
- border-bottom-right-radius: 0;
61
- border-bottom: 0;
62
- align-content: start;
63
- align-self: start; /* own natural height (padding + head) by default */
64
58
  --card-gap: var(--sp-5); /* head→content gap = the strip's 20px rhythm */
65
- z-index: var(--level, 1); /* explicit paint order, 1 = back of the stack */
66
- }
67
- /* THE FRONT SHEET is the last .card, not the last child: a --closed stack
68
- may carry a .sheet-stack__base after its sheets (below). Written once here
69
- as "a card no card follows" so every rule about the front sheet agrees. */
70
- .sheet-stack > .card:not(:has(~ .card)) {
71
- align-self: stretch; /* the front card fills the remaining stack height */
72
59
  }
73
60
  .sheet-stack:has(> .card:nth-child(2)) { --_n: 2; }
74
61
  .sheet-stack:has(> .card:nth-child(3)) { --_n: 3; }
@@ -79,24 +66,17 @@
79
66
  script at all. --slot is that place in steps; the tap animation reads it
80
67
  to know how far up to travel. Five is the practical peek depth. */
81
68
  .sheet-stack > .card:nth-child(1) { --slot: 0; --level: 1; }
82
- .sheet-stack > .card:nth-child(2) { --slot: 1; --level: 2; margin-top: var(--stack-step); }
83
- .sheet-stack > .card:nth-child(3) { --slot: 2; --level: 3; margin-top: calc(2 * var(--stack-step)); }
84
- .sheet-stack > .card:nth-child(4) { --slot: 3; --level: 4; margin-top: calc(3 * var(--stack-step)); }
85
- .sheet-stack > .card:nth-child(5) { --slot: 4; --level: 5; margin-top: calc(4 * var(--stack-step)); }
69
+ .sheet-stack > .card:nth-child(2) { --slot: 1; --level: 2; }
70
+ .sheet-stack > .card:nth-child(3) { --slot: 2; --level: 3; }
71
+ .sheet-stack > .card:nth-child(4) { --slot: 3; --level: 4; }
72
+ .sheet-stack > .card:nth-child(5) { --slot: 4; --level: 5; }
86
73
 
87
- /* stack headers: uniform — [back?] title left, icons right, one height,
88
- same gap to content on every card */
89
- .sheet-stack > .card > .card__head {
90
- align-items: center;
91
- justify-content: flex-start;
92
- min-height: var(--tap);
93
- }
94
74
  /* NOT a skeleton head: a peek is one line at its own size, and the display
95
75
  stack deliberately runs a second line under the next card. */
96
76
  .sheet-stack > .card > .card__head :is(h1, h2, h3) {
97
77
  font-size: var(--fs-h1);
98
- line-height: 1;
99
- letter-spacing: -0.01em;
78
+ line-height: var(--lh-solid);
79
+ letter-spacing: var(--track-h2);
100
80
  margin-right: auto; /* pins the icon cluster right */
101
81
  display: block;
102
82
  overflow-wrap: normal;
@@ -104,8 +84,7 @@
104
84
 
105
85
  /* display stack: the giant title IS the peek — line one shows, line two is
106
86
  clipped by the next card (recents / library lists) */
107
- .sheet-stack--display { --stack-step: 6.5rem; }
108
- .sheet-stack--display > .card > .card__head { align-items: start; min-height: 0; }
87
+ .sheet-stack--display { --stack-step: calc(var(--tap-base) * 13 / 6); }
109
88
  .sheet-stack--display > .card > .card__head :is(h1, h2, h3) {
110
89
  font: var(--type-display);
111
90
  letter-spacing: var(--track-display);
@@ -114,7 +93,7 @@
114
93
  margin-top: calc((var(--stack-step) - 1lh) / 2 - var(--sp-5));
115
94
  }
116
95
  /* fanned stack: a toggle opens the stack wider so every peek shows more */
117
- .sheet-stack--fanned { --stack-step: 9.5rem; }
96
+ .sheet-stack--fanned { --stack-step: calc(var(--tap-base) * 19 / 6); }
118
97
 
119
98
  /* a peeked card in a plain stack hides its own body. A --tap stack does
120
99
  NOT: there, every card is full height and full content always, and a
@@ -122,76 +101,18 @@
122
101
  card in a real deck is never a shorter card, just a mostly hidden one. */
123
102
  .sheet-stack:not(.sheet-stack--tap) > .card:has(~ .card):not(.is-open) > :not(.card__head) { display: none; }
124
103
 
125
- /* ============================================================
126
- .sheet-stack--tap — tap a peeked sheet open; a back button returns it.
127
-
128
- A sheet (a .card — the material, sheet.css's single sheet is the same
129
- stuff) is rigid: it can move in, move out, or be covered — it can
130
- never change shape. So every sheet here is the SAME full height,
131
- always, set once and never touched again by anything. A "peek" is
132
- not a short sheet; it's a full sheet the next one happens to cover,
133
- showing only the strip above it, exactly like a sheet of paper part
134
- way out of a real stack.
135
-
136
- Because of that, the whole animation is one property: translate.
137
- Nothing resizes, nothing re-stacks — z-index is the plain resting
138
- --level order and is never touched, since the order already says
139
- what has to happen. Cards after the tapped one sit IN FRONT of it,
140
- so they're what's covering it, so they're what must leave. Cards
141
- before it are already BEHIND it, so they need do nothing at all
142
- except let themselves be covered as it rises over them.
143
-
144
- .is-open — the tapped card, moved up to the top of the stack. Its
145
- own resting offset is margin-top (below); opening just
146
- translates that offset back off, landing it flush at
147
- the stack's top with its full height exactly filling
148
- to the bottom.
149
- .is-before — an earlier card, nudged one step down so it slips
150
- under the next card's resting strip and is covered
151
- there. Each carries its own --stagger (set in JS: 0
152
- for the one nearest the tapped card, counting up
153
- further back) so they tuck away, and later un-tuck,
154
- one at a time.
155
- .is-after — a later card, moved out below, together, no stagger.
156
- ============================================================ */
104
+ /* ---- .sheet-stack--tap — tap a peeked sheet open; a back button returns it. How its sheets move (one property: translate) is
105
+ told over their rules, where they sit, in card.css. ---- */
157
106
  .sheet-stack--tap > .card {
158
- height: 100%; /* every card identical and full length, always — the one
159
- size in this whole component, never toggled, never transitioned. At a
160
- resting margin-top of n steps it simply overhangs the stack's bottom
161
- by n steps (clipped by .screen--fill), so translating it up by exactly
162
- that much lands it filling the stack precisely. */
163
- overflow: clip;
164
- transition: translate var(--motion-sheet) ease; /* the only animated property here */
165
- transition-delay: calc(var(--stagger, 0) * 70ms); /* 0 for everything
166
- except a staggered is-before card: the card renders --stagger as it tucks or un-tucks */
167
107
  --_back-size: var(--stack-back-size, calc(var(--tap-base) * 2 / 3)); /* read once, shared by the back button and the head's padding below */
168
108
  }
169
- .sheet-stack--tap > .card.is-open {
170
- /* up by its own resting offset — pure movement, nothing resizes */
171
- translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step) + var(--_pull, 0px));
172
- cursor: default;
173
- overflow-y: auto;
174
- overflow-x: clip;
175
- overscroll-behavior: contain;
176
- }
177
- .sheet-stack--tap > .card.is-open.is-dragging { transition: none; }
178
- .sheet-stack--tap > .card > .card__head { touch-action: none; }
179
- /* a pull in the content, already at the top: the words move down, the card stays. --_refresh is the finger, private.
180
- A paper body adds its own --sp-6 on top of this (the rule below): the stack's gap under the head is the
181
- card's colour, so without the white inset the first row sits on that seam. */
182
- .sheet-stack > .card > .sheet__body { padding-top: var(--_refresh, 0px); transition: padding-top var(--motion-sheet) ease; }
183
- .sheet-stack--tap > .card.is-open:not(:has(> .sheet__body)) {
184
- padding-top: calc(var(--card-pad) + var(--_refresh, 0px));
185
- transition: translate var(--motion-sheet) ease, padding-top var(--motion-sheet) ease;
186
- }
187
- .sheet-stack > .card > .sheet__body.is-refreshing,
188
- .sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
109
+
189
110
  /* the GRIP (Lh 2026-10-05): four small dots at the middle of every sheet's top edge — a sign that this card
190
111
  slides up and down. A mark, not a control: nothing to press, nothing added to the sheet (the card's own background, in the
191
112
  room above its head). The card renders `.stack-card` when it is a sheet of a tap stack, so the dots are its own.
192
113
  At rest they are the card's own ink, faint. Only while THAT sheet reloads (`.is-loading`, from its `refreshing` prop,
193
- and only while it is open) they dissolve white → black, so a light card and a dark one each get a moment the dots stand out.
194
- Nothing travels. --_grip is the mix, 0 white → 1 black, private. */
114
+ and only while it is open) they dissolve from the ondark ink to the onlight ink, so a light card and a dark one each get a moment the dots stand out.
115
+ Nothing travels. --_grip is the mix, 0 the ondark ink → 1 the onlight ink, private. */
195
116
  @property --_grip { syntax: "<number>"; inherits: false; initial-value: 0; }
196
117
  .stack-card {
197
118
  --_grip-dot: var(--sp-1);
@@ -207,8 +128,8 @@
207
128
  calc(50% + 0.5 * var(--_grip-step)) var(--_grip-y), calc(50% + 1.5 * var(--_grip-step)) var(--_grip-y);
208
129
  }
209
130
  .stack-card.is-open.is-loading {
210
- --_grip-ink: color-mix(in srgb, black calc(var(--_grip) * 100%), white);
211
- animation: stack-grip-dissolve 0.4s ease-in-out infinite alternate;
131
+ --_grip-ink: color-mix(in srgb, var(--ink-onlight) calc(var(--_grip) * 100%), var(--ink-ondark));
132
+ animation: stack-grip-dissolve var(--motion-pulse) var(--ease-in-out) infinite alternate;
212
133
  }
213
134
  @keyframes stack-grip-dissolve { from { --_grip: 0; } to { --_grip: 1; } }
214
135
  /* the grip as ONE LINE (`mark="line"`, Lh 2026-10-06): the single Sheet's line, at the same place and the same length. It reads the
@@ -228,22 +149,6 @@
228
149
  @media (prefers-reduced-motion: reduce) {
229
150
  .stack-card.is-open.is-loading { animation: none; --_grip-ink: currentColor; }
230
151
  }
231
- /* the front sheet — the one open on arrival — never travels up (it is not
232
- tappable), so its resting offset would leave its last n steps below the
233
- screen forever, unreachable by any scroll (timebank home, 2026-09-13). It
234
- alone is cut to the room it actually has, and it scrolls like an opened
235
- sheet does. Every other sheet keeps the one full height. */
236
- .sheet-stack--tap:not(.sheet-stack--closed) > .card:not(:has(~ .card)) {
237
- height: calc(100% - var(--slot, 0) * var(--stack-step));
238
- overflow-y: auto;
239
- overflow-x: clip;
240
- overscroll-behavior: contain;
241
- }
242
- /* a scrolling sheet shows no scrollbar (a desktop with a mouse would paint one
243
- down the card's edge — Lh 2026-09-15); it still scrolls, like the page itself
244
- (the hiding rule is base.css's, shared with every other scrolling track) */
245
- .sheet-stack--tap > .card.is-before { translate: 0 var(--stack-step); }
246
- .sheet-stack--tap > .card.is-after { translate: 0 100vh; }
247
152
 
248
153
  /* ============================================================
249
154
  .sheet-stack--closed — nothing open on arrival. Every sheet, the front
@@ -260,14 +165,6 @@
260
165
  lands flush at the top. is-before tucks one step below its resting place.
261
166
  ============================================================ */
262
167
  .sheet-stack--closed { --_headroom: calc(100% - var(--_n) * var(--stack-step)); }
263
- .sheet-stack--closed > .card { translate: 0 calc(var(--_headroom) - var(--_lift, 0px)); }
264
- .sheet-stack--closed > .card.is-dragging { transition: none; }
265
- .sheet-stack--tap:not(.sheet-stack--closed) > .card.is-dragging:not(.is-open) {
266
- translate: 0 calc(-1 * var(--_lift, 0px));
267
- transition: none;
268
- }
269
- .sheet-stack--closed > .card.is-before { translate: 0 calc(var(--_headroom) + var(--stack-step)); }
270
- .sheet-stack--closed > .card.is-after { translate: 0 100vh; }
271
168
 
272
169
  /* .sheet-stack__base — the base's content, in the headroom of a closed stack:
273
170
  behind every sheet (level 0), the stack's inset put back (the stack bleeds),
@@ -277,9 +174,9 @@
277
174
  .sheet-stack > .sheet-stack__base {
278
175
  grid-area: 1 / 1;
279
176
  align-self: start;
280
- z-index: 0;
281
- --_under: 0px;
282
- height: calc(var(--_headroom, 0px) + var(--_under));
177
+ z-index: var(--z-flat);
178
+ --_under: 0rem;
179
+ height: calc(var(--_headroom, 0rem) + var(--_under));
283
180
  padding-inline: var(--screen-pad);
284
181
  --_gap: var(--card-gap, var(--sp-5));
285
182
  display: grid;
@@ -296,7 +193,6 @@
296
193
  /* a base that is ONE Centre is one thing handed over (a note, an empty state): it stands in the MIDDLE of the room above the peeks
297
194
  (Lh 2026-10-06). The base already keeps the screen's inset, so the centre block gives its own back. */
298
195
  .sheet-stack__base:has(> .screen__centre:only-child) { align-content: center; }
299
- .sheet-stack__base > .screen__centre { padding-inline: 0; }
300
196
  /* loose things rest against the peeks, so the room under the last one is the room between two of them (Lh 2026-10-09: a card's
301
197
  bottom edge lay on the first sheet's top edge). A lone Page reads from the top and a lone Centre stands in the middle: neither rests there. */
302
198
  .sheet-stack__base:not(:has(> .page:only-child, > .screen__centre:only-child)) {
@@ -319,9 +215,6 @@
319
215
  overscroll-behavior: contain;
320
216
  touch-action: pan-y;
321
217
  }
322
- /* a card standing in this room is FLAT (Lh 2026-10-09): the room clips at its own edges, and a card's shadow cut there drew a straight
323
- line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
324
- :is([data-palette], [data-finish]) .sheet-stack__base .card { box-shadow: none; }
325
218
 
326
219
  /* ============================================================
327
220
  .sheet-stack--flood — a closed stack whose open sheet FLOODS the screen: it rises over the top bar and fills the whole screen
@@ -343,38 +236,17 @@
343
236
  ============================================================ */
344
237
  .screen--fill:has(> .sheet-stack--flood) { --_bar-gap: var(--sp-2); }
345
238
  .screen--fill:has(> .sheet-stack--flood):has(> .topbar:first-child) { --_bar-gap: var(--screen-gap); }
346
- .screen--fill:has(> .sheet-stack--flood) > :is(.appbar, .topbar):first-child {
347
- position: absolute;
348
- inset: var(--safe-top) 0 auto;
349
- max-width: none;
350
- margin: 0;
351
- z-index: 0; /* under every sheet (--level 1-5); the base clears it by margin */
352
- }
353
239
  .sheet-stack--flood { --_bar: calc(var(--top-h, var(--header-h)) + var(--_bar-gap)); }
354
240
  .sheet-stack--flood > .sheet-stack__base {
355
241
  margin-top: var(--_bar);
356
- height: calc(var(--_headroom, 0px) - var(--_bar) + var(--_under));
242
+ height: calc(var(--_headroom, 0rem) - var(--_bar) + var(--_under));
357
243
  }
358
- .screen--fill:has(> .sheet-stack--flood > .stack-card.is-open, > .sheet-stack--flood > :not(.stack-card) > .stack-card.is-open) > :is(.appbar, .topbar):first-child {
359
- visibility: hidden;
360
- transition: visibility 0s linear var(--motion-sheet); /* gone once the sheet has covered it; back at once on a fold (the rule above has no delay) */
361
- }
362
- /* at the screen's top the open sheet's top corners are SQUARE (Lh 2026-10-08): round, they left two small pockets of the
363
- bar's ground at the glass. They square as the sheet rises and round again as it folds, in the slide's own time. Paint
364
- only, the sheet's size is untouched. A pull on the head keeps them square until the finger lets go. */
365
- .sheet-stack--flood > .card { transition: translate var(--motion-sheet) ease, border-radius var(--motion-sheet) ease; }
366
- .sheet-stack--flood > .card.is-open { border-top-left-radius: 0; border-top-right-radius: 0; }
367
244
 
368
245
  /* back button: lives in a slot the header reserves once open, and DISSOLVES
369
246
  into it — an opacity fade in place, never a slide in from the left (the
370
247
  slide read as a second thing moving while the sheet itself moved; Lh,
371
248
  2026-09-11). Only the sheet travels; the button appears where it is. */
372
249
  /* a sheet's OWN head and back: the card is the stack's child or, with one host wrapper, the wrapper's. A card standing in the sheet's body wears its own head */
373
- :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head {
374
- position: relative;
375
- padding-left: 0;
376
- transition: padding-left var(--motion-sheet) ease;
377
- }
378
250
  :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head > .sheet-stack__back {
379
251
  position: absolute;
380
252
  left: 0;
@@ -382,7 +254,7 @@
382
254
  translate: 0 -50%;
383
255
  opacity: 0;
384
256
  visibility: hidden;
385
- transition: opacity var(--motion-sheet) ease, visibility var(--motion-sheet);
257
+ transition: opacity var(--motion-sheet) var(--ease), visibility var(--motion-sheet);
386
258
  /* SMALL (see "the back button, SMALL" below) */
387
259
  width: var(--_back-size);
388
260
  height: var(--_back-size);
@@ -395,63 +267,16 @@
395
267
  visibility: visible;
396
268
  }
397
269
 
398
- /* ---- a stacked sheet with a SheetBody has the ONE sheet shape (Lh 2026-09-23, the home layout): sheet head
399
- pinned at the top · sheet body scrolls, as long as it needs · sheet foot (a CardFoot, the CTA) at the bottom.
400
- The card becomes a flex column and stops scrolling itself — only the body does. A sheet with no SheetBody
401
- keeps the old way: the whole open card scrolls. ---- */
402
- .sheet-stack--tap > .card:has(> .sheet__body) { display: flex; flex-direction: column; gap: var(--card-gap); }
403
- .sheet-stack--tap > .card:has(> .sheet__body).is-open { overflow: clip; }
404
270
  .sheet-stack > .card:has(> .sheet__body) > :not(.sheet__body) { flex-shrink: 0; }
405
271
  .sheet-stack > .card > .sheet__body { --_cover: 0rem; } /* nothing slid under here: the stack's sheet is always whole */
406
- /* a closed paper sheet's body stays clear (Lh 2026-10-06). The next sheet's round top does not
407
- cover the corners, and a white body showed there beside the head. Clear, the card's own colour
408
- shows in that pocket. Open, the body is white, and the first row sits --sp-6 down, the same white
409
- inset a single paper sheet keeps: the flex gap under the head is the card's colour, so the row
410
- would otherwise sit on that seam. A pull adds --_refresh on top of the inset. On the way back
411
- into the stack the card colour fades in quickly (--motion-quick) and that fade ends with the
412
- sheet's own slide (--motion-sheet, plus the same stagger), so the corner is the head colour
413
- at the moment the sheet stops. Rising, the body is white at once. */
414
- /* the head's band is as slim as it can be (Lh 2026-10-06: 20 over · 48 head · 20 under read as a heavy bar): the white starts
415
- --sp-3 under the head, not a whole --card-gap (--sp-2 at first; Lh added 4 the same day). The body is pulled up by the difference — the card's gap is left alone, the
416
- stack's step and the body's own rows read it. */
417
- .sheet-stack > .card.sheet--paper > .sheet__body {
418
- margin-top: calc(var(--sp-3) - var(--card-gap));
419
- padding-top: calc(var(--sp-6) + var(--_refresh, 0px));
420
- transition: padding-top var(--motion-sheet) ease, background-color 0s linear;
421
- }
422
- /* a pull follows the finger at once, on paper too: the rule above is as strong as the stack's own `is-refreshing` one and comes later */
423
- .sheet-stack > .card.sheet--paper > .sheet__body.is-refreshing { transition: none; }
424
- .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
425
- background-color: transparent;
426
- transition:
427
- padding-top var(--motion-sheet) ease,
428
- background-color var(--motion-quick) ease calc(var(--stagger, 0) * 70ms + var(--motion-sheet) - var(--motion-quick));
429
- }
430
- @media (prefers-reduced-motion: reduce) {
431
- .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
432
- transition: padding-top var(--motion-sheet) ease, background-color 0s linear;
433
- }
434
- }
435
- /* a paper stack's foot is white to the card's edges, and it covers the tinted flex gap above it so the
436
- button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
437
- where it was — the padding gives back what the bleed takes. */
438
- .sheet-stack > .card.sheet--paper > .card__foot {
439
- background: var(--color-paper, #fff);
440
- color: var(--color-ink, #141414);
441
- margin-top: calc(-1 * var(--card-gap));
442
- margin-inline: calc(-1 * var(--card-pad));
443
- margin-bottom: calc(-1 * var(--card-pad));
444
- padding-top: var(--card-gap);
445
- padding-inline: var(--card-pad);
446
- padding-bottom: var(--card-pad);
447
- }
272
+ /* a paper stack's foot is paper (card.css paints it): its ink reads the action's side */
273
+ .sheet-stack > .card.sheet--paper > .card__foot { --dark: var(--action-dark); }
448
274
 
449
275
  /* ---- StackSheet's tabs (Lh 2026-09-23): the head's end is a row of small icon buttons, the sheet's pages — the chosen one
450
276
  filled (.icon-btn--invert) only while the sheet is open; a count is a small circle over the icon's top right corner ---- */
451
277
  .stack-sheet__tabs { display: flex; gap: var(--stack-sheet-tabs-gap, var(--sp-2)); flex: none; } /* room between the tabs: with none, the chosen tab's disc touched its neighbour's (Lh 2026-09-24) */
452
278
  /* the head is ONE line, always (Lh 2026-09-24): the tabs keep their size, the TITLE gives — it never wraps, it runs sideways
453
279
  (StackSheet measures --_over, the width it overflows by, and sets .is-long), one way: it waits, runs to the end, waits, and starts again from the first letter — like a song title */
454
- .stack-sheet > .card__head { flex-wrap: nowrap; }
455
280
  .stack-sheet__title {
456
281
  flex: 1; min-width: 0; overflow-x: clip; overflow-y: visible; white-space: nowrap; overflow-wrap: normal; /* clip SIDEWAYS only:
457
282
  hidden clipped the Thai tone marks above a line-height-1 line (เพื่อน lost its ไม้เอก — Lh 2026-09-24) */
@@ -460,7 +285,7 @@
460
285
  }
461
286
  .stack-sheet__run { display: inline-block; }
462
287
  .stack-sheet__title.is-long .stack-sheet__run {
463
- animation: stack-sheet-run var(--stack-sheet-run-time, var(--_run, 6s)) ease-in-out infinite; /* ONE way: start → end, then back to the start at once (Lh 2026-09-24) */ /* --_run: from the overflow, 30px a second + the pauses */
288
+ animation: stack-sheet-run var(--stack-sheet-run-time, var(--_run, var(--motion-run))) var(--ease-in-out) infinite; /* ONE way: start → end, then back to the start at once (Lh 2026-09-24) */ /* --_run: from the overflow, 30px a second + the pauses */
464
289
  }
465
290
  @keyframes stack-sheet-run { 0%, 20% { translate: 0; } 85%, 100% { translate: calc(-1 * var(--_over)); } }
466
291
  @media (prefers-reduced-motion: reduce) {
@@ -473,11 +298,11 @@
473
298
  /* the disc fades with the sheet: in as it rises, out as it folds, and across when another tab is pressed.
474
299
  scale stays on the press (actions.css) so the give under a finger stays quick */
475
300
  transition:
476
- scale var(--motion-press) ease-out,
477
- background-color var(--motion-sheet) ease,
478
- color var(--motion-sheet) ease,
479
- border-color var(--motion-sheet) ease,
480
- box-shadow var(--motion-sheet) ease;
301
+ scale var(--motion-press) var(--ease-out),
302
+ background-color var(--motion-sheet) var(--ease),
303
+ color var(--motion-sheet) var(--ease),
304
+ border-color var(--motion-sheet) var(--ease),
305
+ box-shadow var(--motion-sheet) var(--ease);
481
306
  }
482
307
  .stack-sheet__tab > .stack-sheet__count { /* outranks .badge (numbers.css loads after this file) */
483
308
  position: absolute;
@@ -490,7 +315,7 @@
490
315
  padding: 0 var(--stack-sheet-count-pad-x, 0.25em) var(--stack-sheet-count-lift, 0.1em); /* "99+" keeps its room at every text size;
491
316
  the room UNDER the number lifts it onto the circle's middle: a digit has no descender, so a centred line box sets it low (Lh 2026-10-01) */
492
317
  font: var(--type-caption);
493
- line-height: 1;
318
+ line-height: var(--lh-solid);
494
319
  pointer-events: none;
495
320
  }
496
321
  /* a tab not chosen is the icon alone — no ring, no ground (Lh 2026-09-23: "the circle around is not pretty"); the chosen one turns
@@ -506,22 +331,21 @@
506
331
  margin around it reaches into the card's padding — and an open head (back · title · three tabs) keeps one line (Lh 2026-09-23) */
507
332
  .card__head > .stack-sheet__tabs { margin-right: calc(-1 * var(--sp-2)); }
508
333
 
509
- /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
510
- tappable"): a SMALL DISC — its ring kept, 32px instead of 44, the chevron in it at 20 — so the head gives it 32px + a gap
511
- (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
512
- .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back:not(.sheet-stack__back--end)) { padding-left: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
513
334
  /* AT THE END (Lh 2026-09-29, the home): after the tabs, chevron down — the open sheet folds down. The head's end gives it its room
514
335
  while open; the last tab then sits on the inset (no reach into the padding) */
515
336
  .sheet-stack--tap .sheet-stack__back--end { left: auto; right: 0; }
516
- :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head:has(> .sheet-stack__back--end) { transition: padding-right var(--motion-sheet) ease; }
517
- .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) { padding-right: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
518
337
  .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) > .stack-sheet__tabs { margin-right: 0; }
519
- .sheet-stack--tap .sheet-stack__back > .icon { width: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); height: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); flex: none; } /* 26: the chevron fills the small disc (Lh 2026-09-23) */
338
+ .sheet-stack--tap .sheet-stack__back > .icon { --icon-size: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); } /* 26: the chevron fills the small disc (Lh 2026-09-23) */
520
339
  .sheet-stack--tap .sheet-stack__back::after { content: ""; position: absolute; inset: calc((var(--_back-size) - var(--tap-sm)) / 2); }
521
340
 
522
341
  /* the open sheet's title folds it (SheetStack.tsx): it says so to a pointer */
523
342
  .sheet-stack--tap > .card.is-open > .card__head > :is(h1, h2, h3) { cursor: pointer; }
524
- /* a stacked sheet is a control until it is open — tapped, never read into: no text selection (a drag, a double tap, a long press),
525
- no grey flash, no iOS callout; its head stays one when open. Only the OPEN sheet's body is text to select (Lh 2026-09-23) */
526
- .sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
527
343
  .sheet-stack--tap > .card.is-open > :not(.card__head) { -webkit-user-select: var(--screen-select, none); user-select: var(--screen-select, none); } /* the open sheet reads as the rest of the screen: not selectable (Lh 2026-09-25; it was text) */
344
+
345
+ /* ---- where it sits in what base.css draws (docs/reference.md rule 15) ---- */
346
+ /* min-height: 0 — a flex item's floor is its content, and the stack's content
347
+ is its own full-height sheets, so without it the stack grew to the whole
348
+ screen and its open sheet ran past the bottom edge by the bars' height,
349
+ unreachable by scrolling (timebank home, 2026-09-13). The stack IS the room
350
+ left under the bars; every sheet fills exactly that. */
351
+ .screen--fill > .sheet-stack { flex: 1; min-height: 0; }
package/css/stamp.css CHANGED
@@ -33,42 +33,44 @@
33
33
  position: relative;
34
34
  display: block;
35
35
  container-type: inline-size;
36
- color: var(--stamp-ink, #141414);
37
- filter: drop-shadow(var(--stamp-drop, 0 var(--sp-1) var(--sp-2) rgb(0 0 0 / 0.16))); /* drop-shadow follows the teeth; a box-shadow would draw the rectangle */
36
+ color: var(--stamp-ink, var(--ink-onlight));
37
+ filter: drop-shadow(var(--stamp-drop, var(--shadow-paper))); /* drop-shadow follows the teeth; a box-shadow would draw the rectangle */
38
+ rotate: var(--stamp-turn, none);
39
+ animation: var(--stamp-arrive, none);
38
40
  }
39
41
  .stamp__paper {
40
42
  --_pitch: calc(100cqw / 12); /* one tooth: 12 across, 15 down — the holes land whole on every edge. Fewer and larger than a real stamp's: a small stamp's teeth must still read (Lh 2026-10-07; it was 20 × 25) */
41
43
  --_hole: 2.6cqw;
42
44
  position: absolute;
43
45
  inset: 0;
44
- background-color: var(--stamp-paper-bg, #fffdf8);
46
+ background-color: var(--stamp-paper-bg, var(--stamp-paper));
45
47
  -webkit-mask: linear-gradient(#000 0 0) center / calc(100% - var(--_pitch)) calc(100% - var(--_pitch)) no-repeat,
46
48
  radial-gradient(circle, transparent var(--_hole), #000 calc(var(--_hole) + 0.4cqw)) calc(var(--_pitch) / -2) calc(var(--_pitch) / -2) / var(--_pitch) var(--_pitch);
47
49
  mask: linear-gradient(#000 0 0) center / calc(100% - var(--_pitch)) calc(100% - var(--_pitch)) no-repeat,
48
50
  radial-gradient(circle, transparent var(--_hole), #000 calc(var(--_hole) + 0.4cqw)) calc(var(--_pitch) / -2) calc(var(--_pitch) / -2) / var(--_pitch) var(--_pitch);
49
51
  }
50
- .stamp__pic { position: absolute; inset: var(--stamp-frame, 8cqw); overflow: hidden; } /* the white frame round the picture; the ground is the tone class's */
52
+ .stamp__pic { position: absolute; inset: var(--stamp-frame, 8cqw); overflow: hidden; } /* the paper's frame round the picture; the ground is the tone class's */
51
53
 
52
54
  /* the pictures, placed by COUNT: where, how wide, how far turned — the layout is the stamp's look, never the app's.
53
55
  A share of the picture area; the foot stays clear for the words. */
54
56
  .stamp__art { position: absolute; left: var(--_x); top: var(--_y); width: var(--_w); aspect-ratio: 1; translate: -50% -50%; rotate: var(--_r); }
55
57
  .stamp__art > * { display: block; width: 100%; height: 100%; }
56
58
  .stamp--1 .stamp__art { --_x: 50%; --_y: 40%; --_w: 72%; --_r: 0deg; }
57
- .stamp--2 .stamp__art:nth-child(1) { --_x: 37%; --_y: 27%; --_w: 58%; --_r: -6deg; }
58
- .stamp--2 .stamp__art:nth-child(2) { --_x: 63%; --_y: 56%; --_w: 54%; --_r: 8deg; }
59
- .stamp--3 .stamp__art:nth-child(1) { --_x: 30%; --_y: 21%; --_w: 50%; --_r: -8deg; }
60
- .stamp--3 .stamp__art:nth-child(2) { --_x: 71%; --_y: 31%; --_w: 46%; --_r: 7deg; }
61
- .stamp--3 .stamp__art:nth-child(3) { --_x: 47%; --_y: 58%; --_w: 54%; --_r: -2deg; }
62
- .stamp--4 .stamp__art:nth-child(1) { --_x: 29%; --_y: 19%; --_w: 44%; --_r: -8deg; }
63
- .stamp--4 .stamp__art:nth-child(2) { --_x: 71%; --_y: 24%; --_w: 42%; --_r: 7deg; }
64
- .stamp--4 .stamp__art:nth-child(3) { --_x: 31%; --_y: 57%; --_w: 42%; --_r: 5deg; }
65
- .stamp--4 .stamp__art:nth-child(4) { --_x: 70%; --_y: 60%; --_w: 44%; --_r: -6deg; }
59
+ .stamp--2 .stamp__art:nth-child(1) { --_x: 37%; --_y: 27%; --_w: 58%; --_r: calc(var(--tilt-base) * -6); }
60
+ .stamp--2 .stamp__art:nth-child(2) { --_x: 63%; --_y: 56%; --_w: 54%; --_r: calc(var(--tilt-base) * 8); }
61
+ .stamp--3 .stamp__art:nth-child(1) { --_x: 30%; --_y: 21%; --_w: 50%; --_r: calc(var(--tilt-base) * -8); }
62
+ .stamp--3 .stamp__art:nth-child(2) { --_x: 71%; --_y: 31%; --_w: 46%; --_r: calc(var(--tilt-base) * 7); }
63
+ .stamp--3 .stamp__art:nth-child(3) { --_x: 47%; --_y: 58%; --_w: 54%; --_r: calc(var(--tilt-base) * -2); }
64
+ .stamp--4 .stamp__art:nth-child(1) { --_x: 29%; --_y: 19%; --_w: 44%; --_r: calc(var(--tilt-base) * -8); }
65
+ .stamp--4 .stamp__art:nth-child(2) { --_x: 71%; --_y: 24%; --_w: 42%; --_r: calc(var(--tilt-base) * 7); }
66
+ .stamp--4 .stamp__art:nth-child(3) { --_x: 31%; --_y: 57%; --_w: 42%; --_r: calc(var(--tilt-base) * 5); }
67
+ .stamp--4 .stamp__art:nth-child(4) { --_x: 70%; --_y: 60%; --_w: 44%; --_r: calc(var(--tilt-base) * -6); }
66
68
 
67
69
  /* the words on the picture's foot: a small line, then the big name — one line each (clipped sideways only: Thai tone marks stand above the line box) */
68
70
  .stamp__cap { position: absolute; inset-inline: 5cqw; bottom: 3.5cqw; display: grid; gap: 0.4cqw; }
69
71
  .stamp__words, .stamp__name { display: block; min-width: 0; white-space: nowrap; overflow-x: clip; text-overflow: ellipsis; }
70
- .stamp__words { --_words: 4.8cqw; font: 500 var(--_words) / 1.3 var(--font-ui); font-size: var(--stamp-words-font-size, var(--_words)); }
71
- .stamp__name { --_name: 9.5cqw; font: 900 var(--_name) / 1.15 var(--font-display); font-size: var(--stamp-name-font-size, var(--_name)); }
72
+ .stamp__words { --_words: 4.8cqw; font: var(--fw-medium) var(--_words) / var(--lh-title) var(--font-ui); font-size: var(--stamp-words-font-size, var(--_words)); }
73
+ .stamp__name { --_name: 9.5cqw; font: var(--fw-black) var(--_name) / var(--lh-display) var(--font-display); font-size: var(--stamp-name-font-size, var(--_name)); }
72
74
  /* at the stamp's own size and under (and stuck on a note): the small line goes, the name grows */
73
75
  /* --bare: the pictures alone, no words — a stamp stuck on something (a note, a postcard). The words belong to the album.
74
76
  The pictures take the foot's room: each sits a little lower, so the group stands in the middle of the frame. */
@@ -82,14 +84,13 @@
82
84
 
83
85
  /* --tilt: turned a little on its middle — the one stamp a start screen hands over */
84
86
  .stamp--tilt-left, .stamp--tilt-right { margin-block: var(--sp-4); } /* the turned corners and the shadow need room: a centre scrolls and would cut them */
85
- .stamp--tilt-left { rotate: calc(-1 * var(--stamp-tilt, 5deg)); }
86
- .stamp--tilt-right { rotate: var(--stamp-tilt, 5deg); }
87
+ .stamp--tilt-left { rotate: calc(-1 * var(--stamp-tilt, calc(var(--tilt-base) * 5))); }
88
+ .stamp--tilt-right { rotate: var(--stamp-tilt, calc(var(--tilt-base) * 5)); }
87
89
 
88
90
  /* --tap: the whole stamp is the button (`onClick`); the press and the ring are the PRESS list's (actions.css) */
89
91
  .stamp--tap { appearance: none; border: 0; padding: 0; background: none; font: inherit; text-align: start; -webkit-tap-highlight-color: transparent; }
90
92
 
91
93
  /* a stamp — or the empty place for one — handed over in a Centre: its greeting centred under it. The stamp is its own size here as everywhere */
92
- .screen__centre:has(> :is(.stamp, .stamp-place)) { justify-items: center; text-align: center; }
93
94
 
94
95
  /* ---- stamp place: an empty place for a stamp — an album's gap, "make yours" ----
95
96
  <span class="stamp-place"><svg class="stamp-place__edge" aria-hidden="true"><rect/></svg>สร้าง</span> · <button class="stamp-place">…</button> · <span class="stamp-place stamp-place--faint">…</span>
@@ -183,7 +184,7 @@
183
184
  background-color: var(--stamp-cell-on-bg, var(--accent));
184
185
  color: var(--stamp-cell-on-ink, var(--on-accent));
185
186
  }
186
- .stamp-cell__on > .icon { width: 70%; height: 70%; }
187
+ .stamp-cell__on > .icon { --icon-size: 70%; }
187
188
  /* --bare: no ground, no padding, no round — the stamps lie on what holds the book, three across as on the ground. The name is the Stamp's own; free places still draw.
188
189
  <div class="stamp-book stamp-book--bare"> */
189
190
  .stamp-book--bare { padding: 0; border-radius: 0; background: none; }
@@ -227,24 +228,24 @@
227
228
  After the rule above, at the same weight: `main` is the one place a stuck stamp reads --stamp-size. */
228
229
  .stamp-on--main > .stamp { width: var(--stamp-size, calc(var(--tap) * 2)); }
229
230
  .stamp-on--move { will-change: transform; } /* its own layer: Safari left the stamp's shadow behind as a trail along the drag when it was painted with the note (seen on the simulator, 2026-10-07) */
230
- .stamp-on--move { cursor: grab; touch-action: none; -webkit-user-select: none; user-select: none; transition: scale var(--motion-press) ease-out; }
231
+ .stamp-on--move { cursor: grab; touch-action: none; -webkit-user-select: none; user-select: none; transition: scale var(--motion-press) var(--ease-out); }
231
232
  .stamp-on--move:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
232
- .stamp-on.is-held { cursor: grabbing; z-index: 1; scale: 1.06; } /* lifted off the paper while the finger holds it */
233
+ .stamp-on.is-held { cursor: grabbing; z-index: var(--z-over); scale: var(--scale-lift); } /* lifted off the paper while the finger holds it */
233
234
  @media (prefers-reduced-motion: reduce) { .stamp-on.is-held { scale: 1; } }
234
235
 
235
236
  /* ---- the cross that takes a stamp off (`StampOn onRemove`): a small dark disc with an x on the stamp's top right corner ----
236
237
  <div class="stamp-on stamp-on--move">…<button class="stamp-on__remove" aria-label="Remove"><span class="stamp-on__remove-disc"><svg class="icon">x</svg></span></button></div>
237
238
  The button is the touch target — at least --tap-floor (44) square, invisible, centred on the corner — and the disc inside it is what is seen
238
239
  (24). It is NOT turned with the stamp: a hand turns a stamp, a button stays square to the screen, so the cross counter-turns by the stamp's own turn. Like the
239
- stamp it brings its ground and its ink (a dark disc, a pale x and a pale ring: seen on any paper and over any picture). */
240
+ stamp it takes the theme's two inks, the onlight ink as its disc and the ondark ink as its x and ring: seen on any paper and over any picture. */
240
241
  .stamp-on__remove-disc {
241
242
  display: grid;
242
243
  place-items: center;
243
244
  width: var(--stamp-on-remove-size, var(--sp-6));
244
245
  aspect-ratio: 1;
245
246
  border-radius: 50%;
246
- background-color: var(--stamp-on-remove-bg, #141414);
247
- box-shadow: 0 0 0 var(--sp-0) currentColor, 0 var(--sp-0) var(--sp-1) rgb(0 0 0 / 0.35);
247
+ background-color: var(--stamp-on-remove-bg, var(--ink-onlight));
248
+ box-shadow: 0 0 0 var(--sp-0) currentColor, var(--shadow-lift);
248
249
  }
249
250
  .stamp-on__remove {
250
251
  --_hit: max(var(--tap-floor), var(--stamp-on-remove-size, var(--sp-6)));
@@ -261,16 +262,27 @@
261
262
  border: 0;
262
263
  border-radius: 50%;
263
264
  background: none;
264
- color: var(--stamp-on-remove-ink, #ffffff);
265
+ color: var(--stamp-on-remove-ink, var(--ink-ondark));
265
266
  font: inherit;
266
267
  -webkit-tap-highlight-color: transparent;
267
268
  }
268
- .stamp-on__remove-disc > .icon { width: 60%; height: 60%; }
269
- @media (prefers-reduced-motion: no-preference) { .stamp-on__remove { animation: stamp-on-remove-in var(--motion-press) ease-out; } }
270
- @keyframes stamp-on-remove-in { from { opacity: 0; scale: 0.6; } }
269
+ .stamp-on__remove-disc > .icon { --icon-size: 60%; }
270
+ @media (prefers-reduced-motion: no-preference) { .stamp-on__remove { animation: stamp-on-remove-in var(--motion-press) var(--ease-out); } }
271
+ @keyframes stamp-on-remove-in { from { opacity: 0; scale: var(--scale-arrive); } }
271
272
 
272
273
  /* --arrive: the stamp sticks ITSELF (StampOn `arrive`; a Stamp's `arrive` too, where it already stands) — struck onto its place (`strike`: tokens.css).
273
274
  The component reads the animation's name to tell the app it has landed. `scale` and `opacity` leave a --tilt's `rotate` alone. Knob: --stamp-on-arrive-time */
274
275
  @media (prefers-reduced-motion: no-preference) {
275
- .stamp-on--arrive, .stamp--arrive { animation: strike var(--stamp-on-arrive-time, var(--motion-sheet)) ease-in both; }
276
+ .stamp-on--arrive, .stamp--arrive { animation: strike var(--stamp-on-arrive-time, var(--motion-sheet)) var(--ease-in) both; }
276
277
  }
278
+
279
+ /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
280
+ .card__head > .stamp { grid-column: 2; grid-row: 1 / span 2; margin-inline: 0; }
281
+ /* ---- where it sits in what people.css draws (docs/reference.md rule 15) ---- */
282
+ /* a stamp in the stack is the stamp's small size (--stamp-sm-size: one rule in stamp.css, 48 × 60), lapped like the heads. Measured 2026-10-11 in a Row's
283
+ lead with a title, a meta line and an end button: at 375 the meta line breaks in two, accepted by Lh (a lap of 16 would hold it; the lap stays 8)
284
+ <span class="avatar-stack"><span class="stamp stamp--bare">…</span><span class="stamp stamp--bare">…</span><span class="avatar-stack__more">+9</span></span> */
285
+ .avatar-stack > .stamp { flex: none; margin-inline: 0; }
286
+ .avatar-stack > .stamp + .stamp { margin-left: calc(-1 * var(--sp-2)); }
287
+ /* ---- where it sits in what post.css draws (docs/reference.md rule 15) ---- */
288
+ .post-corner__stamp > :is(.stamp, .stamp-place) { grid-area: 1 / 1; width: 100%; max-width: none; } /* the corner is the postcard's own size (72 × 90), outside the stamp's two sizes, by the owner's decision (2026-10-11); its box sizes the stamp, which fills it */