@usableapp/cardds 0.7.104 → 0.7.106

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
@@ -88,6 +88,7 @@
88
88
  --tap: max(var(--tap-floor), var(--tap-base)); /* 48 primary touch target */
89
89
  --tap-sm: max(var(--tap-floor), calc(var(--tap-base) * 11 / 12)); /* 44 minimum touch target */
90
90
  --tap-lg: max(var(--tap-floor), calc(var(--tap-base) * 7 / 6)); /* 56 hero controls: send, field with action */
91
+ --tap-xl: max(var(--tap-floor), calc(var(--tap-base) * 17 / 12)); /* 68 the biggest round control: a foot of a few big round buttons (IconBtn xl) */
91
92
  --btn-h: calc(var(--tap-base) * 13 / 12); /* 52 button height */
92
93
  --btn-xl-h: calc(var(--tap-base) * 11 / 6); /* 88 the one giant CTA */
93
94
  --chip-h: calc(var(--tap-base) * 19 / 24); /* 38 chip height */
@@ -332,6 +333,7 @@
332
333
  --ink-tint: color-mix(in srgb, currentColor 12%, transparent); /* hover, rails, rests */
333
334
  --ink-rail: color-mix(in srgb, currentColor 30%, transparent); /* todo steps, ghost edges */
334
335
  --accent-soft: color-mix(in srgb, var(--accent) 22%, transparent); /* calendar marks */
336
+ --accent-wash: color-mix(in srgb, var(--accent) 22%, var(--action-bg)); /* the same wash laid on the paper of fields and buttons, opaque: a soft chip reads the same on any card (Chip soft) */
335
337
  --accent-mid: color-mix(in srgb, var(--accent) 50%, transparent);
336
338
  /* PASTELS (Lh 2026-10-20: a component never brings a colour of its own; every colour is the theme's): the nine pastel papers of a
337
339
  sticky note, a note chooser's swatch and a stamp's picture ground (src/sticky/tones.ts names them), and a stamp's paper. Light
@@ -525,7 +527,7 @@
525
527
  [data-palette="orchard"] .field { background: var(--field-bg, #ffffff); color: var(--ink-onlight); }
526
528
  [data-palette="orchard"] .pin__cell { background: var(--pin-cell-bg, #ffffff); color: var(--ink-onlight); }
527
529
  /* the vivid ones, tried on the live parts (Lh 2026-09-25): primary = yellow · a user avatar = fuchsia · ring = sky · a switch on = sky · maple only on small dots (the count, dot a); the chosen stack tab stays moss */
528
- [data-palette="orchard"] .btn--primary { background: var(--btn-primary-bg, var(--highlight-2)); color: var(--btn-primary-ink, var(--ink-onlight)); }
530
+ [data-palette="orchard"] :is(.btn--primary, .icon-btn--primary) { background: var(--btn-primary-bg, var(--highlight-2)); color: var(--btn-primary-ink, var(--ink-onlight)); }
529
531
  [data-palette="orchard"] .avatar:not(.avatar--outline, .avatar--add, .avatar--on) { background: var(--avatar-bg, var(--highlight-4)); --dark: 1; }
530
532
  [data-palette="orchard"] .ring::before { background: conic-gradient(var(--highlight-3) calc(var(--value) * 1%), var(--ink-tint) 0); }
531
533
  [data-palette="orchard"] .toggle:checked { background: var(--highlight-3); --accent-dark: 0; } /* sky is a light ground: its knob is onlight */
@@ -999,12 +1001,23 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
999
1001
  .card__head > :is(h1, h2, h3, div, p):first-child { flex: 1; min-width: 0; }
1000
1002
 
1001
1003
  /* Skeleton head — the heading in a card's OWN head, and only there.
1002
- The heading wraps freely: no line budget, no squeeze, no clamp (Lh
1003
- 2026-09-16 — a height rule on words only distorts them; content that needs
1004
- more rows takes more rows). The group that holds heading ·
1004
+ The heading wraps, no squeeze, up to three lines and cut there with an
1005
+ ellipsis (Lh 2026-10-21: "มันควรจะ max สามบรรทัด" — a request's title ran to
1006
+ four lines on a phone; reverses 2026-09-16's "no clamp"). The count is the knob
1007
+ --card-head-title-lines. The group that holds heading ·
1005
1008
  caption is the head's own wrapping row (above) — no wrapper element, no
1006
1009
  height, only the row gap. */
1007
- .card__head :is(h1, h2, h3) { overflow-wrap: anywhere; }
1010
+ .card__head :is(h1, h2, h3) {
1011
+ overflow-wrap: anywhere;
1012
+ display: -webkit-box;
1013
+ -webkit-box-orient: vertical;
1014
+ -webkit-line-clamp: var(--card-head-title-lines, 3);
1015
+ line-clamp: var(--card-head-title-lines, 3);
1016
+ overflow: hidden;
1017
+ padding-block-start: var(--_mark-room); /* a stacked Thai mark rises over the first line's box: room inside the clip, given back outside */
1018
+ margin-block-start: calc(-1 * var(--_mark-room));
1019
+ --_mark-room: 0.2em;
1020
+ }
1008
1021
 
1009
1022
  /* a Stamp in a card's head (`Card end={<Stamp …/>}`): the words stand at the left, the stamp at the right BESIDE BOTH LINES — the heading and the
1010
1023
  caption pair up beside it, not the stamp beside the heading alone with the caption left under it. The stamp is its own size (--stamp-size,
@@ -1013,19 +1026,33 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1013
1026
  .card__head:has(> .stamp) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
1014
1027
  .card__head:has(> .stamp) > :is(h1, h2, h3) { grid-column: 1; grid-row: 1; align-self: end; }
1015
1028
 
1029
+ /* a head that LEADS with a date (Lh 2026-10-21, timebank's request card): a `CardHead` whose first child is a JourneyBadge and whose second is a
1030
+ ChipRow. The date stands at the left and the chips beside it, the two as tall as each other; the heading and its caption take the whole
1031
+ width under them, so the heading sits right over the words of the middle. The date is always as tall as the chips. The gap under the date row is --card-head-lead-gap; the date and the chips place themselves (journey.css, actions.css).
1032
+ <div class="card__head"><span class="tile-badge tile-badge--stretch">…</span><div class="chip-row">…</div><div><h2 class="t-h1">…</h2>…</div></div> */
1033
+ .card__head:has(> .tile-badge:first-child + .chip-row) {
1034
+ display: grid;
1035
+ grid-template-columns: auto minmax(0, 1fr);
1036
+ align-items: center;
1037
+ row-gap: var(--card-head-lead-gap, var(--sp-3));
1038
+ }
1039
+ .card__head:has(> .tile-badge:first-child + .chip-row) > :nth-child(n + 3) { grid-column: 1 / -1; }
1040
+
1016
1041
  /* a JourneyBadge at the edge of the card's own first head (the date box in the top-left or top-right corner) keeps its corner concentric
1017
1042
  with the card's: the card's corner (--r-card) less the room the padding leaves round it. journey.css reads it behind the theme's --tile-badge-radius,
1018
1043
  so a theme that sets that knob still wins. */
1019
- .card > .card__head:first-child > .tile-badge:is(:first-child, :last-child) { --_badge-radius: calc(var(--r-card) - var(--card-pad)); }
1044
+ :is(.card > .card__head:first-child, .card__face > .card__turn + .card__head) > .tile-badge:is(:first-child, :last-child) { --_badge-radius: calc(var(--r-card) - var(--card-pad)); } /* a turning card's face: its head follows the unseen turn button */
1020
1045
 
1021
1046
  /* ---- paper bands: the two-tone card (`Card paper="bands"`, Lh 2026-10-19): the head and the foot are paper bands edge to
1022
1047
  edge over the card's padding, no hairline; the middle between them keeps the card's tone, lightened (--card-middle-bg). The card
1023
- clips, so the bands keep its corners. A two-faced card (below) carries the look on each face. */
1048
+ clips, so the bands keep its corners. A two-faced card (below) carries the look on each face, never on the turning box: a clip
1049
+ there cut the face off at the card's edges while it turned, its near edge being nearer the eye than the box (2026-10-21). */
1024
1050
  :is(.card--bands, .card--bands > .card__face) {
1025
1051
  background: var(--card-middle-bg, color-mix(in srgb, var(--card-bg) 35%, var(--card-1-bg)));
1026
1052
  --dark: var(--card-1-dark); /* the middle is mostly paper: its words take the paper's side */
1027
1053
  overflow: hidden;
1028
1054
  }
1055
+ .card--bands.card--turns { background: none; overflow: visible; } /* the turning box keeps --dark: the faces' ink is computed from it */
1029
1056
  :is(.card--bands, .card--bands > .card__face) > :is(.card__head, .card__foot) {
1030
1057
  --card-bg: var(--card-1-bg);
1031
1058
  --dark: var(--card-1-dark);
@@ -1035,16 +1062,22 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1035
1062
  padding-inline: var(--card-pad);
1036
1063
  }
1037
1064
  :is(.card--bands, .card--bands > .card__face) > .card__head { margin-top: calc(-1 * var(--card-pad)); padding-block: var(--card-pad); }
1065
+ :is(.card--bands, .card--bands > .card__face) > .card__head { padding-block-end: var(--card-band-seam, var(--sp-2)); }
1066
+ /* the seam: the heading and the words under it read as ONE paragraph that changes colour at the band's edge (Lh 2026-10-21), so the room on
1067
+ each side of the head band's lower edge is --card-band-seam, not the card's padding and gap */
1068
+ :is(.card--bands, .card--bands > .card__face) > .card__head + * { margin-block-start: calc(var(--card-band-seam, var(--sp-2)) - var(--card-gap)); }
1038
1069
  :is(.card--bands, .card--bands > .card__face) > .card__foot { margin-bottom: calc(-1 * var(--card-pad)); padding-block: var(--sp-3); } /* the foot band is slim */
1039
1070
 
1040
1071
  /* ---- two faces: the WHOLE card turns (`Card back`, Lh 2026-10-19). The article is the turning box (.flip, base.css) and only that;
1041
1072
  each face is the card's paper — head · middle · foot — and both lie in one grid cell. The head and the foot are the same on both
1042
- faces; the middle is one fixed height (--card-middle-h, the theme's call), so the card never changes height as it turns. The hidden
1073
+ faces; the card is as tall as its taller face and each face fills it, the middle taking what the head and the foot leave (2026-10-21,
1074
+ timebank: a fixed middle left a short front a tall empty band), so the card never changes height as it turns; --card-middle-h fixes
1075
+ the middle's height instead, the theme's call. The hidden
1043
1076
  face is inert. Each face holds an unseen turn button: the keyboard's way to turn it; its ring is drawn on the face. */
1044
1077
  .card--turns { background: none; border: 0; padding: 0; cursor: pointer; touch-action: manipulation; }
1045
1078
  .card__face {
1046
- display: grid;
1047
- align-content: start;
1079
+ display: flex;
1080
+ flex-direction: column;
1048
1081
  gap: var(--card-gap);
1049
1082
  min-width: 0;
1050
1083
  padding: var(--card-pad);
@@ -1055,7 +1088,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1055
1088
  position: relative;
1056
1089
  }
1057
1090
  .card__face > * { min-width: 0; }
1058
- .card__middle { display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, calc(var(--tap-base) * 25 / 4)); overflow: hidden; }
1091
+ .card__middle { flex: 1 0 auto; display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, auto); overflow: hidden; }
1059
1092
  .card__turn { position: absolute; inset: 0; width: var(--px); height: var(--px); padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); }
1060
1093
  .card__face:has(> .card__turn:focus-visible) { outline: var(--focus); outline-offset: calc(-1 * var(--sp-1)); }
1061
1094
  .card__head > .chip-row > .chip > .icon { --icon-display: inline-block; --icon-align: -0.15em; --icon-margin: 0 var(--chip-sm-gap, var(--sp-1)) 0 0; }
@@ -1079,7 +1112,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1079
1112
  nothing jumps. Chromium/Safari paint it through the ::-webkit-scrollbar
1080
1113
  parts; Firefox gets its thin bar in the same ink. */
1081
1114
  .bleed {
1082
- margin-inline: calc(-1 * var(--card-pad, 0rem));
1115
+ margin-inline: calc(-1 * var(--card-pad, 0rem)); /* the edge a bleeding child takes back from what holds it; .map--bleed (sheet.css) reads the same --card-pad */
1083
1116
  padding-inline: var(--card-pad, 0rem);
1084
1117
  padding-block-end: var(--sp-2);
1085
1118
  scroll-padding-inline: var(--card-pad, 0rem);
@@ -1096,7 +1129,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1096
1129
  }
1097
1130
  /* a bleeding child sits over the container's rounded corner, so a strip that
1098
1131
  is genuinely first or last needs the corner clipped for it */
1099
- .card:has(> .bleed:first-child), .card:has(> .bleed:last-child) { overflow: clip; }
1132
+ .card:has(> :is(.bleed, .map--bleed):first-child), .card:has(> :is(.bleed, .map--bleed):last-child) { overflow: clip; }
1100
1133
 
1101
1134
  /* foot: the card's action buttons on one line. One button takes the width,
1102
1135
  up to four share it in even parts — so the gaps read even too — and from
@@ -1107,6 +1140,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1107
1140
  whole short row of its own, centred — it never shares the buttons' line */
1108
1141
  .card__foot--link { flex-wrap: wrap; align-content: end; row-gap: var(--card-foot-link-gap, var(--sp-1)); }
1109
1142
  .card__foot-link { flex: 1 0 100%; display: flex; justify-content: center; }
1143
+ /* round (CardFoot round): the foot holds a few round IconBtns, each its own size (they never share the line), as ONE group in the middle with
1144
+ a gap that does not follow the card's width. The gap is wider than the Btns' (a miss between two round targets): --card-foot-gap sets it */
1145
+ .card__foot--round { justify-content: center; gap: var(--card-foot-gap, var(--sp-6)); }
1110
1146
 
1111
1147
  /* media placeholder for wireframes — currentColor so it survives any palette */
1112
1148
  .ph {
@@ -1721,7 +1757,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1721
1757
  letter-spacing: var(--track-display);
1722
1758
  /* the FIRST line sits on the centre of the peek strip (--stack-step tall,
1723
1759
  measured from the card's top edge); line two runs under the next card */
1724
- margin-top: calc((var(--stack-step) - 1lh) / 2 - var(--sp-5));
1760
+ margin-top: calc((var(--stack-step) - 1lh) / 2 - var(--sp-5) - var(--_mark-room)); /* --_mark-room: card.css, the room for the marks over line one */
1725
1761
  }
1726
1762
  /* fanned stack: a toggle opens the stack wider so every peek shows more */
1727
1763
  .sheet-stack--fanned { --stack-step: calc(var(--tap-base) * 19 / 6); }
@@ -2418,6 +2454,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2418
2454
  border-radius: var(--map-radius, var(--r-media));
2419
2455
  overflow: hidden;
2420
2456
  }
2457
+ .map--bleed { margin-inline: calc(-1 * var(--card-pad, 0rem)); border-radius: 0; } /* edge to edge is a band, and a band has no corner; --map-radius is for the box in the inset */
2421
2458
  .sheet__body:has(> .map--inline:only-child) { display: flex; flex-direction: column; }
2422
2459
  .sheet__body > .map--inline:only-child { flex: 1 0 auto; }
2423
2460
 
@@ -2542,8 +2579,15 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2542
2579
  }
2543
2580
  .sheet-step__summary { color: var(--sheet-step-summary-ink, var(--card-muted)); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2544
2581
  .sheet-step__check { margin-inline-start: auto; }
2545
- .sheet-step__body { min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
2546
- .sheet-step__inner { display: grid; gap: var(--card-gap); padding-block: var(--card-gap); padding-inline: var(--sheet-step-inner-pad-x, var(--sheet-step-bar-pad-x, var(--sp-3))); } /* the open step's content in the frame of its title's words (the bar's own inset), so it reads as that step's (Lh 2026-09-28, #153) */
2582
+ /* a step's child that bleeds (ChipRow bleed, MapArea bleed, a Banner) reaches the SHEET's edges (Lh 2026-10-19, the ask sheet's
2583
+ จุดนัด step: the map edge to edge under the step's buttons). The fold's clip is the step body's box, which sat inside the sheet's
2584
+ inset, so the body takes that inset back and pads by it again, the way .sheet__body does; the inner keeps its own box and its own
2585
+ inset (--sheet-step-inner-pad-x: the inset BEYOND the sheet's) and publishes the whole, the sheet's plus its own, as --card-pad,
2586
+ so a .bleed cancels it exactly. The sum is computed on the body, before the inner's --card-pad takes over; outside a card there is
2587
+ no --card-pad and the step keeps its own inset (0px, as .bleed reads it). The inner reads the knob itself, not the sum: a theme's
2588
+ unitless 0 (SheetSteps · Ruled) is a length there but not in a calc, so the step stays right and only the bleed is lost. */
2589
+ .sheet-step__body { --_step-pad: var(--sheet-step-inner-pad-x, var(--sheet-step-bar-pad-x, var(--sp-3))); --_pad: calc(var(--card-pad, 0rem) + var(--_step-pad)); margin-inline: calc(-1 * var(--card-pad, 0rem)); padding-inline: var(--card-pad, 0rem); min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
2590
+ .sheet-step__inner { --card-pad: var(--_pad); display: grid; gap: var(--card-gap); padding-block: var(--card-gap); padding-inline: var(--_step-pad); } /* the open step's content in the frame of its title's words (the bar's own inset), so it reads as that step's (Lh 2026-09-28, #153) */
2547
2591
  .sheet-step__inner > * { min-width: 0; }
2548
2592
  .sheet-step:not(.sheet-step--now) > .sheet-step__body { visibility: hidden; transition: visibility 0s var(--ease-linear) var(--motion-sheet); }
2549
2593
  .sheet-step__go { position: absolute; inset: min(0rem, (var(--_row-h) - var(--tap-sm)) / 2) 0 auto; min-height: max(100%, var(--tap-sm)); /* a thin bar still takes a 44px tap */ margin: 0; cursor: pointer; }
@@ -2830,9 +2874,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2830
2874
 
2831
2875
  /* stretch: a date in a head (Lh 2026-10-19) — as tall as the title block beside it and no taller (the head stretches it; size
2832
2876
  containment keeps its own words from adding height). Its height picks the form: short (a one-line title) = two lines, the day name
2833
- over the number and the month on one line; tall (a two-line title) = three lines, day · number · month. */
2877
+ over the number and the month on one line; tall (a two-line title) = three lines, day · number · month. It grows no taller than
2878
+ its three lines (2026-10-21, timebank: beside a four-line title it was a tall empty block); held there, it stands at the head's top. */
2834
2879
  .tile-badge--stretch {
2880
+ line-height: var(--tile-badge-line, var(--lh-badge)); /* the knob's own property; the cap below reads it too */
2835
2881
  align-self: stretch;
2882
+ max-block-size: calc((2 * var(--fs-caption) + var(--fs-lg)) * var(--tile-badge-line, var(--lh-badge)) + 2 * var(--sp-3));
2836
2883
  container-type: size;
2837
2884
  inline-size: calc(var(--fs-lg) * 3.25);
2838
2885
  grid-template-columns: auto auto;
@@ -2842,14 +2889,16 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2842
2889
  column-gap: var(--sp-1);
2843
2890
  row-gap: 0;
2844
2891
  padding-block: var(--sp-3);
2845
- line-height: var(--tile-badge-line, var(--lh-badge));
2846
2892
  }
2847
2893
  .tile-badge--stretch > * { line-height: inherit; }
2848
2894
  .tile-badge--stretch > :first-child { grid-column: 1 / -1; }
2849
- @container (min-height: 5.5rem) {
2895
+ @container (min-height: 3.5rem) { /* just under the three lines' own height, the most the badge grows to */
2850
2896
  .tile-badge--stretch > * { grid-column: 1 / -1; }
2851
2897
  }
2852
2898
 
2899
+ /* where it sits in a head that leads with the date (card.css): the first cell, as tall as the chips beside it however many rows, so no cap */
2900
+ .card__head:has(> .tile-badge:first-child + .chip-row) > .tile-badge--stretch:first-child { grid-column: 1; grid-row: 1; max-block-size: none; }
2901
+
2853
2902
  /* corner badge: pin a tile badge on media (rating, price) — .card__media is relative */
2854
2903
  .tile-badge--corner { position: absolute; right: var(--sp-3); bottom: var(--sp-3); }
2855
2904
 
@@ -2947,6 +2996,14 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2947
2996
  }
2948
2997
  .icon-btn--invert { background: var(--icon-btn-invert-bg, var(--accent)); color: var(--icon-btn-invert-ink, var(--on-accent)); border-color: transparent; }
2949
2998
  .icon-btn--sm { width: var(--icon-btn-sm-size, var(--tap-sm)); height: var(--icon-btn-sm-size, var(--tap-sm)); font-size: var(--fs-title); }
2999
+ /* the bigger round sizes (`IconBtn size`): lg is the hero control's size (--tap-lg), xl the biggest (--tap-xl), both on --tap-base like every size.
3000
+ What stands inside is a share of the circle (`--_ib`, its side), so it grows with it: the ICON is --icon-btn-icon of the side (7/12, the share of the default 48px
3001
+ button's 28px icon), and a WORD (a string child) keeps to --icon-btn-word-max of the side (0.45: a word of four letters stays inside) — it still
3002
+ grows with the member's text size, to that ceiling and no further. The default and the sm button keep their icon tokens and their word as before. */
3003
+ .icon-btn--lg { --_ib: var(--icon-btn-lg-size, var(--tap-lg)); width: var(--_ib); height: var(--_ib); }
3004
+ .icon-btn--xl { --_ib: var(--icon-btn-xl-size, var(--tap-xl)); width: var(--_ib); height: var(--_ib); }
3005
+ :is(.icon-btn--lg, .icon-btn--xl) { font-size: min(var(--fs-lg), calc(var(--_ib) * var(--icon-btn-word-max, 0.45))); }
3006
+ :is(.icon-btn--lg, .icon-btn--xl) > .icon { --icon-size: calc(var(--_ib) * var(--icon-btn-icon, 7 / 12)); --icon-sm-size: var(--icon-size); --icon-lg-size: var(--icon-size); } /* the Icon's own knobs */
2950
3007
  /* ---- icon button group: up to four icon-only commands in ONE frame (Lh 2026-09-29) ----
2951
3008
  <div class="icon-btn-group" role="group"><button class="icon-btn" aria-label="…">…</button>…</div>
2952
3009
  One pill as tall as a button, the buttons share its width, a hairline between them; in a foot it takes the row. */
@@ -3004,7 +3061,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3004
3061
  min-height: var(--btn-h);
3005
3062
  text-decoration: none; /* .btn works as <a> too */
3006
3063
  }
3007
- .btn--primary { background: var(--btn-primary-bg, var(--accent)); color: var(--btn-primary-ink, var(--on-accent)); border-color: transparent; }
3064
+ /* primary: a Btn and a round IconBtn fill alike, from the same tokens (the palette's primary — orchard's yellow — retunes both) */
3065
+ :is(.btn--primary, .icon-btn--primary) { background: var(--btn-primary-bg, var(--accent)); color: var(--btn-primary-ink, var(--on-accent)); border-color: transparent; }
3008
3066
  /* quiet keeps a faint outline so it still reads as a button on colored palettes;
3009
3067
  it is ink on whatever it sits on — inherit, or on the dark slot (card--3) it
3010
3068
  would take the light-card action ink and vanish */
@@ -3052,12 +3110,17 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3052
3110
  .dot--error { background: var(--signal-error); }
3053
3111
  .dot--info { background: var(--signal-info); }
3054
3112
  /* --sm: a small LABEL (a name on a note, a tag in a tight head) — low and tight; never a tap target */
3055
- .chip--sm { min-height: 0; gap: var(--chip-sm-gap, var(--sp-1)); padding: var(--chip-sm-pad-y, var(--sp-0)) var(--chip-sm-pad-x, var(--sp-2)); }
3113
+ /* one height wherever it stands (Lh 2026-10-21): the body's line, so a head that cuts its words with an ellipsis keeps the Thai marks over them, and a foot's or a middle's chip reads the same */
3114
+ .chip--sm { min-height: 0; gap: var(--chip-sm-gap, var(--sp-1)); padding: var(--chip-sm-pad-y, var(--sp-0)) var(--chip-sm-pad-x, var(--sp-2)); line-height: var(--chip-sm-line, var(--lh-body)); }
3115
+ /* soft: the accent washed into the chip's ground, no edge — a status to see, quieter than on (was the inbox Ribbon, Lh 2026-10-21) */
3116
+ .chip--soft { background: var(--chip-soft-bg, var(--accent-wash)); color: var(--chip-soft-ink, var(--card-ink)); border-color: transparent; } /* the wash on the chip's own paper, opaque (--accent-wash): see-through, a dark tone came through under the light side's ink (ink.spec) */
3056
3117
  .chip--on { background: var(--chip-on-bg, var(--accent)); --dark: var(--accent-dark); color: var(--chip-on-ink, var(--card-ink)); border-color: transparent; }
3057
3118
  /* muted: an untouched default — the words wash out, the edge stays; emphasis is the difference from the neighbours (docs/reference.md 13) */
3058
3119
  .chip--muted { color: var(--chip-muted-ink, var(--card-muted)); } /* the chip's own fill's muted words: a muted chip on the dark card is still a light chip */
3059
3120
 
3060
3121
  .chip-row { display: flex; gap: var(--sp-2); }
3122
+ /* the words that lead the chips (ChipRow `lead`, "นัดที่" before a place, Lh 2026-10-21): the chips' own type and size, at the middle of their height */
3123
+ .chip-row__lead { align-self: center; font: var(--type-label); font-size: var(--chip-font-size, var(--fs-sm)); color: var(--chip-row-lead-ink, inherit); }
3061
3124
  .chip-row:not(.bleed) { flex-wrap: wrap; } /* a strip (.bleed, card.css) stays one line */
3062
3125
  .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 */
3063
3126
 
@@ -3310,8 +3373,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3310
3373
 
3311
3374
  /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
3312
3375
  /* a head's ChipRow takes a line of its own under the title: at most two lines of chips, a chip's words cut with an ellipsis */
3313
- .card__head > .chip-row { flex: 1 1 100%; order: 2; max-height: calc(2 * (var(--fs-sm) * 1.5 + 2 * var(--chip-sm-pad-y, var(--sp-0)) + 2 * var(--border-w)) + var(--sp-2)); overflow: hidden; }
3314
- .card__head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: var(--lh-body); }
3376
+ .card__head > .chip-row { flex: 1 1 100%; order: 2; max-height: calc(2 * (var(--fs-sm) * var(--chip-sm-line, var(--lh-body)) + 2 * var(--chip-sm-pad-y, var(--sp-0)) + 2 * var(--border-w)) + var(--sp-2)); overflow: hidden; }
3377
+ /* …beside a date that leads the head (card.css) they stand in the date's row, every chip in sight: the meeting place may take a line of its own */
3378
+ .card__head:has(> .tile-badge:first-child + .chip-row) > .chip-row { grid-column: 2; grid-row: 1; max-height: none; }
3379
+ .card__head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3315
3380
  .card__foot > .btn { flex: 1 1 0; min-width: 0; }
3316
3381
  /* …unless the button says its own size: `Btn form` is as wide as its words in a foot too, centred (Lh 2026-10-10: one button filling
3317
3382
  a card's foot read too big — which button a card wears is the project's choice, so the foot must not undo it) */
@@ -6012,7 +6077,7 @@ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: va
6012
6077
  }
6013
6078
  /* the main button throws its own colour */
6014
6079
  [data-finish="lit"] .btn--primary,
6015
- [data-finish="lit"] .icon-btn--invert:not(.icon-btn-group > *) {
6080
+ [data-finish="lit"] :is(.icon-btn--invert, .icon-btn--primary):not(.icon-btn-group > *) {
6016
6081
  box-shadow: var(--finish-edge),
6017
6082
  0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 20%, transparent),
6018
6083
  0 0.5rem 1rem -0.25rem color-mix(in srgb, var(--accent) 45%, transparent);
@@ -43,6 +43,8 @@ export interface CardProps extends ComponentProps<'article'> {
43
43
  foot?: ReactNode;
44
44
  /** the small Link under the foot's buttons (CardFoot `link`) */
45
45
  footLink?: ReactNode;
46
+ /** the foot is a few big ROUND buttons (CardFoot `round`): `foot` holds `IconBtn`s, stood as one centred group a fixed gap apart */
47
+ footRound?: boolean;
46
48
  /** 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 */
47
49
  stack?: StackPlace;
48
50
  /** 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).
@@ -59,8 +61,8 @@ export interface CardProps extends ComponentProps<'article'> {
59
61
  onRemove?: () => void;
60
62
  /** THE BACK — the card has two faces and the member turns the WHOLE card over: a tap anywhere on it that is not on a control asks for the other
61
63
  * face. The head and the foot (`heading`/`caption`/`end` or a CardHead child, `foot`) are given once and drawn on BOTH faces, so they read the same
62
- * either way; the children are the front's middle, `back` the back's. The middle is one fixed height on both faces (knob `--card-middle-h`), so the
63
- * card never changes height when it turns. The hidden face is `inert`; an `id` in the head or the foot would be drawn twice, so give it none.
64
+ * either way; the children are the front's middle, `back` the back's. The card is as tall as its taller face and each face fills it, the middle taking
65
+ * what is left, so the card never changes height when it turns (knob `--card-middle-h` fixes the middle's height instead). The hidden face is `inert`; an `id` in the head or the foot would be drawn twice, so give it none.
64
66
  * Not on a `row`, `sm` or `stack` card */
65
67
  back?: ReactNode;
66
68
  /** CONTROLLED: the face that shows — pair it with `onFaceChange`. The app owns it */
@@ -87,7 +89,7 @@ export interface CardProps extends ComponentProps<'article'> {
87
89
  * TWO FACES (`back`): the whole card turns over on a tap off a control (the flip mechanic it shares with Postcard); the head and the foot
88
90
  * read the same on both faces, the middle is one fixed height. `paper="bands"`: paper head and foot bands edge to edge, the tint between.
89
91
  */
90
- export declare function Card({ tone, paper, sm, row, cover, fold, centre, band, as, heading, caption, end, level, foot, footLink, stack, leave, onLeft, onRemove, removeLabel, back, face, defaultFace, onFaceChange, toBackLabel, toFrontLabel, className, style, children, onClick, ref, ...rest }: CardProps): import("react").DetailedReactHTMLElement<{
92
+ export declare function Card({ tone, paper, sm, row, cover, fold, centre, band, as, heading, caption, end, level, foot, footLink, footRound, stack, leave, onLeft, onRemove, removeLabel, back, face, defaultFace, onFaceChange, toBackLabel, toFrontLabel, className, style, children, onClick, ref, ...rest }: CardProps): import("react").DetailedReactHTMLElement<{
91
93
  onClick: (e: MouseEvent<HTMLElement>) => void;
92
94
  inert?: boolean | undefined;
93
95
  "aria-busy"?: (boolean | "true" | "false") | undefined;
@@ -19,7 +19,7 @@ import { StackHeadCtx, StackPullCtx, ownHead, stackPaint, useStackCard } from '.
19
19
  * TWO FACES (`back`): the whole card turns over on a tap off a control (the flip mechanic it shares with Postcard); the head and the foot
20
20
  * read the same on both faces, the middle is one fixed height. `paper="bands"`: paper head and foot bands edge to edge, the tint between.
21
21
  */
22
- export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'article', heading, caption, end, level, foot, footLink, stack, leave, onLeft, onRemove, removeLabel, back, face, defaultFace = 'front', onFaceChange, toBackLabel, toFrontLabel, className, style, children, onClick, ref, ...rest }) {
22
+ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'article', heading, caption, end, level, foot, footLink, footRound, stack, leave, onLeft, onRemove, removeLabel, back, face, defaultFace = 'front', onFaceChange, toBackLabel, toFrontLabel, className, style, children, onClick, ref, ...rest }) {
23
23
  if (onRemove && !row)
24
24
  warnOnce('card-remove-row', 'cardds Card: `onRemove` draws its cross on a `row` card only');
25
25
  if (back != null && (row || sm || stack))
@@ -118,10 +118,10 @@ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'ar
118
118
  const ownHeadKid = kids.find((c) => isValidElement(c) && c.type === CardHead);
119
119
  const ownFootKid = kids.find((c) => isValidElement(c) && c.type === CardFoot);
120
120
  const front = kids.filter((c) => c !== ownHeadKid && c !== ownFootKid);
121
- const side = (which, middle) => (_jsxs("div", { className: cx('card__face flip__face', `flip__face--${which}`), ...((which === 'back') !== flipped ? { inert: true } : {}), children: [_jsx("button", { type: "button", className: "card__turn", "aria-label": which === 'front' ? toBackLabel ?? word('turnToBack') : toFrontLabel ?? word('turnToFront'), onClick: () => ask(which === 'front' ? 'back' : 'front') }), head ? _jsx(CardHead, { heading: heading, caption: caption, end: end, level: level }) : ownHeadKid, _jsx("div", { className: "card__middle", children: which === 'front' ? front : middle }), tail ? _jsx(CardFoot, { link: footLink, children: foot }) : ownFootKid] }, which));
121
+ const side = (which, middle) => (_jsxs("div", { className: cx('card__face flip__face', `flip__face--${which}`), ...((which === 'back') !== flipped ? { inert: true } : {}), children: [_jsx("button", { type: "button", className: "card__turn", "aria-label": which === 'front' ? toBackLabel ?? word('turnToBack') : toFrontLabel ?? word('turnToFront'), onClick: () => ask(which === 'front' ? 'back' : 'front') }), head ? _jsx(CardHead, { heading: heading, caption: caption, end: end, level: level }) : ownHeadKid, _jsx("div", { className: "card__middle", children: which === 'front' ? front : middle }), tail ? _jsx(CardFoot, { link: footLink, round: footRound, children: foot }) : ownFootKid] }, which));
122
122
  return createElement(as, props, inner(_jsxs(_Fragment, { children: [side('front', null), side('back', back)] })));
123
123
  }
124
124
  if (!head && !tail)
125
125
  return createElement(as, props, inner(_jsxs(_Fragment, { children: [children, cross] })));
126
- return createElement(as, props, inner(_jsxs(_Fragment, { children: [head && _jsx(CardHead, { heading: heading, caption: caption, end: end, level: level }, "head"), Children.toArray(children), tail && _jsx(CardFoot, { link: footLink, children: foot }, "foot"), cross] })));
126
+ return createElement(as, props, inner(_jsxs(_Fragment, { children: [head && _jsx(CardHead, { heading: heading, caption: caption, end: end, level: level }, "head"), Children.toArray(children), tail && _jsx(CardFoot, { link: footLink, round: footRound, children: foot }, "foot"), cross] })));
127
127
  }
@@ -3,8 +3,13 @@ export interface CardFootProps extends ComponentProps<'div'> {
3
3
  /** a SMALL LINK under the buttons, centred on its own short row (a `Link sm` — "details for developers") — the same as an
4
4
  * ActionBar's ctaLink (Lh 2026-09-23). It never takes a share of the buttons' line */
5
5
  link?: ReactNode;
6
+ /** a foot of a few ROUND buttons (`IconBtn`, a size `lg` / `xl` for the big ones): they keep their own size instead of sharing the line, and
7
+ * stand as ONE group in the middle of the foot, a fixed gap apart (wider than the Btns' — `--card-foot-gap` sets it). The Btn foot is
8
+ * unchanged: this is the other foot, for a few big round answers */
9
+ round?: boolean;
6
10
  }
7
11
  /** CardFoot — the card's action buttons on one line: one takes the width, up to four share it evenly. Btns only in the row; a small
8
12
  * link goes in `link`, under it. A `Btn form` keeps its own size here: as wide as its words, centred — for a foot whose one button
9
- * should not fill the card. */
10
- export declare function CardFoot({ link, className, children, ...rest }: CardFootProps): import("react").JSX.Element;
13
+ * should not fill the card. With `round` it is the foot of a few round `IconBtn`s (`size="xl"`, one `primary` with a word, the others
14
+ * with icons): one centred group, a fixed gap between them. */
15
+ export declare function CardFoot({ link, round, className, children, ...rest }: CardFootProps): import("react").JSX.Element;
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
3
  /** CardFoot — the card's action buttons on one line: one takes the width, up to four share it evenly. Btns only in the row; a small
4
4
  * link goes in `link`, under it. A `Btn form` keeps its own size here: as wide as its words, centred — for a foot whose one button
5
- * should not fill the card. */
6
- export function CardFoot({ link, className, children, ...rest }) {
7
- return (_jsxs("div", { className: cx('card__foot', link != null && 'card__foot--link', className), ...rest, children: [children, link != null && _jsx("div", { className: "card__foot-link", children: link })] }));
5
+ * should not fill the card. With `round` it is the foot of a few round `IconBtn`s (`size="xl"`, one `primary` with a word, the others
6
+ * with icons): one centred group, a fixed gap between them. */
7
+ export function CardFoot({ link, round, className, children, ...rest }) {
8
+ return (_jsxs("div", { className: cx('card__foot', link != null && 'card__foot--link', round && 'card__foot--round', className), ...rest, children: [children, link != null && _jsx("div", { className: "card__foot-link", children: link })] }));
8
9
  }