@usableapp/cardds 0.7.86 → 0.7.88

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/dist/cardds.css CHANGED
@@ -307,6 +307,12 @@
307
307
  @property --sheet-half { syntax: "<number>"; inherits: true; initial-value: 0.5; }
308
308
  @property --sheet-3q { syntax: "<number>"; inherits: true; initial-value: 0.25; }
309
309
 
310
+ /* the ring that turns while waiting: Spinner, FormNote busy */
311
+ @keyframes turn { to { rotate: 1turn; } }
312
+ /* a mark struck onto paper — shown large and faint, pressed a little past its size, then at rest: StampOn `arrive`, the post desk's
313
+ postmark (Lh 2026-10-12: one strike for both; the postmark used to slide in from up-left, turning). Both read --stamp-on-arrive-time */
314
+ @keyframes strike { from { scale: 2; opacity: 0; } 60% { scale: 0.92; opacity: 1; } }
315
+
310
316
  /* ---- css/palette.css — color settings — the file you tune ---- */
311
317
  /* ============================================================
312
318
  cardds/palette.css — COLOR SETTINGS. The only file you tune.
@@ -711,6 +717,15 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
711
717
  screens. Children shrink to the track; they never size the card. */
712
718
  .card > * { min-width: 0; }
713
719
 
720
+ /* LEAVE — a card that goes: up and out with a small turn, as one piece (translate and rotate only, so nothing round it moves). The post desk's
721
+ card once `sent` and a `Card` with `leave` read the same two knobs; the time is --motion-sheet. `.card--leaver` is a card whose app may ask it
722
+ to go (it holds the transition, so it comes back as it went); `.card--away` is one that has been asked. A knob of the time would read dead:
723
+ the knob walk switches motion off */
724
+ :is(.card--leaver, .post-desk__card) { transition: translate var(--motion-sheet) ease, rotate var(--motion-sheet) ease; }
725
+ :is(.card--away, .post-desk--sent .post-desk__card) { --_y: calc(var(--leave-rise, 160%) * -1); rotate: var(--leave-turn, -5deg); }
726
+ .card--away { translate: 0 var(--_y); }
727
+ @media (prefers-reduced-motion: reduce) { :is(.card--leaver, .post-desk__card) { transition: none; } }
728
+
714
729
  .card--2 { --card-bg: var(--card-2-bg); --card-ink: var(--card-2-ink); --card-muted: var(--card-2-muted); }
715
730
  /* dark/accent slot: accent-toned controls would vanish — the slot re-points
716
731
  the accent tokens, so every accent consumer (btn--primary, chip--on,
@@ -898,12 +913,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
898
913
  actions.css) and scrolls up like any page — nothing is capped, nothing
899
914
  shrinks, no scroll box inside a box.
900
915
  So on a .screen that is NOT --fill the group is in flow: the column is at
901
- least one phone tall, the group takes the room after the top bar and
916
+ least one phone tall (step.css), the group takes the room after the top bar and
902
917
  centres its children in it. The screen's own inset stays the inset.
903
918
  <div class="screen"><header class="topbar">…</header>
904
919
  <div class="screen__centre"><article class="card">…</article><div class="pager__at">…</div></div>
905
920
  </div><div class="action-bar">…</div> */
906
- .screen:not(.screen--fill):has(> .screen__centre) { min-height: 100dvh; display: flex; flex-direction: column; }
907
921
  .screen:not(.screen--fill) > .screen__centre { flex: 1; padding-inline: 0; align-content: center; }
908
922
 
909
923
  /* ---- the card that changes slides: out one side, the next in from the other ----
@@ -1387,7 +1401,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1387
1401
  }
1388
1402
  /* a card standing in this room is FLAT (Lh 2026-10-09): the room clips at its own edges, and a card's shadow cut there drew a straight
1389
1403
  line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
1390
- :is([data-palette], [data-finish="lit"]) .sheet-stack__base .card { box-shadow: none; }
1404
+ :is([data-palette], [data-finish]) .sheet-stack__base .card { box-shadow: none; }
1391
1405
 
1392
1406
  /* ============================================================
1393
1407
  .sheet-stack--flood — a closed stack whose open sheet FLOODS the screen: it rises over the top bar and fills the whole screen
@@ -2207,6 +2221,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2207
2221
  padding-bottom: var(--bottom-h, 0rem); /* exactly the bottom layer (measured by Screen): a page scrolls ONLY when the layer would cover its words, and then just far enough to clear them — at 100% nothing scrolls (Lh 2026-09-23) */
2208
2222
  }
2209
2223
 
2224
+ /* a SCROLLING (not fill) screen becomes a column at least one phone tall when it holds a bottom or a centre:
2225
+ an .action-bar as its last child (Lh 2026-09-24, 0.7.0: the bar is the column's last row — at the foot when the words are short,
2226
+ sticky to the viewport's foot while they scroll; actions.css) or a .screen__centre (Lh 2026-09-12: the group is in flow, takes the
2227
+ room after the top bar and centres its children in it; a long one makes the page longer and runs under the bar; card.css) */
2228
+ .screen:not(.screen--fill):has(> .action-bar:last-child, > .screen__centre) { display: flex; flex-direction: column; min-height: 100dvh; }
2229
+
2210
2230
  /* ============================================================
2211
2231
  TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
2212
2232
  in a .screen--fill the content runs to the screen's bottom edge, and the bar lies over it at the foot — it covers what is
@@ -2276,10 +2296,15 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2276
2296
  object-fit: cover;
2277
2297
  }
2278
2298
  /* an EMPTY STATE is a component in the content box too (Lh 2026-09-23 — it no longer floats in a Centre over the screen): alone in a
2279
- Step it stands at the human centre of the content, 40% down (two parts of room above it, three below) */
2280
- .step:has(> .empty-state)::before { content: ""; flex: 2 1 0; }
2281
- .step:has(> .empty-state)::after { content: ""; flex: 3 1 0; }
2299
+ Step it stands at the human centre of the content, 40% down (two parts of room above it, three below). --empty-state-at is the share of the
2300
+ free room that stands above it (a theme's 0.2 puts it a fifth down); a sheet's body holds an only EmptyState by the same share, but defaults to 0: at the top, as it always stood */
2301
+ .step:has(> .empty-state)::before { content: ""; flex: calc(var(--empty-state-at, 0.4) * 5) 1 0; }
2302
+ .step:has(> .empty-state)::after { content: ""; flex: calc(5 - var(--empty-state-at, 0.4) * 5) 1 0; }
2282
2303
  .step > .empty-state { flex: none; }
2304
+ .sheet__body:has(> .empty-state:only-child) { display: flex; flex-direction: column; gap: 0; }
2305
+ .sheet__body:has(> .empty-state:only-child)::before { content: ""; flex: calc(var(--empty-state-at, 0) * 5) 1 0; }
2306
+ .sheet__body:has(> .empty-state:only-child)::after { content: ""; flex: calc(5 - var(--empty-state-at, 0) * 5) 1 0; }
2307
+ .sheet__body > .empty-state:only-child { flex: none; }
2283
2308
 
2284
2309
  /* ---- css/forms.css — outlined fields, add-row ---- */
2285
2310
  /* ============================================================
@@ -2362,8 +2387,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2362
2387
  .form-note { display: flex; align-items: center; justify-content: center; gap: var(--sp-2); margin: 0; font: var(--form-note-font, var(--type-caption)); color: var(--form-note-ink, var(--card-muted)); text-align: center; }
2363
2388
  .form-note > .icon { flex: none; }
2364
2389
  .form-note--error { color: var(--form-note-error-ink, var(--signal-error)); }
2365
- @media (prefers-reduced-motion: no-preference) { .form-note--busy > .icon { animation: form-note-turn 0.9s linear infinite; } }
2366
- @keyframes form-note-turn { to { rotate: 1turn; } }
2390
+ @media (prefers-reduced-motion: no-preference) { .form-note--busy > .icon { animation: turn 0.9s linear infinite; } } /* `turn`: tokens.css */
2367
2391
 
2368
2392
  /* ---- css/journey.css — route, tile badge, note row ---- */
2369
2393
  /* ============================================================
@@ -2702,10 +2726,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2702
2726
  display: flex;
2703
2727
  gap: var(--sp-2);
2704
2728
  }
2705
- /* a bar INSIDE a scrolling (not fill) screen (Lh 2026-09-24, 0.7.0): the screen is a column at least one phone tall, the bar its
2729
+ /* a bar INSIDE a scrolling (not fill) screen (Lh 2026-09-24, 0.7.0): the screen is a column at least one phone tall (step.css), the bar its
2706
2730
  last row — at the foot when the words are short, and sticky to the viewport's foot (above the keyboard) while they scroll;
2707
2731
  it takes the room it needs in the column, so nothing is added to the content for it */
2708
- .screen:not(.screen--fill):has(> .action-bar:last-child) { display: flex; flex-direction: column; min-height: 100dvh; }
2709
2732
  .screen:not(.screen--fill) > .action-bar:last-child {
2710
2733
  position: sticky; bottom: var(--kb, 0rem);
2711
2734
  max-width: none;
@@ -3121,8 +3144,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3121
3144
  The ring turns; words beside it are optional and take the caption's look. Still when motion is reduced. */
3122
3145
  .spinner { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2); vertical-align: middle; font: var(--type-caption); color: var(--spinner-ink, currentColor); }
3123
3146
  .spinner > .icon { flex: none; }
3124
- @media (prefers-reduced-motion: no-preference) { .spinner > .icon { animation: spinner-turn 0.9s linear infinite; } }
3125
- @keyframes spinner-turn { to { rotate: 1turn; } }
3147
+ @media (prefers-reduced-motion: no-preference) { .spinner > .icon { animation: turn 0.9s linear infinite; } } /* `turn`: tokens.css */
3126
3148
 
3127
3149
  /* ---- progress: one straight line, the done share solid, the rest faint (Lh 2026-09-22) — a voice note's
3128
3150
  timeline, a download, a step count as a line. <div class="progress" role="progressbar" style="--progress-at: 0.4"></div>
@@ -3309,8 +3331,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3309
3331
  .chip-grid > .chip { justify-self: stretch; justify-content: center; min-height: var(--btn-h); padding-inline: var(--sp-2); }
3310
3332
  .chip--pick { position: relative; } /* the input covers the whole chip, on a row as in a grid */
3311
3333
  /* the hidden input, the same shape wherever a native radio/checkbox drives a picked look:
3312
- a chip, a mood dot, a tile (media.css) */
3313
- :is(.chip--pick, .mood__opt, .tile--pick) > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: inherit; }
3334
+ a chip, a mood dot, a tile (media.css), a sticky's tone swatch (sticky.css) — the native input lies over its whole box, unseen */
3335
+ :is(.chip--pick, .mood__opt, .tile--pick, .sticky-pick__swatch) > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: inherit; }
3314
3336
  :is(.chip--pick, .tile--pick):has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
3315
3337
  /* --sign: the trailing + that turns 45° into an × when picked — one mask, one rotate, no swap (Lh 2026-09-16) */
3316
3338
  .chip--sign { padding-right: var(--sp-2); }
@@ -3956,7 +3978,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3956
3978
  box-shadow: inset 0 0 0 var(--border-w) var(--ink-tint); /* a pale tone still has an edge on a pale ground */
3957
3979
  cursor: pointer;
3958
3980
  }
3959
- .sticky-pick__swatch > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: inherit; }
3981
+ /* its hidden input: the picks' one rule, choice.css */
3960
3982
  .sticky-pick__swatch:has(> :checked) { outline: var(--rule-w) solid currentColor; outline-offset: var(--sp-0); }
3961
3983
  .sticky-pick__swatch:has(> :focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
3962
3984
 
@@ -4411,12 +4433,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
4411
4433
  @media (prefers-reduced-motion: no-preference) { .stamp-on__remove { animation: stamp-on-remove-in var(--motion-press) ease-out; } }
4412
4434
  @keyframes stamp-on-remove-in { from { opacity: 0; scale: 0.6; } }
4413
4435
 
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 */
4436
+ /* --arrive: the stamp sticks ITSELF (StampOn `arrive`) — struck onto its place (`strike`: tokens.css). The component reads the
4437
+ animation's name to tell the app it has landed. Knob: --stamp-on-arrive-time */
4416
4438
  @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; }
4439
+ .stamp-on--arrive { animation: strike var(--stamp-on-arrive-time, var(--motion-sheet)) ease-in both; }
4418
4440
  }
4419
- @keyframes stamp-on-strike { from { scale: 2; opacity: 0; } 60% { scale: 0.92; opacity: 1; } }
4420
4441
 
4421
4442
  /* ---- css/post.css — the post: postmark, a postcard read (letter), written (desk), kept (box) ---- */
4422
4443
  /* cardds — the post: a postcard read (post-letter), written (post-desk) and kept (post-box), the postmark, the card stock.
@@ -4620,10 +4641,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
4620
4641
  width: calc(var(--_p) * 2);
4621
4642
  aspect-ratio: 3 / 2;
4622
4643
  translate: var(--_x) var(--_y);
4623
- transition: translate var(--motion-sheet) ease, rotate var(--motion-sheet) ease;
4624
4644
  }
4625
4645
  .post-desk--address .post-desk__card { --_x: calc(100cqi - var(--_p) * 2); }
4626
- .post-desk--sent .post-desk__card { --_y: -160%; rotate: -5deg; }
4646
+ /* sent: the card leaves by card.css LEAVE, as a `Card` with `leave` does */
4627
4647
  .post-desk__half { min-width: 0; min-height: 0; padding: var(--post-desk-pad, var(--sp-5)); display: flex; flex-direction: column; }
4628
4648
  .post-desk__half--address { border-inline-start: var(--rule-w) solid var(--_rule); gap: var(--sp-4); }
4629
4649
  .post-desk__write {
@@ -4655,10 +4675,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
4655
4675
  .post-desk__slot { border: 0; background: transparent; color: inherit; }
4656
4676
  @media (prefers-reduced-motion: no-preference) {
4657
4677
  .post-desk__slot > .stamp { animation: post-desk-stick var(--motion-quick) ease-out both; }
4658
- .post-desk .postmark { animation: post-desk-strike var(--motion-press) ease-in both; }
4678
+ .post-desk .postmark { animation: strike var(--stamp-on-arrive-time, var(--motion-sheet)) ease-in both; } /* struck as a StampOn is (`strike`: tokens.css), in the same time */
4659
4679
  }
4660
4680
  @keyframes post-desk-stick { from { translate: 0 calc(-1 * var(--sp-8)); rotate: 14deg; opacity: 0; } }
4661
- @keyframes post-desk-strike { from { translate: calc(-1 * var(--sp-3)) calc(-1 * var(--sp-6)); rotate: -24deg; opacity: 0; } }
4662
4681
  /* the other half's showing edge brings it across: a tap-wide strip out to the screen's edge */
4663
4682
  .post-desk__reach { position: absolute; inset-block: 0; width: var(--tap); border: 0; background: transparent; }
4664
4683
  .post-desk__reach--address { inset-inline-end: calc(-1 * var(--screen-pad)); }
@@ -4693,7 +4712,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
4693
4712
  .post-desk__note { margin: 0; font: var(--type-caption); color: var(--post-desk-note-ink, var(--on-surface-muted)); min-height: 1.35em; }
4694
4713
  /* what stands where the card was, once it has left */
4695
4714
  .post-desk__gone { position: absolute; inset: 0; display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; gap: var(--sp-2); }
4696
- @media (prefers-reduced-motion: reduce) { .post-desk__card { transition: none; } }
4697
4715
 
4698
4716
  /* ============================================================
4699
4717
  post-box — KEEPING many. The cards whole and landscape, standing one behind another; each shows its top edge: who, where,
@@ -5402,19 +5420,37 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
5402
5420
 
5403
5421
  /* ---- css/finish.css — light, shadow, edges — only under data-finish="lit" ---- */
5404
5422
  /* ============================================================
5405
- cardds/finish.css — FINISH: light, shadow and edges. A style of its own (Lh 2026-10-02: the plain
5406
- look read flat, "not finished"), apart from colour: palette.css says WHICH colours, this says how
5407
- the material is lit. Apply with <html data-finish="lit"> beside any palette (or none, or "own").
5408
- No attribute = nothing here applies — the flat look, unchanged.
5423
+ cardds/finish.css — FINISH: the finish of a card's paper. A style of its own (Lh 2026-10-02: the plain
5424
+ look read flat, "not finished"), apart from colour: palette.css says WHICH colours, this says what the
5425
+ material is and how it is lit. Apply with <html data-finish="…"> (or on any ancestor) beside any palette,
5426
+ or none, or "own". No attribute = nothing here applies — the flat look, unchanged.
5427
+
5428
+ FINISHES — four values of the one attribute (Lh 2026-10-12, from the timebank home with each injected:
5429
+ four finishes, variations of one concept, never new pieces; teeth · tape · ticket · slab were rejected):
5430
+ lit = lit from above: a lit top edge, a hairline ring and a layered shadow that grows with height; filled buttons stand up and go down under the finger; fields and pin cells are sunk.
5431
+ print = printed paper: an ink line in the card's own ink, a hard misregistered offset instead of a blurred shadow, paper grain over the card, a dotted rule under the head as on a printed form; information inside stays flat.
5432
+ sticker = a die-cut sticker: a white cut line round the card, a short hard drop, a slight tilt on a lone card, buttons with a bottom edge that press down.
5433
+ ink = a poster / comic print: a thick line in the surface's ink round every boundary and control, a hard offset in the same ink.
5434
+ KNOBS (set in a theme, as any knob; docs/knobs.md lists them):
5435
+ print --print-off (the misregistration: how far the second pass missed) · --print-grain (the grain's opacity; a ::after, so the knob walk cannot probe it)
5436
+ sticker --sticker-cut (the paper left round the print) · --sticker-tilt (a lone card's lean; 0deg for none — a card in a stack or a list never leans)
5437
+ ink --ink-w (the line) · --ink-off (the offset)
5438
+
5439
+ Information inside a card stays flat under every finish: only a container or a control gets an edge.
5440
+ Colours are mixes of the roles (--on-surface, --card-ink, --accent, currentColor), never plain black,
5441
+ so a finish sits in any palette; the one plain white is the sticker's backing paper and the lit edge's light.
5442
+ ============================================================ */
5409
5443
 
5410
- The light comes from above:
5444
+ /* every finish: the finish IS the edge, so the wireframe's two borders step back (as [data-palette] in palette.css); ink puts its own line there below */
5445
+ [data-finish] { --card-border: transparent; --action-border: transparent; }
5446
+
5447
+ /* ============================================================
5448
+ lit — the light comes from above:
5411
5449
  - a thing that stands UP (card, sheet, button) has a lit top edge, a hairline ring and a shadow
5412
5450
  that grows with its height: card 1 · sheet / float 2 — the layers cardds already has;
5413
- - a thing that is SUNK (a field, a pin cell) has its shadow inside, under its top edge;
5414
- - information inside a card stays flat: only a container or a control gets an edge.
5451
+ - a thing that is SUNK (a field, a pin cell) has its shadow inside, under its top edge.
5415
5452
  Shadows are mixed from the ink (--on-surface), never plain black, so they sit in the palette.
5416
5453
  ============================================================ */
5417
-
5418
5454
  [data-finish="lit"] {
5419
5455
  --_shade: var(--on-surface);
5420
5456
  --finish-edge: inset 0 1px 0 rgb(255 255 255 / 0.55); /* the lit top edge */
@@ -5440,9 +5476,6 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
5440
5476
  --finish-sheen: linear-gradient(to bottom, rgb(255 255 255 / 0.16), rgb(255 255 255 / 0) 60%);
5441
5477
  --finish-sunk: inset 0 1px 2px color-mix(in srgb, var(--_shade) 14%, transparent), 0 1px 0 rgb(255 255 255 / 0.5);
5442
5478
 
5443
- /* the shadow is the edge: the wireframe's lines step back (as in every colour palette) */
5444
- --card-border: transparent;
5445
- --action-border: transparent;
5446
5479
  --outline: color-mix(in srgb, currentColor 14%, transparent);
5447
5480
  }
5448
5481
 
@@ -5484,3 +5517,74 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
5484
5517
  /* ---- sunk: what the member types into ---- */
5485
5518
  [data-finish="lit"] :is(.field, .pin__cell) { box-shadow: var(--finish-sunk); }
5486
5519
 
5520
+ /* ============================================================
5521
+ print — printed paper (riso / zine): the second pass missed by --print-off, so the card's shadow is a
5522
+ hard offset in its own ink; a line in that ink round the card; grain over the whole card; the head
5523
+ underlined by a dotted rule as on a printed form. A lone card's head is a form's head; a stacked
5524
+ sheet's, a row card's, a cover's (controls over a picture) and a folded feed card's (the head is the
5525
+ whole card) have no rule.
5526
+ ============================================================ */
5527
+ [data-finish="print"] {
5528
+ --_ink: color-mix(in srgb, currentColor 70%, transparent);
5529
+ --outline: var(--_ink);
5530
+ }
5531
+ [data-finish="print"] .card {
5532
+ border: var(--border-w) solid var(--_ink);
5533
+ box-shadow: var(--print-off, 3px) var(--print-off, 3px) 0 0 color-mix(in srgb, var(--card-ink) 28%, transparent);
5534
+ isolation: isolate; /* the grain's blend stays inside the card */
5535
+ }
5536
+ /* the grain: a ::after, not ::before — .card--cover's scrim is its ::before. Over everything in the card (z-index 1, the foot's own level), touches pass through it */
5537
+ [data-finish="print"] .card::after {
5538
+ content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; z-index: 1;
5539
+ opacity: var(--print-grain, 0.32); mix-blend-mode: multiply;
5540
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.3 0 0 0 0 0.25 0 0 0 0 0.2 0 0 0 0.9 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
5541
+ }
5542
+ /* a dark card (the brand tone, a cover): multiply would only blacken it, so the grain lightens, fainter */
5543
+ [data-finish="print"] :is(.card--3, .card--cover)::after { mix-blend-mode: screen; opacity: calc(var(--print-grain, 0.32) * 0.56); }
5544
+ [data-finish="print"] .card:not(.card--row, .card--cover, .feed-card--small, .sheet-stack > *, .sheet-stack > :not(.sheet-stack__base) > *) > .card__head {
5545
+ padding-bottom: var(--sp-2);
5546
+ border-bottom: var(--rule-w) dotted color-mix(in srgb, currentColor 45%, transparent);
5547
+ }
5548
+ [data-finish="print"] :is(.btn, .chip) {
5549
+ border-color: var(--_ink);
5550
+ box-shadow: 2px 2px 0 0 color-mix(in srgb, currentColor 35%, transparent);
5551
+ }
5552
+ [data-finish="print"] .btn--primary { box-shadow: 2px 2px 0 0 color-mix(in srgb, var(--card-ink) 60%, transparent); }
5553
+ [data-finish="print"] :is(.btn, .chip):active { box-shadow: none; translate: 2px 2px; }
5554
+ [data-finish="print"] .card__thumb { border-style: dashed; }
5555
+
5556
+ /* ============================================================
5557
+ sticker — a die-cut sticker on a sheet: --sticker-cut of white paper left round the print, a short hard
5558
+ drop under it, a lone card leans by --sticker-tilt (the next one the other way); chunky buttons with a
5559
+ bottom edge that press down. A card in a stack, in a list, in a feed, a sheet or a modal never leans:
5560
+ those are placed by their holder, a lean would cut or cross them.
5561
+ ============================================================ */
5562
+ [data-finish="sticker"] { --outline: color-mix(in srgb, currentColor 18%, transparent); }
5563
+ [data-finish="sticker"] .card {
5564
+ box-shadow:
5565
+ 0 0 0 var(--sticker-cut, 5px) #fff,
5566
+ 0 calc(var(--sticker-cut, 5px) + 4px) 0 0 color-mix(in srgb, var(--on-surface) 16%, transparent),
5567
+ 0 calc(var(--sticker-cut, 5px) + 12px) 20px -8px color-mix(in srgb, var(--on-surface) 22%, transparent);
5568
+ }
5569
+ [data-finish="sticker"] .card:not(.sheet, .modal, .card--away, .sheet-stack *, .card-list > *, .card-feed > *) { rotate: var(--sticker-tilt, 0.8deg); }
5570
+ [data-finish="sticker"] .card:not(.sheet, .modal, .card--away, .sheet-stack *, .card-list > *, .card-feed > *):nth-of-type(even) { rotate: calc(-1 * var(--sticker-tilt, 0.8deg)); }
5571
+ [data-finish="sticker"] .card-list { gap: var(--sp-5); }
5572
+ [data-finish="sticker"] .btn {
5573
+ box-shadow: 0 3px 0 0 color-mix(in srgb, currentColor 30%, transparent);
5574
+ border-color: color-mix(in srgb, currentColor 25%, transparent);
5575
+ }
5576
+ [data-finish="sticker"] .btn--primary { box-shadow: 0 3px 0 0 color-mix(in srgb, var(--btn-primary-bg, var(--accent)) 55%, var(--on-surface)); }
5577
+ [data-finish="sticker"] .btn:active { box-shadow: none; translate: 0 3px; }
5578
+ [data-finish="sticker"] .chip { box-shadow: 0 0 0 3px #fff; }
5579
+
5580
+ /* ============================================================
5581
+ ink — a poster / comic print: --ink-w of the surface's ink round every boundary and control, and a hard
5582
+ offset of --ink-off in the same ink. The two wireframe borders come back as that line.
5583
+ ============================================================ */
5584
+ [data-finish="ink"] { --card-border: var(--on-surface); --action-border: var(--on-surface); --outline: currentColor; }
5585
+ [data-finish="ink"] .card { border: var(--ink-w, 2.5px) solid var(--on-surface); box-shadow: var(--ink-off, 5px) var(--ink-off, 5px) 0 0 var(--on-surface); }
5586
+ [data-finish="ink"] :is(.btn, .chip, .icon-btn:not(.icon-btn-group > *)) { border: var(--ink-w, 2.5px) solid currentColor; box-shadow: 3px 3px 0 0 currentColor; }
5587
+ [data-finish="ink"] .btn--primary { border-color: var(--on-surface); box-shadow: 3px 3px 0 0 var(--on-surface); }
5588
+ [data-finish="ink"] .toggle { box-shadow: 2px 2px 0 0 var(--on-surface); }
5589
+ [data-finish="ink"] .card__thumb { border-width: var(--ink-w, 2.5px); }
5590
+
@@ -40,6 +40,14 @@ export interface CardProps extends ComponentProps<'article'> {
40
40
  footLink?: ReactNode;
41
41
  /** set by `SheetStack` from the app's `open` and `refreshing`. The card renders its place, its grip and its pull. Leave it unset on any other card */
42
42
  stack?: StackPlace;
43
+ /** THE CARD LEAVES — the card floats away: up and out with a small turn, as one piece with all it holds (a stamp that has just landed on it goes with it).
44
+ * The app sets it when its own move begins (the send went out) and does what follows in `onLeft`; if the send fails the app never sets it and the card
45
+ * stays. Set back to false the card stands again where it was. Controlled like an input: the app owns it, the card only moves. A card that has left
46
+ * takes no taps and is hidden from assistive tech (`inert`); the room round it does not change, so nothing jumps. A card in a box that clips (a
47
+ * scrolling `StackSheet` body clips at its edge) is cut there as it goes. A card that is a `SheetStack` sheet (`stack`) ignores it. Knobs `--leave-rise` · `--leave-turn` */
48
+ leave?: boolean;
49
+ /** a card that was asked to `leave` HAS left (its motion ended; at once where motion is off, or when it has no motion) — the app does what follows: changes the page, closes. Once per leave */
50
+ onLeft?: () => void;
43
51
  }
44
52
  /**
45
53
  * Card — ONE boundary: head · content · foot. Cards are content the system offers (the words, the thing to read).
@@ -50,8 +58,9 @@ export interface CardProps extends ComponentProps<'article'> {
50
58
  * PRESSED WHOLE: a card whose ONLY control is its foot's button, or a row card's end button, is pressed anywhere — a touch off the
51
59
  * button runs that button's own action once, and the card dims under the finger. The app writes nothing; any other card is only read.
52
60
  */
53
- export declare function Card({ tone, paper, sm, row, cover, fold, centre, band, as, heading, caption, end, level, foot, footLink, stack, className, style, children, onClick, ref, ...rest }: CardProps): import("react").DetailedReactHTMLElement<{
61
+ export declare function Card({ tone, paper, sm, row, cover, fold, centre, band, as, heading, caption, end, level, foot, footLink, stack, leave, onLeft, className, style, children, onClick, ref, ...rest }: CardProps): import("react").DetailedReactHTMLElement<{
54
62
  onClick: (e: MouseEvent<HTMLElement>) => void;
63
+ inert?: boolean | undefined;
55
64
  "aria-busy"?: (boolean | "true" | "false") | undefined;
56
65
  ref: (node: HTMLElement | null) => void;
57
66
  className: string;
@@ -965,7 +974,6 @@ export declare function Card({ tone, paper, sm, row, cover, fold, centre, band,
965
974
  popover?: "" | "auto" | "manual" | "hint" | undefined;
966
975
  popoverTargetAction?: "toggle" | "show" | "hide" | undefined;
967
976
  popoverTarget?: string | undefined;
968
- inert?: boolean | undefined;
969
977
  inputMode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search" | undefined;
970
978
  is?: string | undefined;
971
979
  exportparts?: string | undefined;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Children, createElement, isValidElement, useLayoutEffect, useRef, useState } from 'react';
2
+ import { Children, createElement, isValidElement, useEffect, useLayoutEffect, useRef, useState } from 'react';
3
3
  import { cx } from '../cx.js';
4
- import { warnOnce } from '../state.js';
4
+ import { useLatest, warnOnce } from '../state.js';
5
5
  import { CardHead } from './CardHead.js';
6
6
  import { CardFoot } from './CardFoot.js';
7
7
  import { goControl } from './cardGo.js';
@@ -15,7 +15,7 @@ import { StackHeadCtx, StackPullCtx, ownHead, stackPaint, useStackCard } from '.
15
15
  * PRESSED WHOLE: a card whose ONLY control is its foot's button, or a row card's end button, is pressed anywhere — a touch off the
16
16
  * button runs that button's own action once, and the card dims under the finger. The app writes nothing; any other card is only read.
17
17
  */
18
- export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'article', heading, caption, end, level, foot, footLink, stack, className, style, children, onClick, ref, ...rest }) {
18
+ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'article', heading, caption, end, level, foot, footLink, stack, leave, onLeft, className, style, children, onClick, ref, ...rest }) {
19
19
  const head = heading != null || caption != null || end != null;
20
20
  const tail = foot != null || footLink != null;
21
21
  if (head || tail) {
@@ -37,6 +37,19 @@ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'ar
37
37
  else if (passed)
38
38
  passed.current = node;
39
39
  };
40
+ const leaver = leave != null && !stack;
41
+ const away = leaver && leave;
42
+ const left = useLatest(onLeft);
43
+ useEffect(() => {
44
+ const card = own.current;
45
+ if (!away || !card)
46
+ return;
47
+ let live = true;
48
+ const moving = card.getAnimations().filter((a) => a instanceof CSSTransition && (a.transitionProperty === 'translate' || a.transitionProperty === 'rotate')); // the leave's own; none where motion is off
49
+ Promise.all(moving.map((a) => a.finished)).then(() => { if (live)
50
+ left.current?.(); }, () => { }); // a transition cancelled by `leave` set back rejects
51
+ return () => { live = false; };
52
+ }, [away]);
40
53
  const [go, setGo] = useState(false);
41
54
  useLayoutEffect(() => { setGo(own.current != null && goControl(own.current) != null); }); // every render: what the card holds is its children's
42
55
  const pressing = useRef(false);
@@ -77,9 +90,10 @@ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'ar
77
90
  const props = {
78
91
  ...rest,
79
92
  ref: setRef,
80
- className: cx('card', tone && tone !== 1 && `card--${tone}`, paper && 'sheet--paper', sm && 'card--sm', row && 'card--row', cover && 'card--cover', fold && 'card--fold', centre && 'card--centre', band && 'card--band', go && 'card--go', painted.className, className),
93
+ className: cx('card', tone && tone !== 1 && `card--${tone}`, paper && 'sheet--paper', sm && 'card--sm', row && 'card--row', cover && 'card--cover', fold && 'card--fold', centre && 'card--centre', band && 'card--band', go && 'card--go', leaver && 'card--leaver', away && 'card--away', painted.className, className),
81
94
  style: painted.style || style ? { ...painted.style, ...style } : undefined,
82
95
  ...(stack ? { 'aria-busy': painted.busy } : {}),
96
+ ...(away ? { inert: true } : {}),
83
97
  onClick: click,
84
98
  };
85
99
  if (!head && !tail)
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Children, createContext, isValidElement, useContext, useId, useLayoutEffect, useRef } from 'react';
3
3
  import { cx } from '../cx.js';
4
- import { reducedMotion, useControlled, warnOnce } from '../state.js';
4
+ import { reducedMotion, setRefs, useControlled, warnOnce } from '../state.js';
5
5
  import { Card } from './Card.js';
6
6
  import { CardHead } from './CardHead.js';
7
7
  import { CardFoot } from './CardFoot.js';
@@ -53,16 +53,9 @@ export function CardFeed({ big: bigProp, defaultBig, onBigChange, className, chi
53
53
  }
54
54
  tops.current = at;
55
55
  });
56
- const setRef = (node) => {
57
- own.current = node;
58
- if (typeof ref === 'function')
59
- ref(node);
60
- else if (ref)
61
- ref.current = node;
62
- };
63
56
  if (!shown.length)
64
57
  return null;
65
- return (_jsx(FeedCtx.Provider, { value: { big, grow: (id) => request(id, 'tap') }, children: _jsx("div", { ref: setRef, className: cx('card-feed', className), ...rest, children: shown }) }));
58
+ return (_jsx(FeedCtx.Provider, { value: { big, grow: (id) => request(id, 'tap') }, children: _jsx("div", { ref: setRefs(own, ref), className: cx('card-feed', className), ...rest, children: shown }) }));
66
59
  }
67
60
  const onSmallKey = (e) => {
68
61
  if (e.target !== e.currentTarget || (e.key !== 'Enter' && e.key !== ' '))