@usableapp/cardds 0.7.82 → 0.7.84

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 (47) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +72 -12
  3. package/bin/cardds.mjs +21 -10
  4. package/css/actions.css +2 -0
  5. package/css/card.css +4 -4
  6. package/css/layover.css +5 -0
  7. package/css/people.css +3 -2
  8. package/css/post.css +16 -4
  9. package/css/stamp.css +23 -21
  10. package/css/sticky.css +138 -28
  11. package/dist/actions/Chip.d.ts +4 -2
  12. package/dist/actions/Chip.js +3 -3
  13. package/dist/cardds.css +191 -59
  14. package/dist/catalog.json +1 -1
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +2 -0
  17. package/dist/post/PostBox.d.ts +1 -1
  18. package/dist/post/PostDesk.d.ts +2 -2
  19. package/dist/post/PostLetter.d.ts +1 -1
  20. package/dist/post/Postmark.d.ts +1 -1
  21. package/dist/post/Postmark.js +1 -1
  22. package/dist/post/postCorner.js +1 -1
  23. package/dist/scaffold/LayerHistory.d.ts +2 -0
  24. package/dist/scaffold/LayerHistory.js +2 -0
  25. package/dist/sheets/Modal.d.ts +4 -1
  26. package/dist/sheets/Modal.js +4 -1
  27. package/dist/stamp/Stamp.d.ts +4 -3
  28. package/dist/stamp/Stamp.js +3 -2
  29. package/dist/stamp/StampBook.d.ts +3 -3
  30. package/dist/stamp/StampBook.js +1 -1
  31. package/dist/stamp/StampOn.d.ts +1 -0
  32. package/dist/stamp/StampOn.js +1 -0
  33. package/dist/sticky/Sticky.d.ts +8 -6
  34. package/dist/sticky/Sticky.js +6 -3
  35. package/dist/sticky/StickyAnswer.d.ts +23 -0
  36. package/dist/sticky/StickyAnswer.js +35 -0
  37. package/dist/sticky/StickyBoard.d.ts +12 -0
  38. package/dist/sticky/StickyBoard.js +13 -0
  39. package/dist/sticky/StickyThread.d.ts +12 -6
  40. package/dist/sticky/StickyThread.js +8 -4
  41. package/dist/words.d.ts +1 -1
  42. package/dist/words.js +2 -2
  43. package/lib/catalog.mjs +1 -0
  44. package/lib/mcp.mjs +1 -1
  45. package/lib/role.mjs +11 -3
  46. package/lint/rules.mjs +7 -2
  47. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,53 @@
2
2
 
3
3
  > For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
4
4
 
5
+ ## 0.7.84 — 2026-10-11
6
+
7
+ ### Changed — breaking for a screen that draws a `Stamp`
8
+ - **A stamp is one of two sizes, wherever it stands** (Lh 2026-10-11) — the standard size, `--stamp-size`, and the small size, `--stamp-sm-size`, for a stamp in a `StampOn` and in an `AvatarStack`. The stamp at a postcard's corner is neither: it keeps its own size, 72 × 90 (`--post-corner-stamp-w`), as before. Before → after for the standard size: a `Stamp` or a `StampPlace` filled its column up to a cap, so its size came from what held it → it is `--stamp-size` wide (two taps: 96 × 120; 90 × 112.5 on a screen 360 wide) in every holder, and the holder only places it. A holder narrower than a stamp shrinks it. Every screen that draws a stamp changes look. Measured at 375, before → after: in a `Centre` on a fill screen 192.08 × 240.09 → 96 × 120; in a sheet body or on a page 216 × 270 → 96 × 120; in a `Centre` inside a sheet body 210 × 262.5 → 96 × 120; in a `StampBook` on its ground 95.66 × 119.56 → 96 × 120; in a `StampBook bare` 109 × 136.25 → 96 × 120; in a `StampBook strip` 72 × 90 → 96 × 120; in a card's head 96 × 120, unchanged. To bring a screen up to date: look at each stamp screen again. For a larger or smaller stamp set `--stamp-size`, on `:root` for every stamp or on an element for the stamps inside it.
9
+ - **Nine knobs are gone**: `--stamp-max-w`, `--stamp-centre-max-w`, `--card-head-stamp-w`, `--stamp-book-bare-gap`, `--stamp-book-strip-w`, `--stamp-on-w`, `--sticky-stamp-w`, `--avatar-stack-stamp-w`, `--post-desk-pick-w`. Setting one now does nothing. Use `--stamp-size` for a standing stamp (the first five, and `--post-desk-pick-w`) and `--stamp-sm-size` for a stamp in a `StampOn` or an `AvatarStack` (`--sticky-stamp-w`, `--stamp-on-w`, `--avatar-stack-stamp-w`). There is no knob per place: set `--stamp-size` on the element that holds the stamps to move only those.
10
+ - **`StampBook bare` no longer makes its stamps fill their thirds** — before → after: no ground, the stamp's cap lifted, each stamp the whole width of its third (109 × 136.25 at 375) → no ground, no padding, no round, and nothing else: each stamp is its own size in the middle of its third, with the same gaps as on the ground.
11
+ - **`--stamp-book-col-gap` defaults to `--sp-2`** — before → after: `--sp-3` → `--sp-2` (8), so three stamps of 96 keep their size on the ground in a 343 column (343 − 32 of padding − 16 of gaps = 295). In a holder narrower than that the three shrink alike (81.67 each at 320). The strip reads the same knob.
12
+ - **A stamp in a `StampOn` is 48 × 60 on any surface** — before → after: 27% of the surface's width (54 × 67.5 on a 200 surface), or 48 on a note → 48 × 60 everywhere, `--stamp-sm-size` (one tap). On a note nothing moved.
13
+ - **`Stamp words` is not drawn at the standard size** — before → after: the small line showed on a stamp wider than 96, which a centred stamp was (192 to 218) → a stamp is 96, where the small line is not drawn and `name` is larger. It shows again only where `--stamp-size` is set above 6rem (96).
14
+ - **A stamp in an `AvatarStack` is the small size** (Lh 2026-10-11) — before → after: 32 × 40 (`--avatar-stack-stamp-w`) → 48 × 60 (`--stamp-sm-size`), the same size as a stamp in a `StampOn`. The lap between stamps is 8, unchanged. The stack is wider, so the words beside it have less room. Measured on a `Row` whose lead is three stamps and a count, with a title, a meta line and an end button: at 375 the meta line is on two lines and the row is 76.6 high (was 53.6); at 360 and 390 it stays on one line and the rows are 56.3 and 60 high. To bring a screen up to date: look at each row that leads with a stack of stamps, and shorten its meta line where it now breaks.
15
+ - **The stamps in a `PostDesk`'s tray are the standard size** (Lh 2026-10-11, "ทำในรุ่นนี้เลย"; this release only, so the postcard set changes with the stamp's) — before → after: each stamp in the tray was 72 × 90 (`--post-desk-pick-w`) → `--stamp-size`, 96 × 120 (90 × 112.5 at 360, where the root is 15px). The tilt (`--post-corner-stamp-tilt`, 2deg) and the 4:5 ratio are as before. The tray row is 130 high at 375 (was 100), so on the address half of `PostScreens · Write` its bottom is at 698 on a screen 812 high (the bar starts at 720); on a screen 667 high the tray was already under the bar and the page scrolls, now 30 further. To bring a screen up to date: look at each `PostDesk` again. The corner's stamp, the desk's stamp box and the postmark are unchanged.
16
+ - **The desk's tray scrolls sideways when its stamps outrun the screen** — before → after: a row that did not scroll (four stamps of 72 ran 25 past a column of 343) → one line that scrolls (`overflow-x: auto`, no scroll bar) and never shrinks a stamp. Three fit a column of 343 at 375 (3 × 104 + 2 × 12 = 336), four do not.
17
+ - **`--post-desk-stamps-gap` defaults to `--sp-3`** — before → after: `--sp-4` (16) → `--sp-3` (12). Three stamps of 96 with the old gap are 344 wide, 1px more than the column at 375, and the tray would scroll for nothing.
18
+
19
+ ### Docs
20
+ - **Mount a `Modal` only when it is to show.** A `Modal` first mounted with `open={false}` is drawn, leaves, and calls `onClosed` after about 700ms. Known fault, not fixed in this release: under React StrictMode `onClosed` is never called for a `Modal` mounted that way.
21
+ - **`useLayer`: one layer per tick.** Two layers opened in the same handler lose the first (`a.open(1); b.open(2)` gives `a.value` null, `b.value` 2). Open the second in a later tick.
22
+
23
+ ## 0.7.83 — 2026-10-11
24
+
25
+ ### Changed — breaking for a screen that draws a `Sticky`
26
+ - **Every `Sticky` is one square of one size** (Lh 2026-10-10) — before → after: a note hugged its words, was at least 211 tall, grew with more words, and had 4px round corners → every note is a square of 168 with square corners, whatever it says. Words it cannot hold are cut where their room ends, with no ellipsis. To bring a screen up to date: shorten what a note says, or open the note to show the rest. One knob sets the side: `--sticky-size` (on `:root` for every note, on an element for the notes inside it). `--sticky-radius` still rounds the corners; its default is now 0.
27
+ - **`Sticky square` is gone** — before → after: `square` made a note 1:1 and as wide as its column → every note is the one square; delete the prop. No alias: a screen that still passes it fails its typecheck.
28
+ - **Seven knobs are gone**: `--sticky-min-w`, `--sticky-max-w`, `--sticky-min-h`, `--sticky-square-max-w`, `--sticky-tilt-max-w`, `--sticky-post-max-w`, and `--sticky-field-min-h` (the field fills the note). Setting one now does nothing; use `--sticky-size`.
29
+ - **The fold means unread** — before → after: the folded corner was a look, `flat` the plain square → a folded corner says the note has not been read, `flat` says it has. A folded note also carries a hidden word for a screen reader ("Unread" / "ยังไม่ได้อ่าน" by `<html lang>`; `unreadLabel` for the app's own; `setWords` for another language). To bring a screen up to date: pass `flat` on every note that is not new.
30
+ - **A stamp on a note is 48 wide** — before → after: a `StampOn` in `Sticky stuck` was 27% of the note → 48, whatever the note's size (`--sticky-stamp-w`). `--stamp-on-w` still sets it; the note's own knob wins.
31
+ - **A lone note stands in the middle** of a `Page` or a `Centre` (it used to be the `square` note's own doing).
32
+
33
+ ### Changed
34
+ - **`npx cardds check` says how many files it read, and refuses to call nothing "clean"** (Lh 2026-10-10) — before → after: `cardds check docs` (a folder with no `.js .jsx .ts .tsx`) or `cardds check src scr` (a typo next to a real folder) printed `cardds check: clean` and exited 0, because a path that was not there was dropped and a folder with nothing in it had nothing to flag → the line ends with the count (`cardds check: clean (103 files read)`), a path that is not there is `…: no such folder or file` and exit 2 before any file is read, a folder with no source in it is exit 2, and a single file is a path too. Findings still exit 1. A script that ran `cardds check <dir>` on an empty or misspelt folder now fails where it passed.
35
+
36
+ ### Fixed
37
+ - **A `BtnRow` in a `ModalAsk` fills the card** (Lh 2026-10-11) — before → after: the row was as wide as its buttons' words, so two word buttons were squeezed and their words wrapped (99 × 87 each at 375) → the row fills the ask, 24 under the question, and each button keeps its words on one line (146.5 × 60). A `Btn quiet` in a `ModalAsk` is edged in the ask's own ink at full strength (it was 35%): light on the dark end, dark on `ModalAsk light`. A `BtnTrio` is unchanged. Knobs: `--modal-drawer-answers-gap`, `--modal-drawer-quiet-border`. Story `Modal · WordAnswers`. A project that forced this from outside can delete its own rule.
38
+ - **`cardds check` and the ESLint rule missed two cardds classes** — before → after: `raw-class` read the class list from `css/*.css` and lost every class that only ever stands second in a compound selector (`.field.field--error`), so `className="field--error"` and `className="pin--error"` were not flagged → both are in the list (494 of 494). A screen that wrote either by hand now gets the `raw-class` error: use `Field error` / `Pin error`.
39
+ - **`cardds get <Name>` left out props and parameters** — before → after: `Countdown` showed none of the `Link` props it takes (`disabled`, `muted`, `icon`, `sm`) and `Home`, `Splash`, `Walkthrough`, `CallSheet` none of the `Screen` ones (`as`, `palette`, `ready`), because only the component's own interface was read; the ten functions an app calls (`slideTo`, `slideScreen`, `useLayer`, `useOptionalLayer`, `word`, `setWords`, `setTextScale`, `controlsFor`, `registerIcons`, `applyMask`) showed an empty `props` → inherited props are listed (`Omit<…>` taken off), and a function lists its `parameters` (`slideTo`: `direction`, `update`, `scope = document`; `--json` has `params`).
40
+ - **`cardds get slideTo` showed no variation though three stories call it** — before → after: a story counted as using a component only when it rendered it as a tag, so `slideTo`, `slideScreen`, `useLayer` and `useLayers` were used by no story → a function a story calls counts (7 story–function pairs found), and `get` / `find` show those stories.
41
+ - **The MCP server kept an old catalog after `package.json` or `gallery/vite.config.ts` changed** — both are read by the catalog build (the version; how the stories render), neither moved the stamp that makes the server load again.
42
+ - **A line of `docs/roles.md` the reader could not parse was dropped** — the table is read line by line now and a line that is not a row names itself in the error (nothing in the table is wrong today).
43
+
44
+ ### Added
45
+ - **`StickyBoard`** — notes stuck on a page: rows that wrap by the width (two on a phone, three on a wider screen), edge to edge, each turned by one of four angles in turn, the later note over the earlier where they cross. Knobs: `--sticky-board-lap`, `--sticky-board-turn-1` … `-4`, `--sticky-board-drop`, `--sticky-board-foot-inset`. A note's `foot` there lies over the note's foot and takes no room from the words.
46
+ - **`StickyThread lapped`** — a chat with no head rows: theirs at the left, mine at the right, each turn lying over a corner of the one before, the time close under its note. `StickyPost who` is not drawn and `name` is only heard. Knobs: `--sticky-thread-lap` (76 down), `--sticky-thread-lap-x` (32 across), `--sticky-thread-same-gap`, `--sticky-thread-time-gap`, `--sticky-thread-tilt`. The head-row way and `rail` are unchanged.
47
+ - **`StickyAnswer`** — the note you answer on, in its two places: waiting at the screen's foot, and at the top while written on, with `tools` and `tray` under it. `writing` + `onWritingChange(writing, 'focus' | 'outside' | 'escape')`, or `defaultWriting`. Knobs: `--sticky-answer-show`, `-end`, `-tilt`, `-top`, `-gap`, `-tools-gap`, `-tray-gap`, `-scrim-bg`.
48
+ - **`Chip sm`** — a small label, low and tight (a name on a note). Before: a chip was 38 tall whatever its padding knobs said, because its height is the `--chip-h` token. Never a tap target. Knobs: `--chip-sm-pad-y`, `--chip-sm-pad-x`, `--chip-sm-gap`.
49
+ - **A `Chip` in a note's `foot` or `head`** is a label on the paper: a pale ground (`--sticky-chip-bg`), no edge. In the foot it stands at the start and is cut with an ellipsis past `--sticky-foot-chip-max-w` (62%) of the row; what follows it stands at the end and is never cut.
50
+ - Gallery: `StickyBoard`, `StickyAnswer`, `StickyThread · Lapped`, `Sticky · Unread / LongWords / Size`, `Chip · Small`, and the screens `StickyScreens · ChatPage`, `Chat` and `EmptyChat` (a chat with no note yet: an `EmptyState` on the ground, the answer note at the foot).
51
+
5
52
  ## 0.7.82 — 2026-10-10
6
53
 
7
54
  ### Changed
package/README.md CHANGED
@@ -386,6 +386,10 @@ whose markup has rules, because the sheet's box is the step itself:
386
386
  bottom layer), stays `duration` (4 s), slides out, never takes the cursor, never stops the member. `open` +
387
387
  `onOpenChange(open, reason)` or `defaultOpen`; unmount it in `onClosed`. One at a time: the queue and the notification
388
388
  list are the app's — the app shows its newest notification in it, `onTap` opens that notification.
389
+ - **Mount a `Modal` only when it is to show.** A `Modal` first mounted with `open={false}` is drawn in place, then leaves,
390
+ and calls `onClosed` after about 700ms. Known fault, not fixed: under React StrictMode (any dev build) `onClosed` is
391
+ never called for a `Modal` mounted that way. Keep the app's own `mounted` flag: set it when the question is asked,
392
+ clear it in `onClosed`.
389
393
  - **cardds names its own controls, in the page's language** (Lh 2026-09-29): a handle, a back button, a Pin's cells… read
390
394
  their name from `src/words.ts` by `<html lang>` (en, th ship; `setWords(lang, {…})` adds one). An app passes no label that
391
395
  only repeats these words; a prop still wins.
@@ -447,7 +451,9 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
447
451
  back one entry, or, when it came with the page, the entry without it) · `way.arrive(value)` (a link straight to an open
448
452
  sheet: read `?way=` once, no entry added, the query dropped). `useLayers().closeAll(then)` takes every layer off, then moves
449
453
  (signing out). NOTHING INSIDE A LAYER TAKES AN ENTRY — a step, typed text, a sheet's rest, a tab: back at any step takes the
450
- whole sheet down, and opened again it starts anew (a new `key` per opening). The project hands its router over once:
454
+ whole sheet down, and opened again it starts anew (a new `key` per opening). ONE LAYER PER TICK: two layers opened in
455
+ the same handler lose the first (`a.open(1); b.open(2)` gives `a.value` null and `b.value` 2). Open the second in a
456
+ later tick. The project hands its router over once:
451
457
  `<LayerHistory state push replace back>` around its screens — react-router: `location = useLocation()`, `state = location.state`,
452
458
  `push = (s, search) => navigate({ pathname: location.pathname, search: search === 'drop' ? '' : location.search }, { state: s })`, `replace = (s, search) => navigate({ pathname: location.pathname, search: search === 'drop' ? '' : location.search }, { replace: true, state: s })`,
453
459
  `back = (n) => navigate(-n)`. cardds never calls the History API itself. Story `LayerHistory · Back`.
@@ -569,6 +575,45 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
569
575
 
570
576
  Live: every screen in the gallery (`npm run dev`).
571
577
 
578
+ ## Sticky notes
579
+
580
+ A sticky note is a square of paper with a few words on it. The family is in `src/sticky/`, its CSS in `css/sticky.css`.
581
+
582
+ - **One note, one size.** Every `Sticky` is one square, 168 by default, with square corners. It never grows with its
583
+ words: words it cannot hold are cut where their room ends, with no ellipsis. One knob sets the side,
584
+ `--sticky-size`; set it on an element to resize the notes inside that element only.
585
+ - **The fold says unread.** A folded bottom-right corner means the note has not been read. `flat` means it has. A
586
+ folded note also says so to a screen reader after its words: cardds's own word by the page's language, or
587
+ `unreadLabel`.
588
+ - **`Sticky`** — `tone` is one of the nine papers in `STICKY_TONES`. `head`, `date`, `foot` are small rows above and
589
+ under the words. `onClick` makes the whole note a button. `stuck` holds what is stuck on the paper (`StampOn`).
590
+ `tilt` turns the one note a start screen hands over. A `Chip sm` in the `foot` is a name label on the paper: it
591
+ stands at the start, what follows it (a time) at the end, on one line.
592
+ - **`StickyBoard`** — notes stuck on a page. Rows wrap by the width: two on a phone, three from 504 of room, a screen about 540 wide.
593
+ The notes stand edge to edge and each is turned by one of four angles in turn (`--sticky-board-turn-1` to `-4`), so
594
+ no two notes that touch lean alike. Later is on top, always: the right note over the left, the lower over the
595
+ upper. `--sticky-board-lap` (0) is how far each lies over the one before. Where two notes do not fit side by side,
596
+ each lies over the one before by what is missing, up to half the fold; past that there is one note in a row. A
597
+ note's `foot` is laid over the note's foot and takes no room from the words, so a note reads the same on the page
598
+ as where it is opened.
599
+ - **`StickyThread`** — notes as a conversation, in two ways. With head rows: each `StickyPost` shows `who`, `name`
600
+ and `time` above its notes, and `rail` joins the avatars for a feed. `lapped`: a chat with no head rows. Theirs
601
+ stand at the left and mine (`side="right"`) at the right, each turn lying over a corner of the one before
602
+ (`--sticky-thread-lap` down, `--sticky-thread-lap-x` across), the time close under its note, the name only heard.
603
+ Two turns from one side stand `--sticky-thread-same-gap` apart.
604
+ - **`StickyAnswer`** — the note you answer on. Its child is a `Sticky flat` holding a `StickyField`. At rest it waits
605
+ at the screen's foot near the right, three quarters of it in sight (`--sticky-answer-show`). A touch on it puts the
606
+ cursor in the field and the holder asks to write: `writing` + `onWritingChange(writing, reason)`, or
607
+ `defaultWriting`. While writing, the note stands at the top, the screen behind is washed pale and takes no touch,
608
+ and `tools` (the paper chooser, the send button) and `tray` (the writer's stamp) stand under it. A touch outside or
609
+ Escape asks to stop. The words, the paper and the stamps on the note are the app's. It is a child of the `Screen`.
610
+ - **`StickyField`** is the bare textarea on a note. **`StickyPick`** is the paper chooser: `value` + `onChange`,
611
+ `tones` for the ones on offer.
612
+
613
+ Whose colour a note wears is the app's rule. The gallery's `StickyScreens` shows one: a neighbour's note wears the
614
+ ground of that neighbour's stamp, and my note wears the paper I picked while writing, from a writing set plus the
615
+ ground of my own stamp.
616
+
572
617
  ## Stamps
573
618
 
574
619
  A stamp is a small paper picture a member keeps and sticks on a note or a postcard for a neighbour. Sending never uses
@@ -577,25 +622,35 @@ it up. cardds draws the paper; the pictures, their words and which stamp a membe
577
622
  - **`Stamp`** — a perforated 4:5 stamp. `pictures` is one to four nodes, the app's own art (an svg, an img), in placement
578
623
  order: the stamp places, sizes and turns them by how many there are. `tone` is the ground, one of `STICKY_TONES` (the
579
624
  same list and the same `--sticky-<tone>-bg` knobs as a note's paper). `name` is the big line on the picture's foot,
580
- `words` the small line above it. `tilt` turns it, `onClick` makes the whole stamp a button. It fills its column up to
581
- `--stamp-max-w`, and every inner length follows its width, so one stamp reads in a tray and in a sheet.
625
+ `words` the small line above it. `tilt` turns it, `onClick` makes the whole stamp a button. Every inner length follows
626
+ the stamp's width.
627
+ - **A stamp is one size wherever it stands** (Lh 2026-10-11). A `Stamp` and a `StampPlace` are `--stamp-size` wide (two
628
+ taps: 96 × 120) in a `Centre`, a sheet, a page, a `StampBook` and a card's head. What holds a stamp only places it. A
629
+ holder narrower than a stamp shrinks it, and the stamp is still shown whole. Set `--stamp-size` on `:root` for every
630
+ stamp, or on an element for the stamps inside it. There is one other size, the small one: a stamp in a `StampOn` and a
631
+ stamp in an `AvatarStack` are `--stamp-sm-size` wide (one tap: 48 × 60), on any surface and in any row. The stamps in
632
+ a `PostDesk`'s tray are the standard size. One exception, by the owner's decision: the stamp at a postcard's corner is
633
+ the postcard's own size, 72 × 90 (`--post-corner-stamp-w`), in neither. `words` is not drawn on a stamp 96 wide or
634
+ narrower, so at the standard size only `name` shows, and it is larger.
582
635
  - **`StampPlace`** — an empty place the size of a stamp, its edge a ring of round dots: a word inside, `onClick` for "make
583
636
  yours", `faint` for a gap nobody is asked to fill. The edge is a drawn stroke, so the dots are yours: `--stamp-place-dot`
584
637
  is how wide each is (default `--rule-w`, 2px), `--stamp-place-gap` the distance from one dot's centre to the next
585
638
  (default 6px), and a `--stamp-place-dash` above 0 stretches each dot into a round-ended dash. Its corner is
586
639
  `--stamp-place-radius`. The dots' outer edge is the box's edge whatever their width.
587
640
  - **`StampBook`** + **`StampCell`** — the album page: cells three across on an album ground, a `label` under each stamp,
588
- the free places drawn up to `places`. A cell with `onClick` is a button; `on` marks it pressed. `strip` is one line
589
- of small stamps that scrolls sideways, the tray a member picks from. A `false` or `null` child holds no place.
590
- - A stamp stands for a person in three more places. In a `Centre` — a `Stamp` or a `StampPlace` — it takes the screen's
591
- size and the words and link under it centre. In a card's head — `<Card heading={name} caption={greeting}
641
+ the free places drawn up to `places`. A cell with `onClick` is a button; `on` marks it pressed. Each stamp stands in
642
+ the middle of its third. `bare` is the page with no ground, no padding and no round. `strip` is one line of stamps
643
+ that scrolls sideways, the tray a member picks from. A `false` or `null` child holds no place.
644
+ - A stamp stands for a person in three more places. In a `Centre` — a `Stamp` or a `StampPlace` — it stands in the
645
+ middle and the words and link under it centre. In a card's head — `<Card heading={name} caption={greeting}
592
646
  end={<Stamp bare …/>} foot={<Btn primary block>…</Btn>} />` — the words stand at the left and the stamp at the right,
593
- as tall as both lines, `--card-head-stamp-w` wide (default two taps); the card's one button presses it whole. In an
594
- `AvatarStack` it is a head (`more="+9"` for the count).
647
+ beside both lines; the card's one button presses it whole. In an `AvatarStack` it is a head, the small size
648
+ (`more="+9"` for the count).
595
649
  - **`StampOn`** — a stamp stuck on a surface. `at` is the stamp's middle as a share of the surface (`{ x, y }`, 0 to 1),
596
650
  `turn` its angle in degrees. With `onAtChange` the member drags it (the arrow keys move it too) and it asks with the
597
651
  new place when the finger lets go; without it the stamp is still. The surface is the nearest positioned box:
598
- `Sticky stuck={…}` is one.
652
+ `Sticky stuck={…}` is one. The stamp is the small size, 48 × 60 whatever the surface's size (`--stamp-sm-size`), and
653
+ on a note it may lie over the words.
599
654
 
600
655
  What an app stores for a stamp: the picture ids (1 to 4, in order), the ground tone, the name, the small line. Making
601
656
  one is the app's screen: `ChipPick` checkboxes that each show a picture, the app refuses a fifth, and the app draws
@@ -631,8 +686,13 @@ the older `Postcard` (a member's cover card that turns over) is another componen
631
686
  the box at that card (`open` is its `id`, `null` is the front card; `onOpenChange`).
632
687
  - **`Postmark`**: the round mark, `place` and `date`. Information, nothing to press.
633
688
 
634
- The stamp is always a `Stamp` the app hands over. On a card's corner it shows its pictures only (pass `bare`; the corner
635
- drops the words by itself too); in the desk's tray it keeps its words. `PostLetter` and `PostDesk` size themselves to the
689
+ The stamp is always a `Stamp` the app hands over. On a card's corner it is the postcard's own size, 72 × 90
690
+ (`--post-corner-stamp-w`; it is not one of the stamp's two sizes), and shows its pictures only (pass `bare`; the corner
691
+ drops the words by itself too); the postmark is struck over it by 0.4 of its own size. The empty stamp box in a
692
+ `PostDesk` is the same size and shows its word. In the desk's tray a stamp is the standard size, 96 × 120
693
+ (`--stamp-size`), and keeps its words; the tray is one line that scrolls sideways when the stamps are wider than the
694
+ screen (three fit a 343 column). `--post-desk-stamps-gap` is the space between them. `PostLetter` and `PostDesk` size
695
+ themselves to the
636
696
  screen they are in, so they go in a `Step` > `Page bleed`. **`Page bleed`** runs the page to the screen's edges and keeps
637
697
  the inset as its own padding, so a card that leans, slides or casts a shadow is cut at the glass, not at the inset.
638
698
  The gallery's `PostScreens` page has the three finished screens (read, write, keep) to start from.
package/bin/cardds.mjs CHANGED
@@ -16,6 +16,7 @@ const cwd = process.cwd();
16
16
  const SKIP = new Set(['node_modules', 'dist', 'build', '.git', '.next', 'coverage']);
17
17
 
18
18
  function* sources(dir) {
19
+ if (statSync(dir).isFile()) { yield dir; return; }
19
20
  for (const name of readdirSync(dir)) {
20
21
  if (SKIP.has(name) || name.startsWith('.')) continue;
21
22
  const p = join(dir, name);
@@ -24,17 +25,26 @@ function* sources(dir) {
24
25
  }
25
26
  }
26
27
 
28
+ // A check that read nothing has proved nothing: a path that is not there, or a folder with no source in it, is exit 2 — never "clean".
27
29
  function check(dirs) {
28
- const roots = (dirs.length ? dirs : ['src']).map((d) => join(cwd, d)).filter((d) => existsSync(d));
29
- if (!roots.length) { console.error('cardds check: no such folder — `npx cardds check <dir>`'); return 2; }
30
- let errors = 0, warns = 0;
31
- for (const root of roots) for (const file of sources(root)) {
32
- for (const f of checkSource(readFileSync(file, 'utf8'), relative(cwd, file))) {
33
- f.level === 'error' ? errors++ : warns++;
34
- console.log(`${f.file}:${f.line} ${f.level === 'error' ? 'error' : 'warn '} [${f.rule}] ${f.message}`);
30
+ const roots = dirs.length ? dirs : ['src'];
31
+ const missing = roots.filter((d) => !existsSync(join(cwd, d)));
32
+ if (missing.length) { for (const d of missing) console.error(`cardds check: ${d}: no such folder or file — \`npx cardds check <dir>\``); return 2; }
33
+ let errors = 0, warns = 0, read = 0;
34
+ for (const dir of roots) {
35
+ let here = 0;
36
+ for (const file of sources(join(cwd, dir))) {
37
+ here++;
38
+ for (const f of checkSource(readFileSync(file, 'utf8'), relative(cwd, file))) {
39
+ f.level === 'error' ? errors++ : warns++;
40
+ console.log(`${f.file}:${f.line} ${f.level === 'error' ? 'error' : 'warn '} [${f.rule}] ${f.message}`);
41
+ }
35
42
  }
43
+ if (!here) { console.error(`cardds check: ${dir}: no .js .jsx .ts .tsx file to read (node_modules, dist, build and dot-folders are skipped)`); return 2; }
44
+ read += here;
36
45
  }
37
- console.log(errors + warns ? `\ncardds check: ${errors} error(s), ${warns} warning(s)` : 'cardds check: clean');
46
+ const files = `${read} file${read === 1 ? '' : 's'} read`;
47
+ console.log(errors + warns ? `\ncardds check: ${errors} error(s), ${warns} warning(s) (${files})` : `cardds check: clean (${files})`);
38
48
  return errors ? 1 : 0;
39
49
  }
40
50
 
@@ -93,8 +103,9 @@ const asText = {
93
103
  `${c.name} (${c.group})${c.element ? ` — forwards the <${c.element}> attributes` : ''}`,
94
104
  one(c.description),
95
105
  ...(c.role ? [`role: represents ${[c.role.represents, c.role.count, c.role.density, c.role.job].flat().join(' · ')}${c.role.standsInFor.length ? `; stands in for: ${c.role.standsInFor.map((s) => (s.condition ? `${s.name} (${s.condition})` : s.name)).join('; ')}` : ''}`] : []),
96
- '\nprops',
97
- ...c.props.map((p) => ` ${p.name}${p.optional ? '?' : ''}: ${p.type}${p.slot ? ' [slot]' : ''}${p.description ? ` — ${one(p.description)}` : ''}`),
106
+ ...(c.params ? ['\nparameters', ...(c.params.length ? [] : [' (none)']), ...c.params.map((p) => ` ${p.name}${p.optional && !p.default ? '?' : ''}: ${p.type}${p.default ? ` = ${p.default}` : ''}`)] : [
107
+ '\nprops',
108
+ ...c.props.map((p) => ` ${p.name}${p.optional ? '?' : ''}: ${p.type}${p.slot ? ' [slot]' : ''}${p.description ? ` — ${one(p.description)}` : ''}`)]),
98
109
  `\nvariations (${c.variations.length})`,
99
110
  ...c.variations.map((v) => `\n ${v.id} — ${one(v.description)}\n${indent(v.tsx)}${html ? `\n html:\n${indent(v.html, ' ')}` : ''}`),
100
111
  ...(c.alsoUsedIn ? [`\nalso used in: ${c.alsoUsedIn.join(' ')}\n(--all prints their code too)`] : []),
package/css/actions.css CHANGED
@@ -195,6 +195,8 @@
195
195
  .dot--warn { background: var(--signal-warn); }
196
196
  .dot--error { background: var(--signal-error); }
197
197
  .dot--info { background: var(--signal-info); }
198
+ /* --sm: a small LABEL (a name on a note, a tag in a tight head) — low and tight; never a tap target */
199
+ .chip--sm { min-height: 0; gap: var(--chip-sm-gap, var(--sp-1)); padding: var(--chip-sm-pad-y, var(--sp-0)) var(--chip-sm-pad-x, var(--sp-2)); }
198
200
  .chip--on { background: var(--chip-on-bg, var(--accent)); color: var(--chip-on-ink, var(--on-accent)); border-color: transparent; }
199
201
  /* muted: an untouched default — the words wash out, the edge stays; emphasis is the difference from the neighbours (README 13) */
200
202
  .chip--muted { color: var(--chip-muted-ink, var(--card-muted)); }
package/css/card.css CHANGED
@@ -84,14 +84,14 @@
84
84
  height, only the row gap. */
85
85
  .card__head :is(h1, h2, h3) { overflow-wrap: anywhere; }
86
86
 
87
- /* a Stamp in a card's head (`Card end={<Stamp …/>}`): the words stand at the left, the stamp at the right and AS TALL AS BOTH LINES — the heading and the
88
- caption pair up beside it, not the stamp beside the heading alone with the caption left under it. A stamp has no size of its own in a flex
89
- row (a width of 100% of the head, up to its cap), so it takes --card-head-stamp-w here, as a stamp in an AvatarStack takes a width of its own.
87
+ /* a Stamp in a card's head (`Card end={<Stamp …/>}`): the words stand at the left, the stamp at the right BESIDE BOTH LINES — the heading and the
88
+ caption pair up beside it, not the stamp beside the heading alone with the caption left under it. The stamp is its own size (--stamp-size,
89
+ stamp.css); the head only places it.
90
90
  <div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><span class="stamp">…</span></div> */
91
91
  .card__head:has(> .stamp) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
92
92
  .card__head:has(> .stamp) > :is(h1, h2, h3) { grid-column: 1; grid-row: 1; align-self: end; }
93
93
  .card__head:has(> .stamp) > :is(.t-caption, .t-body) { grid-column: 1; grid-row: 2; align-self: start; }
94
- .card__head > .stamp { grid-column: 2; grid-row: 1 / span 2; width: var(--card-head-stamp-w, calc(var(--tap) * 2)); margin-inline: 0; }
94
+ .card__head > .stamp { grid-column: 2; grid-row: 1 / span 2; margin-inline: 0; }
95
95
 
96
96
  /* ---- bleed: a scrolling track that runs to the container's edges ----
97
97
  For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
package/css/layover.css CHANGED
@@ -83,6 +83,11 @@
83
83
  .modal:not(:has(> .modal__lift)) > .modal__drawer { height: auto; padding-block: var(--modal-confirm-pad-y, var(--sp-6)); animation: none; }
84
84
  .modal__drawer .t-caption { opacity: var(--dim); }
85
85
  .modal__drawer > h2:focus { outline: none; } /* the question takes the cursor when the modal arrives (for a screen reader, the keyboard's way in) — it is not a control: no ring */
86
+ /* the answers as WORD buttons (a BtnRow of Btns; Lh 2026-10-11): the row fills the ask with room over it and each button keeps its words on one
87
+ line — left to its words' width the row squeezed two buttons into round blobs, their words on two lines. A quiet button is an outline only,
88
+ so on the ask its edge is the ask's ink at full strength (light on the dark end, dark on --light). The BtnTrio of round buttons is not touched. */
89
+ .modal__drawer > .btn-row { justify-self: stretch; margin-top: var(--modal-drawer-answers-gap, var(--sp-5)); white-space: nowrap; }
90
+ .modal__drawer .btn--quiet { border-color: var(--modal-drawer-quiet-border, currentColor); }
86
91
  .modal__drawer .icon-btn { background: transparent; color: inherit; border-color: var(--ink-rail); }
87
92
  .modal__drawer .icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; width: var(--tap-lg); height: var(--tap-lg); }
88
93
  @media (prefers-reduced-motion: no-preference) {
package/css/people.css CHANGED
@@ -50,9 +50,10 @@
50
50
  .avatar-stack { display: inline-flex; align-items: center; }
51
51
  .avatar-stack > .avatar { box-shadow: 0 0 0 var(--sp-1) var(--card-bg, var(--surface)); }
52
52
  .avatar-stack > .avatar + .avatar { margin-left: calc(-1 * var(--sp-3)); }
53
- /* stamps in the stack: a Stamp is as wide as its column, and an inline row has none — so it brings one, as tall as an sm head (4:5) and a little overlapped, like the heads
53
+ /* a stamp in the stack is the stamp's small size, the one a stuck stamp is (--stamp-sm-size, stamp.css: 48 × 60), lapped like the heads. Measured 2026-10-11 in a Row's
54
+ 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)
54
55
  <span class="avatar-stack"><span class="stamp stamp--bare">…</span><span class="stamp stamp--bare">…</span><span class="avatar-stack__more">+9</span></span> */
55
- .avatar-stack > .stamp { --_stack-w: calc(var(--avatar-sm) * 4 / 5); flex: none; width: var(--avatar-stack-stamp-w, var(--_stack-w)); margin-inline: 0; }
56
+ .avatar-stack > .stamp { flex: none; width: var(--stamp-sm-size, var(--tap)); margin-inline: 0; }
56
57
  .avatar-stack > .stamp + .stamp { margin-left: calc(-1 * var(--sp-2)); }
57
58
  /* more: the count that trails the stack — plain words on the ground, title size so it reads at arm's length (the accent "+7" avatar is the small, framed way) */
58
59
  .avatar-stack__more { margin-left: var(--avatar-stack-more-gap, var(--sp-3)); font: var(--avatar-stack-more-font, var(--type-title)); color: var(--avatar-stack-more-ink, inherit); white-space: nowrap; }
package/css/post.css CHANGED
@@ -60,7 +60,7 @@
60
60
  aspect-ratio: 4 / 5;
61
61
  rotate: var(--post-corner-stamp-tilt, 2deg);
62
62
  }
63
- .post-corner__stamp > :is(.stamp, .stamp-place) { grid-area: 1 / 1; max-width: none; }
63
+ .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 */
64
64
 
65
65
  /* ============================================================
66
66
  post-letter — READING one that arrived. The written side stood upright: the address half over the message half.
@@ -243,9 +243,21 @@
243
243
  .post-desk__reach--address { inset-inline-end: calc(-1 * var(--screen-pad)); }
244
244
  .post-desk__reach--message { inset-inline-start: calc(-1 * var(--screen-pad)); }
245
245
  .post-desk__reach:focus-visible { outline: var(--focus); outline-offset: calc(-1 * var(--sp-1)); }
246
- /* the sender's stamps to choose from, under the card */
247
- .post-desk__stamps { display: flex; gap: var(--post-desk-stamps-gap, var(--sp-4)); align-items: center; }
246
+ /* the sender's stamps to choose from, under the card: each the standard size (--stamp-size, stamp.css), in one line that scrolls sideways when
247
+ they outrun the screen — a stamp never shrinks to fit. Three fit a 343 column at 375 (3 × 104 + 2 × 12); with the gap at sp-4 they were 1px over. The padding (taken back by the margin) keeps the focus ring and the lean inside the scroller */
248
+ .post-desk__stamps {
249
+ display: flex;
250
+ gap: var(--post-desk-stamps-gap, var(--sp-3));
251
+ align-items: center;
252
+ margin: calc(-1 * var(--sp-2));
253
+ padding: var(--sp-2);
254
+ overflow-x: auto;
255
+ overscroll-behavior-x: contain;
256
+ scrollbar-width: none;
257
+ }
258
+ .post-desk__stamps::-webkit-scrollbar { display: none; }
248
259
  .post-desk__pick {
260
+ flex: none;
249
261
  display: grid;
250
262
  place-items: center;
251
263
  min-width: var(--tap);
@@ -255,7 +267,7 @@
255
267
  border-block-end: var(--rule-w) solid transparent;
256
268
  background: transparent;
257
269
  }
258
- .post-desk__pick > .stamp { --_w: calc(var(--sp-2) * 9); width: var(--post-desk-pick-w, var(--_w)); max-width: none; rotate: var(--post-corner-stamp-tilt, 2deg); } /* in the tray a stamp keeps its words */
270
+ .post-desk__pick > .stamp { rotate: var(--post-corner-stamp-tilt, 2deg); } /* in the tray a stamp keeps its words */
259
271
  .post-desk__pick[aria-pressed="true"] { border-block-end-color: var(--on-surface); }
260
272
  .post-desk__note { margin: 0; font: var(--type-caption); color: var(--post-desk-note-ink, var(--on-surface-muted)); min-height: 1.35em; }
261
273
  /* what stands where the card was, once it has left */
package/css/stamp.css CHANGED
@@ -13,13 +13,17 @@
13
13
  </span>
14
14
  Two boxes, as the sticky note: the OUTER one holds the size and the shadow, the PAPER is the one whose edge is
15
15
  punched (a mask on the outer box would cut its own shadow off). The stamp is a size container: every inner length
16
- is a share of its width (cqw), so it reads the same from a tray to a sheet. The ground wears a sticky tone class —
17
- one list of tones (src/sticky/tones.ts), one set of tone knobs. */
16
+ is a share of its width (cqw). The ground wears a sticky tone class — one list of tones (src/sticky/tones.ts), one
17
+ set of tone knobs.
18
+ ONE SIZE (Lh 2026-10-11): a stamp, and the empty place for one, is --stamp-size wide wherever it stands — in a book, a tray,
19
+ a card's head, a centre, a sheet, a page. What holds it only PLACES it. There is ONE other size, the small one, --stamp-sm-size
20
+ (one tap: half the stamp), for a stamp stuck ON something (`.stamp-on`, below) and a stamp in an avatar stack (people.css): one knob for both.
21
+ ONE exception, by the owner's decision (Lh 2026-10-11): the stamp at a postcard's corner (post.css) is the postcard's own size, 72 × 90,
22
+ --post-corner-stamp-w, in neither. The desk's tray (post.css) holds stamps of the standard size. */
18
23
  .stamp, .stamp-place {
19
- --_cap: calc(var(--tap) * 4.5);
20
24
  box-sizing: border-box;
21
- width: 100%; /* sized by its column… */
22
- max-width: var(--stamp-max-w, var(--_cap)); /* …up to a cap */
25
+ width: var(--stamp-size, calc(var(--tap) * 2));
26
+ max-width: 100%; /* a holder narrower than a stamp still shows it whole */
23
27
  aspect-ratio: 4 / 5;
24
28
  margin-inline: auto;
25
29
  }
@@ -63,7 +67,7 @@
63
67
  .stamp__words, .stamp__name { display: block; min-width: 0; white-space: nowrap; overflow-x: clip; text-overflow: ellipsis; }
64
68
  .stamp__words { --_words: 4.8cqw; font: 500 var(--_words) / 1.3 var(--font-ui); font-size: var(--stamp-words-font-size, var(--_words)); }
65
69
  .stamp__name { --_name: 9.5cqw; font: 900 var(--_name) / 1.15 var(--font-display); font-size: var(--stamp-name-font-size, var(--_name)); }
66
- /* tiny (a tray, an album cell, a stamp stuck on a note): the small line goes, the name grows */
70
+ /* at the stamp's own size and under (and stuck on a note): the small line goes, the name grows */
67
71
  /* --bare: the pictures alone, no words — a stamp stuck on something (a note, a postcard). The words belong to the album.
68
72
  The pictures take the foot's room: each sits a little lower, so the group stands in the middle of the frame. */
69
73
  .stamp--bare .stamp__cap, .stamp-on .stamp__cap, .post-corner .stamp__cap { display: none; }
@@ -82,12 +86,8 @@
82
86
  /* --tap: the whole stamp is the button (`onClick`); the press and the ring are the PRESS list's (actions.css) */
83
87
  .stamp--tap { appearance: none; border: 0; padding: 0; background: none; font: inherit; text-align: start; -webkit-tap-highlight-color: transparent; }
84
88
 
85
- /* a stamp — or the empty place for one — handed over in a Centre: sized by the SCREEN (a little over half its width inside the inset), its greeting centred under it */
89
+ /* 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 */
86
90
  .screen__centre:has(> :is(.stamp, .stamp-place)) { justify-items: center; text-align: center; }
87
- .screen__centre > :is(.stamp, .stamp-place) { max-width: var(--stamp-centre-max-w, 56cqw); }
88
- /* in the room above a closed stack's heads the stamp is held by that room's HEIGHT too: its height at most 45% of it, so two lines of words under it
89
- still fit on a short phone (320×568 left 253px for a group 314 tall — reported by timebank, 2026-10-07). cqh reads the base, a size container. */
90
- .sheet-stack__base > .screen__centre > :is(.stamp, .stamp-place) { max-width: min(var(--stamp-centre-max-w, 56cqw), 36cqh); }
91
91
 
92
92
  /* ---- stamp place: an empty place for a stamp — an album's gap, "make yours" ----
93
93
  <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>
@@ -135,6 +135,9 @@
135
135
  .stamp-place--faint { --_edge: var(--stamp-place-faint-border, var(--hairline)); }
136
136
 
137
137
  /* ---- stamp book: the album page — three stamps across on an album ground, who each is from under it ----
138
+ The book places its stamps, it does not size them: each stands in the middle of its third. The column gap is the one that lets
139
+ three stamps keep their size in a phone's column (343 − 2 × 16 of padding − 2 × 8 = 295 ≥ 3 × 96); in a holder narrower than
140
+ that the thirds, and the stamps with them, shrink alike.
138
141
  <div class="stamp-book">
139
142
  <button class="stamp-cell"><span class="stamp">…</span><span class="stamp-cell__label">ต้น</span></button>
140
143
  <span class="stamp-cell">…</span>
@@ -144,7 +147,7 @@
144
147
  display: grid;
145
148
  grid-template-columns: repeat(3, minmax(0, 1fr));
146
149
  align-items: start;
147
- gap: var(--stamp-book-row-gap, var(--sp-4)) var(--stamp-book-col-gap, var(--sp-3));
150
+ gap: var(--stamp-book-row-gap, var(--sp-4)) var(--stamp-book-col-gap, var(--sp-2));
148
151
  padding: var(--stamp-book-pad, var(--sp-4));
149
152
  border-radius: var(--stamp-book-radius, var(--r-media));
150
153
  background-color: var(--stamp-book-bg, var(--ink-tint));
@@ -152,6 +155,7 @@
152
155
  .stamp-cell {
153
156
  position: relative;
154
157
  display: grid;
158
+ grid-template-columns: minmax(0, 1fr); /* the cell's one column is the cell, never its stamp: in a third narrower than a stamp the stamp gives way (its max-width) */
155
159
  gap: var(--stamp-cell-gap, var(--sp-2));
156
160
  min-width: 0;
157
161
  appearance: none;
@@ -178,15 +182,13 @@
178
182
  color: var(--stamp-cell-on-ink, var(--on-accent));
179
183
  }
180
184
  .stamp-cell__on > .icon { width: 70%; height: 70%; }
181
- /* --bare: no ground, no padding — the stamps lie on what holds the book and fill it: three across, each the whole width of its third (the Stamp's own cap is lifted), the gap the least that still keeps three apart (the shadow's room), the row gap the same. The name is the Stamp's own; free places still draw.
185
+ /* --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.
182
186
  <div class="stamp-book stamp-book--bare"> */
183
- .stamp-book--bare { gap: var(--stamp-book-bare-gap, var(--sp-2)); padding: 0; border-radius: 0; background: none; }
184
- .stamp-book--bare :is(.stamp, .stamp-place) { max-width: none; }
185
- /* --strip: ONE line of small stamps that scrolls sideways — the tray a member picks from. No ground. */
187
+ .stamp-book--bare { padding: 0; border-radius: 0; background: none; }
188
+ /* --strip: ONE line of stamps that scrolls sideways — the tray a member picks from, each stamp its own size. No ground. */
186
189
  .stamp-book--strip {
187
- --_strip: calc(var(--tap) * 1.5);
188
190
  display: flex;
189
- gap: var(--stamp-book-col-gap, var(--sp-3));
191
+ gap: var(--stamp-book-col-gap, var(--sp-2));
190
192
  padding: var(--sp-2) 0;
191
193
  border-radius: 0;
192
194
  background: none;
@@ -195,7 +197,7 @@
195
197
  scrollbar-width: none;
196
198
  }
197
199
  .stamp-book--strip::-webkit-scrollbar { display: none; }
198
- .stamp-book--strip > * { flex: none; width: var(--stamp-book-strip-w, var(--_strip)); }
200
+ .stamp-book--strip > * { flex: none; }
199
201
 
200
202
  /* ---- stamp on: a stamp stuck on a surface where the member put it ----
201
203
  <div class="sticky__stuck"><div class="stamp-on stamp-on--move" style="--stamp-on-x: 0.8; --stamp-on-y: 0.2; --stamp-on-turn: 6"><span class="stamp">…</span></div></div>
@@ -205,12 +207,12 @@
205
207
  position: absolute;
206
208
  left: calc(var(--stamp-on-x, 0.5) * 100%);
207
209
  top: calc(var(--stamp-on-y, 0.5) * 100%);
208
- width: var(--stamp-on-w, 27%);
209
210
  translate: -50% -50%;
210
211
  rotate: calc(var(--stamp-on-turn, 0) * 1deg);
211
212
  pointer-events: auto; /* the layer it lies in lets a tap through to the surface; the stamp itself takes it */
212
213
  }
213
- .stamp-on > .stamp { max-width: none; }
214
+ /* the SMALL stamp: stuck on a surface (a note). 48 × 60 whatever the surface's size. The stamp carries the size and the `.stamp-on` box is as wide as its stamp */
215
+ .stamp-on > .stamp { width: var(--stamp-sm-size, var(--tap)); max-width: none; }
214
216
  .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) */
215
217
  .stamp-on--move { cursor: grab; touch-action: none; -webkit-user-select: none; user-select: none; transition: scale var(--motion-press) ease-out; }
216
218
  .stamp-on--move:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }