@usableapp/cardds 0.7.84 → 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,27 @@
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
+
16
+ ## 0.7.85 — 2026-10-11
17
+
18
+ ### Added
19
+ - **`StampOn arrive` + `onArrived`: a stamp that sticks itself** (Lh 2026-10-11) — as the `StampOn` comes onto the screen it is struck onto its place (shown large, pressed down to its size), and `onArrived` tells the app when it has landed; with reduced motion it is there at once and `onArrived` still comes. Nobody picks the stamp or presses to stick it: the app shows it when its own move begins (a send) and does what follows in `onArrived`. The surface is any positioned box, a `Card` and a `Sheet` among them. Knob `--stamp-on-arrive-time`. Story `StampOn · Arrives`.
20
+ - **Story `Home · AskSheet`: a task in one sheet over the home** — `SheetStage over dim` holding a `Sheet`, as the `Home`'s children, over a flooded sheet; two `SheetSteps` inside; the sheet is one layer of the history (`useLayer`). Nothing new in the components: the story shows the composition.
21
+ - **Story `CardFoot · CardFootFourAnswers`: four answers on one card** — one word button and an `IconBtnGroup form` of three picture buttons. Four word buttons on one line do not fit a phone (67 to 79 wide each, the words break). An answer already given is `aria-disabled` with its name changed, so it fades, stays, and is still read.
22
+
23
+ ### Fixed
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
+
5
26
  ## 0.7.84 — 2026-10-11
6
27
 
7
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
 
@@ -650,7 +660,18 @@ it up. cardds draws the paper; the pictures, their words and which stamp a membe
650
660
  `turn` its angle in degrees. With `onAtChange` the member drags it (the arrow keys move it too) and it asks with the
651
661
  new place when the finger lets go; without it the stamp is still. The surface is the nearest positioned box:
652
662
  `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.
663
+ on a note it may lie over the words. With `arrive` the stamp sticks itself: as it comes onto the screen it is struck
664
+ onto its place, and `onArrived` tells the app when it has landed (at once where motion is off), so the app does what
665
+ follows. A `Card` and a `Sheet` are positioned boxes, so a `StampOn` written inside one lies on it.
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.
654
675
 
655
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
656
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
@@ -204,6 +204,7 @@
204
204
 
205
205
  .chip-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
206
206
  .chip-row.bleed { flex-wrap: nowrap; } /* actions.css loads after card.css — the strip stays one line */
207
+ .chip-row.bleed > .chip { flex: none; white-space: nowrap; } /* a chip of the strip keeps its words on one line at its own width: the strip scrolls, a chip never shrinks to fit it */
207
208
 
208
209
  /* ---- dropdown: the standard for every select — a pill trigger + .menu popover ----
209
210
  <div class="dropdown"><button class="dropdown__btn" popovertarget="f1"><span class="dropdown__label">this week</span><svg class="icon icon--xs"><use href="icons.svg#chevron-down"/></svg></button>
@@ -381,7 +382,7 @@
381
382
  button's, the card itself is never focused or disabled. */
382
383
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
383
384
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
384
- 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) {
385
386
  cursor: pointer;
386
387
  touch-action: manipulation;
387
388
  transition: scale var(--motion-press) ease-out;
@@ -400,9 +401,15 @@
400
401
  }
401
402
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
402
403
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
403
- 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); }
404
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); }
405
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; }
406
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:
407
414
  it darkens a touch instead (--press-shade) */
408
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; }
@@ -410,17 +417,17 @@
410
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) */
411
418
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
412
419
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
413
- 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,
414
421
  :where(.chip--pick, .tile--pick, label.btn, label.row, .toggle-field):has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
415
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); }
416
423
  /* disabled: no pointer anywhere; a control fades (a link and a row keep their own muted look) */
417
424
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
418
425
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
419
- 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"]),
420
427
  :where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn, label.row):has(:disabled) { cursor: default; }
421
428
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
422
429
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
423
- 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"]),
424
431
  :where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn):has(:disabled) { opacity: var(--off); }
425
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 */
426
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,8 +213,55 @@
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
+
262
+ /* --arrive: the stamp sticks ITSELF (StampOn `arrive`) — struck onto its place: shown large and faint, pressed a little past its
263
+ size, then at rest. The component reads the animation's name to tell the app it has landed. Knob: --stamp-on-arrive-time */
264
+ @media (prefers-reduced-motion: no-preference) {
265
+ .stamp-on--arrive { animation: stamp-on-strike var(--stamp-on-arrive-time, var(--motion-sheet)) ease-in both; }
266
+ }
267
+ @keyframes stamp-on-strike { from { scale: 2; opacity: 0; } 60% { scale: 0.92; opacity: 1; } }
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
@@ -2633,6 +2633,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2633
2633
 
2634
2634
  .chip-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
2635
2635
  .chip-row.bleed { flex-wrap: nowrap; } /* actions.css loads after card.css — the strip stays one line */
2636
+ .chip-row.bleed > .chip { flex: none; white-space: nowrap; } /* a chip of the strip keeps its words on one line at its own width: the strip scrolls, a chip never shrinks to fit it */
2636
2637
 
2637
2638
  /* ---- dropdown: the standard for every select — a pill trigger + .menu popover ----
2638
2639
  <div class="dropdown"><button class="dropdown__btn" popovertarget="f1"><span class="dropdown__label">this week</span><svg class="icon icon--xs"><use href="icons.svg#chevron-down"/></svg></button>
@@ -2810,7 +2811,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2810
2811
  button's, the card itself is never focused or disabled. */
2811
2812
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
2812
2813
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
2813
- 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) {
2814
2815
  cursor: pointer;
2815
2816
  touch-action: manipulation;
2816
2817
  transition: scale var(--motion-press) ease-out;
@@ -2829,9 +2830,15 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2829
2830
  }
2830
2831
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
2831
2832
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
2832
- 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); }
2833
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); }
2834
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; }
2835
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:
2836
2843
  it darkens a touch instead (--press-shade) */
2837
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; }
@@ -2839,17 +2846,17 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2839
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) */
2840
2847
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
2841
2848
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
2842
- 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,
2843
2850
  :where(.chip--pick, .tile--pick, label.btn, label.row, .toggle-field):has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
2844
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); }
2845
2852
  /* disabled: no pointer anywhere; a control fades (a link and a row keep their own muted look) */
2846
2853
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
2847
2854
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
2848
- 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"]),
2849
2856
  :where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn, label.row):has(:disabled) { cursor: default; }
2850
2857
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
2851
2858
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
2852
- 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"]),
2853
2860
  :where(.chip--pick, .tile--pick, .mood__opt, .toggle-field, label.btn):has(:disabled) { opacity: var(--off); }
2854
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 */
2855
2862
  @media (prefers-reduced-motion: reduce) { :root { --press-scale: 1; } } /* the give goes; the dim, the shade and the focus ring stay */
@@ -3861,7 +3868,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3861
3868
  box-shadow: calc(-1 * var(--sp-0)) calc(-1 * var(--sp-0)) var(--sp-1) rgb(0 0 0 / 0.08);
3862
3869
  }
3863
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.
3864
- 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. */
3865
3872
  .sticky__stuck { position: absolute; inset: 0; pointer-events: none; }
3866
3873
  .sticky--flat > .sticky__paper { clip-path: none; }
3867
3874
  .sticky--flat > .sticky__paper > .sticky__foot { padding-inline-end: 0; }
@@ -4052,6 +4059,49 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
4052
4059
  .sticky-board > :nth-child(4n + 3) { rotate: var(--sticky-board-turn-3, 2.75deg); }
4053
4060
  .sticky-board > :nth-child(4n) { rotate: var(--sticky-board-turn-4, -1.75deg); }
4054
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
+
4055
4105
  /* ---- sticky answer: the note you answer on — it waits at the screen's foot and comes to the top to be written on ----
4056
4106
  <div class="sticky-answer sticky-answer--writing">
4057
4107
  <div class="sticky-answer__scrim"></div>
@@ -4315,12 +4365,59 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
4315
4365
  }
4316
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 */
4317
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)); }
4318
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) */
4319
4372
  .stamp-on--move { cursor: grab; touch-action: none; -webkit-user-select: none; user-select: none; transition: scale var(--motion-press) ease-out; }
4320
4373
  .stamp-on--move:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
4321
4374
  .stamp-on.is-held { cursor: grabbing; z-index: 1; scale: 1.06; } /* lifted off the paper while the finger holds it */
4322
4375
  @media (prefers-reduced-motion: reduce) { .stamp-on.is-held { scale: 1; } }
4323
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
+
4414
+ /* --arrive: the stamp sticks ITSELF (StampOn `arrive`) — struck onto its place: shown large and faint, pressed a little past its
4415
+ size, then at rest. The component reads the animation's name to tell the app it has landed. Knob: --stamp-on-arrive-time */
4416
+ @media (prefers-reduced-motion: no-preference) {
4417
+ .stamp-on--arrive { animation: stamp-on-strike var(--stamp-on-arrive-time, var(--motion-sheet)) ease-in both; }
4418
+ }
4419
+ @keyframes stamp-on-strike { from { scale: 2; opacity: 0; } 60% { scale: 0.92; opacity: 1; } }
4420
+
4324
4421
  /* ---- css/post.css — the post: postmark, a postcard read (letter), written (desk), kept (box) ---- */
4325
4422
  /* cardds — the post: a postcard read (post-letter), written (post-desk) and kept (post-box), the postmark, the card stock.
4326
4423
  Not the `Postcard` of media.css (a member's cover card that flips) — nothing here flips, and no class is shared with it.