@usableapp/cardds 0.7.85 → 0.7.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
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.86 — 2026-10-11
6
+
7
+ Nothing here breaks an existing screen: every prop is new and optional, and a screen that does not use them draws and behaves as in 0.7.85.
8
+
9
+ ### Added
10
+ - **`StampOn onRemove`: a stuck stamp can be taken off.** When a drag ends or an arrow key moves the stamp, a cross stands on its top right corner for `removeFor` ms (default 3000). A press on the cross, or Delete / Backspace on the focused stamp, calls `onRemove`. The stamp only asks: the app takes the `StampOn` away, as it owns `at`. A stamp with `onRemove` and no `onAtChange` cannot be dragged, so a press on it shows the cross. A new drag hides the cross while the finger holds. The cross is a 24 disc in a 44 × 44 target, centred on the stamp's corner, and stays upright whatever the stamp's `turn`. Its name is `removeLabel`, default cardds's word `remove` ("Remove" / "เอาออก" by the page's language). Knobs: `--stamp-on-remove-size` (`--sp-6`, 24), `--stamp-on-remove-bg` (#141414), `--stamp-on-remove-ink` (#ffffff). A press or a drag on a removable stamp or its cross never reaches a pressable card or pile under it. Without `onRemove` a `StampOn` is as before.
11
+ - **`StampOn main`: a stuck stamp at the stamp's main size.** `--stamp-size`, 96 × 120 (90 × 112.5 on a screen 360 wide), to cover what it lies on. The default stays the small size, `--stamp-sm-size`, 48 × 60 (45 × 56.25 at 360).
12
+ - **`StickyPile`: one to six notes in a pile.** A feed item of its own, with no card frame. Its children are `Sticky` notes, the first on top. Each note under the top one lies 24 right and 8 down from the one above (`--sticky-pile-step-x`, `--sp-6`; `--sticky-pile-step-y`, `--sp-2`), turned by `StickyBoard`'s four angles by its place, so the top note's turn is the same for every count. Only the top note draws its `foot`. Measured at 375: one note 184 × 184, three 232 × 200, six 304 × 224, with `--sticky-pile-pad` (`--sp-2`, 8) of room round the notes. The pile stands in the middle of its column and is never wider than it: a column too narrow shortens the steps. A seventh note is not drawn (a console warning, once). `onClick` makes the whole pile one button under its notes, one Tab stop, with a ring round the pile; it needs `label`, the button's name, and warns once without it. The pile only says it was pressed. A `StampOn` on the top note keeps its drag and its cross; the stamps on the notes under it are inert whatever the app passed. A pile's notes wear their stamp at one size: pass `main` to all or to none.
13
+ - A feed card that holds a `StampOn` is still not pressed whole: a `.stamp-on` counts as a control of the card. Unchanged.
14
+ - **`StampOn onRemove` with `arrive`: no cross until the stamp has landed.** A stamp that is still being struck on is not stuck yet, so it shows no cross, takes no focus and answers no Delete; once `onArrived` fires it is a removable stamp as any other. `main` with `arrive` strikes the 96 × 120 stamp.
15
+
5
16
  ## 0.7.85 — 2026-10-11
6
17
 
7
18
  ### Added
@@ -11,6 +22,7 @@
11
22
 
12
23
  ### Fixed
13
24
  - **`ChipRow bleed`: a chip keeps its words on one line** — before → after: a chip of whole sentences shrank to fit the row and its words broke onto three or four lines, so the strip never scrolled → each chip is as wide as its words and the strip scrolls sideways. A row of one-word chips is unchanged. Story `Chip · ChipRowBleedSentences`.
25
+
14
26
  ## 0.7.84 — 2026-10-11
15
27
 
16
28
  ### Changed — breaking for a screen that draws a `Stamp`
package/README.md CHANGED
@@ -607,6 +607,16 @@ A sticky note is a square of paper with a few words on it. The family is in `src
607
607
  `defaultWriting`. While writing, the note stands at the top, the screen behind is washed pale and takes no touch,
608
608
  and `tools` (the paper chooser, the send button) and `tray` (the writer's stamp) stand under it. A touch outside or
609
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
+ - **`StickyPile`** — one to six notes lying one over another, as the newest notes of a feed do. It is a feed item of
611
+ its own, with no card frame. The first child is on top, so pass the newest first. Each note under it lies a step
612
+ right and down (`--sticky-pile-step-x`, 24, and `--sticky-pile-step-y`, 8), turned by the board's four angles by
613
+ its place, so the top note turns the same whatever the count. Only the top note draws its `foot`. At 375 a pile of
614
+ one is 184 × 184, of three 232 × 200, of six 304 × 224 (`--sticky-pile-pad`, 8, is the room round the notes for
615
+ their turn). The pile stands in the middle of its column and is never wider than it: a narrow column shortens the
616
+ steps. A seventh note is not drawn, and the console says so once. With `onClick` the whole pile is one button, and
617
+ `label` is its name (required with `onClick`): one Tab stop, a ring round the pile. Where the press goes is the
618
+ app's. A `StampOn` on the top note keeps its own touch; the stamps on the notes under it are inert. A pile's notes
619
+ all wear their stamp at one size.
610
620
  - **`StickyField`** is the bare textarea on a note. **`StickyPick`** is the paper chooser: `value` + `onChange`,
611
621
  `tones` for the ones on offer.
612
622
 
@@ -654,6 +664,14 @@ it up. cardds draws the paper; the pictures, their words and which stamp a membe
654
664
  onto its place, and `onArrived` tells the app when it has landed (at once where motion is off), so the app does what
655
665
  follows. A `Card` and a `Sheet` are positioned boxes, so a `StampOn` written inside one lies on it.
656
666
  `--stamp-on-arrive-time` sets how long the strike takes.
667
+ `main` makes it the stamp's main size, 96 × 120 (`--stamp-size`; 90 × 112.5 on a screen 360 wide), to cover what
668
+ it lies on. With `onRemove` the stamp can be taken off: when a drag ends or an arrow key moves it, a cross stands on
669
+ its top right corner for `removeFor` ms (default 3000). A press on the cross, or Delete / Backspace on the focused
670
+ stamp, asks, and the app takes the `StampOn` away. A stamp with `onRemove` and no `onAtChange` shows the cross on a
671
+ press. The cross is a 24 disc (`--stamp-on-remove-size`, `--stamp-on-remove-bg` #141414, `--stamp-on-remove-ink`
672
+ #ffffff) in a 44 target, centred on the corner and upright whatever the stamp's `turn`. Its name is cardds's word
673
+ ("Remove" / "เอาออก"), or `removeLabel`. A press or a drag on a removable stamp never reaches what lies under it.
674
+ A stamp that is still arriving cannot be taken off yet: its cross waits until it has landed.
657
675
 
658
676
  What an app stores for a stamp: the picture ids (1 to 4, in order), the ground tone, the name, the small line. Making
659
677
  one is the app's screen: `ChipPick` checkboxes that each show a picture, the app refuses a fifth, and the app draws
package/css/actions.css CHANGED
@@ -382,7 +382,7 @@
382
382
  button's, the card itself is never focused or disabled. */
383
383
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
384
384
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
385
- a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach) {
385
+ a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove) {
386
386
  cursor: pointer;
387
387
  touch-action: manipulation;
388
388
  transition: scale var(--motion-press) ease-out;
@@ -401,9 +401,15 @@
401
401
  }
402
402
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
403
403
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
404
- a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
404
+ a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
405
405
  :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .feed-card--small, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
406
406
  .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
407
+ /* a pile of notes pressed whole (Lh 2026-10-11): the button lies invisible UNDER the notes, so it is the notes that answer — DIM, as a card does (the group dims as one, the
408
+ notes do not show through one another); the ring is the button's own, round the whole pile */
409
+ .sticky-pile__press { cursor: pointer; touch-action: manipulation; }
410
+ .sticky-pile__notes:has(> .sticky-pile__press) { transition: opacity var(--motion-press) ease-out; }
411
+ .sticky-pile__notes:has(> .sticky-pile__press:active) { opacity: var(--press-dim); }
412
+ .sticky-pile__press:focus-visible { outline: var(--focus); outline-offset: 0; }
407
413
  /* SHADE — what has something BEHIND it (a peeked sheet over the pile, a postcard's face over its back, a post letter's sheet over the other) must not turn see-through:
408
414
  it darkens a touch instead (--press-shade) */
409
415
  :where(.sheet-stack--tap > .card:not(.is-open), .postcard__turn, .post-letter__sheet:not(:has(.icon-btn:active))) { cursor: pointer; touch-action: manipulation; transition: filter var(--motion-press) ease-out; }
@@ -411,17 +417,17 @@
411
417
  /* the focus ring: on the control, or on the label that wraps a hidden input (chip, tile, file button, a row that is a label) */
412
418
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
413
419
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
414
- a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach):focus-visible,
420
+ a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove):focus-visible,
415
421
  :where(.chip--pick, .tile--pick, label.btn, label.row, .toggle-field):has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
416
422
  :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .feed-card--small, .toggle-field, .post-box__edge):not(label, .toggle-field):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
417
423
  /* disabled: no pointer anywhere; a control fades (a link and a row keep their own muted look) */
418
424
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
419
425
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
420
- a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge):is(:disabled, [aria-disabled="true"]),
426
+ a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove, a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge):is(:disabled, [aria-disabled="true"]),
421
427
  :where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn, label.row):has(:disabled) { cursor: default; }
422
428
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
423
429
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
424
- a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach):not(.link):is(:disabled, [aria-disabled="true"]),
430
+ a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove):not(.link):is(:disabled, [aria-disabled="true"]),
425
431
  :where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn):has(:disabled) { opacity: var(--off); }
426
432
  .sheet-stack--tap > .card > .card__head > :is(h1, h2, h3):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); } /* the keyboard's way into a stack sheet */
427
433
  @media (prefers-reduced-motion: reduce) { :root { --press-scale: 1; } } /* the give goes; the dim, the shade and the focus ring stay */
package/css/stamp.css CHANGED
@@ -213,11 +213,52 @@
213
213
  }
214
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
215
  .stamp-on > .stamp { width: var(--stamp-sm-size, var(--tap)); max-width: none; }
216
+ /* --main: the same stuck stamp at the stamp's MAIN size, 96 × 120 (--stamp-size), to cover what it lies on (the owner's call, 2026-10-11: the home's note).
217
+ After the rule above, at the same weight: `main` is the one place a stuck stamp reads --stamp-size. */
218
+ .stamp-on--main > .stamp { width: var(--stamp-size, calc(var(--tap) * 2)); }
216
219
  .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) */
217
220
  .stamp-on--move { cursor: grab; touch-action: none; -webkit-user-select: none; user-select: none; transition: scale var(--motion-press) ease-out; }
218
221
  .stamp-on--move:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
219
222
  .stamp-on.is-held { cursor: grabbing; z-index: 1; scale: 1.06; } /* lifted off the paper while the finger holds it */
220
223
  @media (prefers-reduced-motion: reduce) { .stamp-on.is-held { scale: 1; } }
224
+
225
+ /* ---- the cross that takes a stamp off (`StampOn onRemove`): a small dark disc with an x on the stamp's top right corner ----
226
+ <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>
227
+ 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
228
+ (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
229
+ 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). */
230
+ .stamp-on__remove-disc {
231
+ display: grid;
232
+ place-items: center;
233
+ width: var(--stamp-on-remove-size, var(--sp-6));
234
+ aspect-ratio: 1;
235
+ border-radius: 50%;
236
+ background-color: var(--stamp-on-remove-bg, #141414);
237
+ box-shadow: 0 0 0 var(--sp-0) currentColor, 0 var(--sp-0) var(--sp-1) rgb(0 0 0 / 0.35);
238
+ }
239
+ .stamp-on__remove {
240
+ --_hit: max(var(--tap-floor), var(--stamp-on-remove-size, var(--sp-6)));
241
+ position: absolute;
242
+ top: 0;
243
+ right: 0;
244
+ translate: 50% -50%;
245
+ rotate: calc(var(--stamp-on-turn, 0) * -1deg);
246
+ display: grid;
247
+ place-items: center;
248
+ width: var(--_hit);
249
+ height: var(--_hit);
250
+ padding: 0;
251
+ border: 0;
252
+ border-radius: 50%;
253
+ background: none;
254
+ color: var(--stamp-on-remove-ink, #ffffff);
255
+ font: inherit;
256
+ -webkit-tap-highlight-color: transparent;
257
+ }
258
+ .stamp-on__remove-disc > .icon { width: 60%; height: 60%; }
259
+ @media (prefers-reduced-motion: no-preference) { .stamp-on__remove { animation: stamp-on-remove-in var(--motion-press) ease-out; } }
260
+ @keyframes stamp-on-remove-in { from { opacity: 0; scale: 0.6; } }
261
+
221
262
  /* --arrive: the stamp sticks ITSELF (StampOn `arrive`) — struck onto its place: shown large and faint, pressed a little past its
222
263
  size, then at rest. The component reads the animation's name to tell the app it has landed. Knob: --stamp-on-arrive-time */
223
264
  @media (prefers-reduced-motion: no-preference) {
package/css/sticky.css CHANGED
@@ -94,7 +94,7 @@
94
94
  box-shadow: calc(-1 * var(--sp-0)) calc(-1 * var(--sp-0)) var(--sp-1) rgb(0 0 0 / 0.08);
95
95
  }
96
96
  /* things stuck ON the note (a stamp, placed by hand): a layer over the whole paper. A tap between them still reaches the words under it.
97
- A stamp on a note is the stamp's small size whatever the note's (48 × 60, --stamp-sm-size: stamp.css), and may lie over the words. */
97
+ A stamp on a note is the stamp's small size whatever the note's (48 × 60, --stamp-sm-size: stamp.css; `StampOn main` is the main size), and may lie over the words. */
98
98
  .sticky__stuck { position: absolute; inset: 0; pointer-events: none; }
99
99
  .sticky--flat > .sticky__paper { clip-path: none; }
100
100
  .sticky--flat > .sticky__paper > .sticky__foot { padding-inline-end: 0; }
@@ -285,6 +285,49 @@
285
285
  .sticky-board > :nth-child(4n + 3) { rotate: var(--sticky-board-turn-3, 2.75deg); }
286
286
  .sticky-board > :nth-child(4n) { rotate: var(--sticky-board-turn-4, -1.75deg); }
287
287
 
288
+ /* ---- sticky pile: one to six notes lying one over another — the newest on top (Lh 2026-10-11) ----
289
+ <div class="sticky-pile sticky-pile--tap" style="--_n: 3">
290
+ <div class="sticky-pile__notes">
291
+ <div class="sticky">…</div><div class="sticky">…</div><div class="sticky">…</div>
292
+ <button class="sticky-pile__press" aria-label="Three new notes"></button>
293
+ </div>
294
+ </div>
295
+ The FIRST note is on top; each one under it is shifted a step right and down (--sticky-pile-step-x 24, --sticky-pile-step-y 8) so a strip of it peeks
296
+ out. All lie in ONE grid cell. The turn of each is the board's, by its place among the children (4n + 1 … 4n): the first, the top, is always −3°, whatever the count.
297
+ The notes under the top one draw no foot. The pile is centred in its column (the root is the column's width and a size container) and never
298
+ wider than it: the steps are shortened to what the column leaves. --_n is the count, written by the component: data, not a knob.
299
+ The room round the notes (--sticky-pile-pad) is for the turned corners and the shadow.
300
+ --tap: the whole pile is the button, a layer UNDER the notes (the last child): the notes let a touch through to it (pointer-events), a stamp on the top note keeps its own.
301
+ The press look is actions.css's (DIM); the ring is the button's own, round the whole pile. */
302
+ .sticky-pile {
303
+ --_size: var(--sticky-size, calc(var(--sp-12) * 3.5));
304
+ --_pad: var(--sticky-pile-pad, var(--sp-2));
305
+ --_step-x: min(var(--sticky-pile-step-x, var(--sp-6)), (100cqw - var(--_size) - 2 * var(--_pad)) / max(var(--_n) - 1, 1));
306
+ --_step-y: var(--sticky-pile-step-y, var(--sp-2));
307
+ container-type: inline-size;
308
+ display: grid;
309
+ justify-items: center;
310
+ }
311
+ .sticky-pile__notes {
312
+ position: relative;
313
+ display: grid;
314
+ grid-template: var(--_size) / var(--_size);
315
+ padding: var(--_pad) calc(var(--_pad) + (var(--_n) - 1) * var(--_step-x)) calc(var(--_pad) + (var(--_n) - 1) * var(--_step-y)) var(--_pad);
316
+ }
317
+ .sticky-pile__notes > .sticky { --_depth: 0; grid-area: 1 / 1; z-index: calc(7 - var(--_depth)); translate: calc(var(--_depth) * var(--_step-x)) calc(var(--_depth) * var(--_step-y)); }
318
+ .sticky-pile__notes > .sticky:nth-child(2) { --_depth: 1; }
319
+ .sticky-pile__notes > .sticky:nth-child(3) { --_depth: 2; }
320
+ .sticky-pile__notes > .sticky:nth-child(4) { --_depth: 3; }
321
+ .sticky-pile__notes > .sticky:nth-child(5) { --_depth: 4; }
322
+ .sticky-pile__notes > .sticky:nth-child(6) { --_depth: 5; }
323
+ .sticky-pile__notes > .sticky:nth-child(n + 2) .sticky__foot { display: none; }
324
+ .sticky-pile__notes > .sticky:nth-child(4n + 1) { rotate: var(--sticky-board-turn-1, -3deg); }
325
+ .sticky-pile__notes > .sticky:nth-child(4n + 2) { rotate: var(--sticky-board-turn-2, 1.5deg); }
326
+ .sticky-pile__notes > .sticky:nth-child(4n + 3) { rotate: var(--sticky-board-turn-3, 2.75deg); }
327
+ .sticky-pile__notes > .sticky:nth-child(4n) { rotate: var(--sticky-board-turn-4, -1.75deg); }
328
+ .sticky-pile__press { position: absolute; inset: 0; margin: 0; padding: 0; border: 0; appearance: none; background: none; -webkit-tap-highlight-color: transparent; }
329
+ .sticky-pile--tap .sticky-pile__notes > .sticky { pointer-events: none; }
330
+
288
331
  /* ---- sticky answer: the note you answer on — it waits at the screen's foot and comes to the top to be written on ----
289
332
  <div class="sticky-answer sticky-answer--writing">
290
333
  <div class="sticky-answer__scrim"></div>
package/dist/cardds.css CHANGED
@@ -2811,7 +2811,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2811
2811
  button's, the card itself is never focused or disabled. */
2812
2812
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
2813
2813
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
2814
- a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach) {
2814
+ a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove) {
2815
2815
  cursor: pointer;
2816
2816
  touch-action: manipulation;
2817
2817
  transition: scale var(--motion-press) ease-out;
@@ -2830,9 +2830,15 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2830
2830
  }
2831
2831
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
2832
2832
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
2833
- a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
2833
+ a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
2834
2834
  :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .feed-card--small, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
2835
2835
  .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
2836
+ /* a pile of notes pressed whole (Lh 2026-10-11): the button lies invisible UNDER the notes, so it is the notes that answer — DIM, as a card does (the group dims as one, the
2837
+ notes do not show through one another); the ring is the button's own, round the whole pile */
2838
+ .sticky-pile__press { cursor: pointer; touch-action: manipulation; }
2839
+ .sticky-pile__notes:has(> .sticky-pile__press) { transition: opacity var(--motion-press) ease-out; }
2840
+ .sticky-pile__notes:has(> .sticky-pile__press:active) { opacity: var(--press-dim); }
2841
+ .sticky-pile__press:focus-visible { outline: var(--focus); outline-offset: 0; }
2836
2842
  /* SHADE — what has something BEHIND it (a peeked sheet over the pile, a postcard's face over its back, a post letter's sheet over the other) must not turn see-through:
2837
2843
  it darkens a touch instead (--press-shade) */
2838
2844
  :where(.sheet-stack--tap > .card:not(.is-open), .postcard__turn, .post-letter__sheet:not(:has(.icon-btn:active))) { cursor: pointer; touch-action: manipulation; transition: filter var(--motion-press) ease-out; }
@@ -2840,17 +2846,17 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2840
2846
  /* the focus ring: on the control, or on the label that wraps a hidden input (chip, tile, file button, a row that is a label) */
2841
2847
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
2842
2848
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
2843
- a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach):focus-visible,
2849
+ a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove):focus-visible,
2844
2850
  :where(.chip--pick, .tile--pick, label.btn, label.row, .toggle-field):has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
2845
2851
  :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .feed-card--small, .toggle-field, .post-box__edge):not(label, .toggle-field):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
2846
2852
  /* disabled: no pointer anywhere; a control fades (a link and a row keep their own muted look) */
2847
2853
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
2848
2854
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
2849
- a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge):is(:disabled, [aria-disabled="true"]),
2855
+ a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove, a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge):is(:disabled, [aria-disabled="true"]),
2850
2856
  :where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn, label.row):has(:disabled) { cursor: default; }
2851
2857
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
2852
2858
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
2853
- a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach):not(.link):is(:disabled, [aria-disabled="true"]),
2859
+ a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach, .stamp-on__remove):not(.link):is(:disabled, [aria-disabled="true"]),
2854
2860
  :where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn):has(:disabled) { opacity: var(--off); }
2855
2861
  .sheet-stack--tap > .card > .card__head > :is(h1, h2, h3):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); } /* the keyboard's way into a stack sheet */
2856
2862
  @media (prefers-reduced-motion: reduce) { :root { --press-scale: 1; } } /* the give goes; the dim, the shade and the focus ring stay */
@@ -3862,7 +3868,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3862
3868
  box-shadow: calc(-1 * var(--sp-0)) calc(-1 * var(--sp-0)) var(--sp-1) rgb(0 0 0 / 0.08);
3863
3869
  }
3864
3870
  /* things stuck ON the note (a stamp, placed by hand): a layer over the whole paper. A tap between them still reaches the words under it.
3865
- A stamp on a note is the stamp's small size whatever the note's (48 × 60, --stamp-sm-size: stamp.css), and may lie over the words. */
3871
+ A stamp on a note is the stamp's small size whatever the note's (48 × 60, --stamp-sm-size: stamp.css; `StampOn main` is the main size), and may lie over the words. */
3866
3872
  .sticky__stuck { position: absolute; inset: 0; pointer-events: none; }
3867
3873
  .sticky--flat > .sticky__paper { clip-path: none; }
3868
3874
  .sticky--flat > .sticky__paper > .sticky__foot { padding-inline-end: 0; }
@@ -4053,6 +4059,49 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
4053
4059
  .sticky-board > :nth-child(4n + 3) { rotate: var(--sticky-board-turn-3, 2.75deg); }
4054
4060
  .sticky-board > :nth-child(4n) { rotate: var(--sticky-board-turn-4, -1.75deg); }
4055
4061
 
4062
+ /* ---- sticky pile: one to six notes lying one over another — the newest on top (Lh 2026-10-11) ----
4063
+ <div class="sticky-pile sticky-pile--tap" style="--_n: 3">
4064
+ <div class="sticky-pile__notes">
4065
+ <div class="sticky">…</div><div class="sticky">…</div><div class="sticky">…</div>
4066
+ <button class="sticky-pile__press" aria-label="Three new notes"></button>
4067
+ </div>
4068
+ </div>
4069
+ The FIRST note is on top; each one under it is shifted a step right and down (--sticky-pile-step-x 24, --sticky-pile-step-y 8) so a strip of it peeks
4070
+ out. All lie in ONE grid cell. The turn of each is the board's, by its place among the children (4n + 1 … 4n): the first, the top, is always −3°, whatever the count.
4071
+ The notes under the top one draw no foot. The pile is centred in its column (the root is the column's width and a size container) and never
4072
+ wider than it: the steps are shortened to what the column leaves. --_n is the count, written by the component: data, not a knob.
4073
+ The room round the notes (--sticky-pile-pad) is for the turned corners and the shadow.
4074
+ --tap: the whole pile is the button, a layer UNDER the notes (the last child): the notes let a touch through to it (pointer-events), a stamp on the top note keeps its own.
4075
+ The press look is actions.css's (DIM); the ring is the button's own, round the whole pile. */
4076
+ .sticky-pile {
4077
+ --_size: var(--sticky-size, calc(var(--sp-12) * 3.5));
4078
+ --_pad: var(--sticky-pile-pad, var(--sp-2));
4079
+ --_step-x: min(var(--sticky-pile-step-x, var(--sp-6)), (100cqw - var(--_size) - 2 * var(--_pad)) / max(var(--_n) - 1, 1));
4080
+ --_step-y: var(--sticky-pile-step-y, var(--sp-2));
4081
+ container-type: inline-size;
4082
+ display: grid;
4083
+ justify-items: center;
4084
+ }
4085
+ .sticky-pile__notes {
4086
+ position: relative;
4087
+ display: grid;
4088
+ grid-template: var(--_size) / var(--_size);
4089
+ padding: var(--_pad) calc(var(--_pad) + (var(--_n) - 1) * var(--_step-x)) calc(var(--_pad) + (var(--_n) - 1) * var(--_step-y)) var(--_pad);
4090
+ }
4091
+ .sticky-pile__notes > .sticky { --_depth: 0; grid-area: 1 / 1; z-index: calc(7 - var(--_depth)); translate: calc(var(--_depth) * var(--_step-x)) calc(var(--_depth) * var(--_step-y)); }
4092
+ .sticky-pile__notes > .sticky:nth-child(2) { --_depth: 1; }
4093
+ .sticky-pile__notes > .sticky:nth-child(3) { --_depth: 2; }
4094
+ .sticky-pile__notes > .sticky:nth-child(4) { --_depth: 3; }
4095
+ .sticky-pile__notes > .sticky:nth-child(5) { --_depth: 4; }
4096
+ .sticky-pile__notes > .sticky:nth-child(6) { --_depth: 5; }
4097
+ .sticky-pile__notes > .sticky:nth-child(n + 2) .sticky__foot { display: none; }
4098
+ .sticky-pile__notes > .sticky:nth-child(4n + 1) { rotate: var(--sticky-board-turn-1, -3deg); }
4099
+ .sticky-pile__notes > .sticky:nth-child(4n + 2) { rotate: var(--sticky-board-turn-2, 1.5deg); }
4100
+ .sticky-pile__notes > .sticky:nth-child(4n + 3) { rotate: var(--sticky-board-turn-3, 2.75deg); }
4101
+ .sticky-pile__notes > .sticky:nth-child(4n) { rotate: var(--sticky-board-turn-4, -1.75deg); }
4102
+ .sticky-pile__press { position: absolute; inset: 0; margin: 0; padding: 0; border: 0; appearance: none; background: none; -webkit-tap-highlight-color: transparent; }
4103
+ .sticky-pile--tap .sticky-pile__notes > .sticky { pointer-events: none; }
4104
+
4056
4105
  /* ---- sticky answer: the note you answer on — it waits at the screen's foot and comes to the top to be written on ----
4057
4106
  <div class="sticky-answer sticky-answer--writing">
4058
4107
  <div class="sticky-answer__scrim"></div>
@@ -4316,11 +4365,52 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
4316
4365
  }
4317
4366
  /* 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 */
4318
4367
  .stamp-on > .stamp { width: var(--stamp-sm-size, var(--tap)); max-width: none; }
4368
+ /* --main: the same stuck stamp at the stamp's MAIN size, 96 × 120 (--stamp-size), to cover what it lies on (the owner's call, 2026-10-11: the home's note).
4369
+ After the rule above, at the same weight: `main` is the one place a stuck stamp reads --stamp-size. */
4370
+ .stamp-on--main > .stamp { width: var(--stamp-size, calc(var(--tap) * 2)); }
4319
4371
  .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) */
4320
4372
  .stamp-on--move { cursor: grab; touch-action: none; -webkit-user-select: none; user-select: none; transition: scale var(--motion-press) ease-out; }
4321
4373
  .stamp-on--move:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
4322
4374
  .stamp-on.is-held { cursor: grabbing; z-index: 1; scale: 1.06; } /* lifted off the paper while the finger holds it */
4323
4375
  @media (prefers-reduced-motion: reduce) { .stamp-on.is-held { scale: 1; } }
4376
+
4377
+ /* ---- the cross that takes a stamp off (`StampOn onRemove`): a small dark disc with an x on the stamp's top right corner ----
4378
+ <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>
4379
+ 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
4380
+ (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
4381
+ 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). */
4382
+ .stamp-on__remove-disc {
4383
+ display: grid;
4384
+ place-items: center;
4385
+ width: var(--stamp-on-remove-size, var(--sp-6));
4386
+ aspect-ratio: 1;
4387
+ border-radius: 50%;
4388
+ background-color: var(--stamp-on-remove-bg, #141414);
4389
+ box-shadow: 0 0 0 var(--sp-0) currentColor, 0 var(--sp-0) var(--sp-1) rgb(0 0 0 / 0.35);
4390
+ }
4391
+ .stamp-on__remove {
4392
+ --_hit: max(var(--tap-floor), var(--stamp-on-remove-size, var(--sp-6)));
4393
+ position: absolute;
4394
+ top: 0;
4395
+ right: 0;
4396
+ translate: 50% -50%;
4397
+ rotate: calc(var(--stamp-on-turn, 0) * -1deg);
4398
+ display: grid;
4399
+ place-items: center;
4400
+ width: var(--_hit);
4401
+ height: var(--_hit);
4402
+ padding: 0;
4403
+ border: 0;
4404
+ border-radius: 50%;
4405
+ background: none;
4406
+ color: var(--stamp-on-remove-ink, #ffffff);
4407
+ font: inherit;
4408
+ -webkit-tap-highlight-color: transparent;
4409
+ }
4410
+ .stamp-on__remove-disc > .icon { width: 60%; height: 60%; }
4411
+ @media (prefers-reduced-motion: no-preference) { .stamp-on__remove { animation: stamp-on-remove-in var(--motion-press) ease-out; } }
4412
+ @keyframes stamp-on-remove-in { from { opacity: 0; scale: 0.6; } }
4413
+
4324
4414
  /* --arrive: the stamp sticks ITSELF (StampOn `arrive`) — struck onto its place: shown large and faint, pressed a little past its
4325
4415
  size, then at rest. The component reads the animation's name to tell the app it has landed. Knob: --stamp-on-arrive-time */
4326
4416
  @media (prefers-reduced-motion: no-preference) {