@usableapp/cardds 0.1.7 → 0.2.1

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
@@ -56,56 +56,69 @@
56
56
  --screen-w: 25rem; /* 400: mobile column — .screen, .action-bar, .sheet-stage */
57
57
  --screen-centre: 40%; /* the human centre of a screen: the middle of its top four fifths — the bottom fifth is the thumb's (Lh, 2026-09-10). .card--centre sits here */
58
58
 
59
- /* ---- spacing (4pt grid) ---- */
60
- --sp-0: 0.125rem; /* 2 hairline gaps (title→meta) */
61
- --sp-1: 0.25rem; /* 4 */
62
- --sp-2: 0.5rem; /* 8 */
63
- --sp-3: 0.75rem; /* 12 */
64
- --sp-4: 1rem; /* 16 */
65
- --sp-5: 1.25rem; /* 20 */
66
- --sp-6: 1.5rem; /* 24 */
67
- --sp-8: 2rem; /* 32 */
68
- --sp-10: 2.5rem; /* 40 */
69
- --sp-12: 3rem; /* 48 */
59
+ /* ---- the four bases: what a project retunes (Lh, 2026-09-20) ----
60
+ Every size token below is multiplier × its family's base. A project sets a BASE
61
+ and never a multiplier, so the proportions of the system hold at any size.
62
+ The px in the comments are at these defaults (1rem = 16px). */
63
+ --sp-base: 0.25rem; /* 4 spacing: --sp-N = base × N */
64
+ --fs-base: 1rem; /* 16 type: --fs-* */
65
+ --r-base: 1rem; /* 16 radius: --r-* (--r-chip stays fully round) */
66
+ --tap-base: 3rem; /* 48 the size of THINGS: touch targets and control heights (--tap*, --btn-h, --chip-h, --bar-h,
67
+ --header-h, --fab), icons, avatars, dots, the ring and the tiles. --sp-base is the room BETWEEN things.
68
+ Text-holders follow the type instead: --badge and --icon-xs (inline with captions) are on --fs-base.
69
+ Not on a base: --screen-w (the column), borders / rules / focus (px, crisp), shadows, motion. */
70
+
71
+ /* ---- spacing (4pt grid): the number in the name is the multiplier ---- */
72
+ --sp-0: calc(var(--sp-base) * 0.5); /* 2 hairline gaps (title→meta) */
73
+ --sp-1: var(--sp-base); /* 4 */
74
+ --sp-2: calc(var(--sp-base) * 2); /* 8 */
75
+ --sp-3: calc(var(--sp-base) * 3); /* 12 */
76
+ --sp-4: calc(var(--sp-base) * 4); /* 16 */
77
+ --sp-5: calc(var(--sp-base) * 5); /* 20 */
78
+ --sp-6: calc(var(--sp-base) * 6); /* 24 */
79
+ --sp-8: calc(var(--sp-base) * 8); /* 32 */
80
+ --sp-10: calc(var(--sp-base) * 10); /* 40 */
81
+ --sp-12: calc(var(--sp-base) * 12); /* 48 */
70
82
 
71
83
  /* ---- touch & control sizes ---- */
72
- --tap: 3rem; /* 48 primary touch target */
73
- --tap-sm: 2.75rem; /* 44 minimum touch target (Apple HIG floor) */
74
- --tap-lg: 3.5rem; /* 56 hero controls: track steps, send, field with action */
75
- --btn-h: 3.25rem; /* 52 button height */
76
- --btn-xl-h: 5.5rem; /* 88 the one giant CTA */
77
- --chip-h: 2.375rem; /* 38 chip height */
78
- --bar-h: 4rem; /* 64 float bar, day cell, deck actions */
79
- --header-h: 3.75rem;/* 60 top bar — .topbar and .appbar alike */
84
+ --tap-floor: 2.75rem; /* 44 the HIG floor — no --tap-base takes a touch target under it */
85
+ --tap: max(var(--tap-floor), var(--tap-base)); /* 48 primary touch target */
86
+ --tap-sm: max(var(--tap-floor), calc(var(--tap-base) * 11 / 12)); /* 44 minimum touch target */
87
+ --tap-lg: max(var(--tap-floor), calc(var(--tap-base) * 7 / 6)); /* 56 hero controls: track steps, send, field with action */
88
+ --btn-h: calc(var(--tap-base) * 13 / 12); /* 52 button height */
89
+ --btn-xl-h: calc(var(--tap-base) * 11 / 6); /* 88 the one giant CTA */
90
+ --chip-h: calc(var(--tap-base) * 19 / 24); /* 38 chip height */
91
+ --bar-h: calc(var(--tap-base) * 4 / 3); /* 64 float bar, day cell, deck actions */
92
+ --header-h: calc(var(--tap-base) * 5 / 4); /* 60 top bar — .topbar and .appbar alike */
80
93
  --screen-pad: var(--sp-4); /* 16 the screen's inset — .sheet-stack cancels it */
81
- --map-ring: 10rem; /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
82
- --fab: 4.5rem; /* 72 square floating action */
83
- --badge: 1.625rem; /* 26 count badge */
84
- --dot: 0.625rem; /* 10 signal dot */
85
- --dot-xs: 0.375rem; /* 6 marker dot */
94
+ --map-ring: calc(var(--tap-base) * 10 / 3); /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
95
+ --fab: calc(var(--tap-base) * 3 / 2); /* 72 square floating action */
96
+ --badge: calc(var(--fs-base) * 1.625); /* 26 count badge */
97
+ --dot: calc(var(--tap-base) * 5 / 24); /* 10 signal dot */
98
+ --dot-xs: calc(var(--tap-base) / 8); /* 6 marker dot */
86
99
 
87
100
  /* ---- icons (icons.svg sprite, Lucide) ---- */
88
- --icon: 1.75rem; /* 28 default */
89
- --icon-sm: 1.75rem; /* one size everywhere; retune if small contexts appear */
90
- --icon-lg: 2rem; /* 32 empty states */
91
- --icon-xs: 1rem; /* 16 inline with captions/chips only (row meta, link arrows) */
101
+ --icon: calc(var(--tap-base) * 7 / 12); /* 28 default */
102
+ --icon-sm: calc(var(--tap-base) * 7 / 12); /* one size everywhere; retune if small contexts appear */
103
+ --icon-lg: calc(var(--tap-base) * 2 / 3); /* 32 empty states */
104
+ --icon-xs: var(--fs-base); /* 16 inline with captions/chips only (row meta, link arrows) */
92
105
 
93
106
  /* ---- people & big elements ---- */
94
- --avatar-sm: 2.5rem; /* 40 row lead */
95
- --avatar: 3.5rem; /* 56 card head, person row */
96
- --avatar-lg: 5rem; /* 80 chooser row */
97
- --avatar-xl: 6rem; /* 96 sheet head icon ring */
98
- --ring: 11rem; /* 176 .ring gauge diameter */
99
- --ring-w: 1.375rem; /* 22 .ring stroke */
100
- --tile-min: 7.5rem; /* 120 .tile-grid column floor */
101
- --tile-h: 8.25rem; /* 132 tile / band height */
107
+ --avatar-sm: calc(var(--tap-base) * 5 / 6); /* 40 row lead */
108
+ --avatar: calc(var(--tap-base) * 7 / 6); /* 56 card head, person row */
109
+ --avatar-lg: calc(var(--tap-base) * 5 / 3); /* 80 chooser row */
110
+ --avatar-xl: calc(var(--tap-base) * 2); /* 96 sheet head icon ring */
111
+ --ring: calc(var(--tap-base) * 11 / 3); /* 176 .ring gauge diameter */
112
+ --ring-w: calc(var(--tap-base) * 11 / 24); /* 22 .ring stroke */
113
+ --tile-min: calc(var(--tap-base) * 5 / 2); /* 120 .tile-grid column floor */
114
+ --tile-h: calc(var(--tap-base) * 11 / 4); /* 132 tile / band height */
102
115
 
103
116
  /* ---- radius ---- */
104
117
  --r-chip: 999px; /* pills, chips, buttons — always fully round */
105
- --r-card: 2rem; /* 32 the one card radius: every card, row, tile, menu, sheet, fab */
106
- --r-media: 1rem; /* 16 images inside cards */
107
- --r-control: 0.875rem; /* 14 fields, thumbs, menu rows */
108
- --r-badge: 0.625rem; /* 10 tile badge, bars */
118
+ --r-card: calc(var(--r-base) * 2); /* 32 the one card radius: every card, row, tile, menu, sheet, fab */
119
+ --r-media: var(--r-base); /* 16 images inside cards */
120
+ --r-control: calc(var(--r-base) * 0.875); /* 14 fields, thumbs, menu rows */
121
+ --r-badge: calc(var(--r-base) * 0.625); /* 10 tile badge, bars */
109
122
 
110
123
  /* ---- borders / rules / elevation (px on purpose: crisp at any scale) ---- */
111
124
  --border-w: 1.5px;
@@ -126,22 +139,22 @@
126
139
  --font-display: "FC Pride", var(--font-ui); /* the character face for headings and big numbers (fonts.css) */
127
140
  --font-mono: "SF Mono", "JetBrains Mono", ui-monospace, monospace;
128
141
 
129
- --fs-xs: 0.6875rem; /* 11 overline */
130
- --fs-caption: 0.75rem; /* 12 */
131
- --fs-label: 0.8125rem; /* 13 */
132
- --fs-sm: 0.875rem; /* 14 chips, segments */
133
- --fs-body: 0.9375rem; /* 15 */
134
- --fs-md: 1rem; /* 16 buttons */
135
- --fs-title: 1.0625rem; /* 17 */
136
- --fs-lg: 1.25rem; /* 20 day numbers, tile values */
137
- --fs-h2: 1.375rem; /* 22 */
138
- --fs-2xl: 1.5rem; /* 24 composer, small ring */
139
- --fs-h1: 1.75rem; /* 28 */
140
- --fs-3xl: 2rem; /* 32 xl button */
141
- --fs-display: clamp(2.125rem, 9vw, 2.625rem); /* 34–42 */
142
- --fs-stat: clamp(1.875rem, 8vw, 2.375rem); /* 30–38 big numbers */
143
- --fs-hero: 3.5rem; /* 56 the one number on a sheet */
144
- --fs-giant: clamp(3rem, 15vw, 4rem); /* 48–64 data bands */
142
+ --fs-xs: calc(var(--fs-base) * 0.6875); /* 11 overline */
143
+ --fs-caption: calc(var(--fs-base) * 0.75); /* 12 */
144
+ --fs-label: calc(var(--fs-base) * 0.8125); /* 13 */
145
+ --fs-sm: calc(var(--fs-base) * 0.875); /* 14 chips, segments */
146
+ --fs-body: calc(var(--fs-base) * 0.9375); /* 15 */
147
+ --fs-md: var(--fs-base); /* 16 buttons */
148
+ --fs-title: calc(var(--fs-base) * 1.0625); /* 17 */
149
+ --fs-lg: calc(var(--fs-base) * 1.25); /* 20 day numbers, tile values */
150
+ --fs-h2: calc(var(--fs-base) * 1.375); /* 22 */
151
+ --fs-2xl: calc(var(--fs-base) * 1.5); /* 24 composer, small ring */
152
+ --fs-h1: calc(var(--fs-base) * 1.75); /* 28 */
153
+ --fs-3xl: calc(var(--fs-base) * 2); /* 32 xl button */
154
+ --fs-display: clamp(calc(var(--fs-base) * 2.125), 9vw, calc(var(--fs-base) * 2.625)); /* 34–42 */
155
+ --fs-stat: clamp(calc(var(--fs-base) * 1.875), 8vw, calc(var(--fs-base) * 2.375)); /* 30–38 big numbers */
156
+ --fs-hero: calc(var(--fs-base) * 3.5); /* 56 the one number on a sheet */
157
+ --fs-giant: clamp(calc(var(--fs-base) * 3), 15vw, calc(var(--fs-base) * 4)); /* 48–64 data bands */
145
158
 
146
159
  /* level weight size / line-height family */
147
160
  --type-display: 800 var(--fs-display) / 1.15 var(--font-display); /* leading: Thai marks stack to 1.1em up and 0.3em down in FC Pride — 1.02 clipped them (2026-09-10) */
@@ -161,35 +174,53 @@
161
174
  /* ============================================================
162
175
  COLOR ROLES — wireframe defaults (grayscale).
163
176
  palette.css overrides these under [data-palette="…"].
177
+
178
+ THE FIVE COLOUR BASES (Lh, 2026-09-20) — a project's own colours, the
179
+ same idea as the size bases: set a base, the roles follow.
180
+ --color-surface the app's ground
181
+ --color-ink the words
182
+ --color-paper the plain card, neutral buttons and fields
183
+ --color-brand the primary action, the strong card, the signal dot
184
+ --color-on-brand the words on the brand
185
+ They have NO default: unset, every role below is its exact wireframe
186
+ value. That is what the fallbacks are for — a private mix (--_*) that
187
+ reads an unset base is invalid, so var(--_x, #grey) lands on the grey.
188
+ Set, the roles derive: tone 2 = the brand washed into the surface, the
189
+ muted inks = the ink washed into their ground. A role set directly
190
+ (a palette, or a project's theme.css) always wins over its derivation.
164
191
  ============================================================ */
192
+ --_muted-on-surface: color-mix(in srgb, var(--color-ink) 66%, var(--color-surface, #f2f2f0));
193
+ --_muted-on-paper: color-mix(in srgb, var(--color-ink) 66%, var(--color-paper, #ffffff));
194
+ --_tone-2: color-mix(in srgb, var(--color-brand) 14%, var(--color-surface, #f2f2f0));
195
+ --_muted-on-tone-2: color-mix(in srgb, var(--color-ink, #141414) 72%, var(--_tone-2));
165
196
 
166
197
  /* app background ("earth-toned surface or shiny color, up to the app") */
167
- --surface: #f2f2f0;
168
- --on-surface: #141414;
169
- --on-surface-muted: #6b6b6b;
198
+ --surface: var(--color-surface, #f2f2f0);
199
+ --on-surface: var(--color-ink, #141414);
200
+ --on-surface-muted: var(--_muted-on-surface, #6b6b6b);
170
201
 
171
202
  /* card boundaries — 3 slots so stacked/adjacent cards can differ */
172
- --card-1-bg: #ffffff; --card-1-ink: #141414;
173
- --card-2-bg: #e6e6e3; --card-2-ink: #141414;
174
- --card-3-bg: #141414; --card-3-ink: #ffffff;
203
+ --card-1-bg: var(--color-paper, #ffffff); --card-1-ink: var(--color-ink, #141414);
204
+ --card-2-bg: var(--_tone-2, #e6e6e3); --card-2-ink: var(--color-ink, #141414);
205
+ --card-3-bg: var(--color-brand, #141414); --card-3-ink: var(--color-on-brand, #ffffff);
175
206
 
176
207
  --card-border: #141414; /* wireframe: visible outline; palettes may set transparent */
177
- --card-muted: #6b6b6b; /* secondary text inside cards (the default, tone-1 card) */
178
- --card-2-muted: #5f5f5f; /* the same text on the tone-2 card — 5.1:1 on its grey (#6b6b6b is 4.3:1, under AA);
208
+ --card-muted: var(--_muted-on-paper, #6b6b6b); /* secondary text inside cards (the default, tone-1 card) */
209
+ --card-2-muted: var(--_muted-on-tone-2, #5f5f5f); /* the same text on the tone-2 card — 5.1:1 on its grey (#6b6b6b is 4.3:1, under AA);
179
210
  a palette whose tone-2 ground is strong
180
211
  sets this to its tone-2 ink — a muted shade cannot reach AA there (clay) */
181
212
 
182
213
  /* actions */
183
- --accent: #141414; /* primary CTA fill */
184
- --on-accent: #ffffff;
185
- --action-bg: #ffffff; /* neutral buttons/chips/inputs */
186
- --action-ink: #141414;
214
+ --accent: var(--color-brand, #141414); /* primary CTA fill */
215
+ --on-accent: var(--color-on-brand, #ffffff);
216
+ --action-bg: var(--color-paper, #ffffff); /* neutral buttons/chips/inputs */
217
+ --action-ink: var(--color-ink, #141414);
187
218
  --action-border: #141414;
188
219
 
189
220
  /* signals (chip dots, deltas) */
190
221
  --pos: #8a8a8a;
191
222
  --neg: #3d3d3d;
192
- --dot-a: #141414;
223
+ --dot-a: var(--color-brand, #141414);
193
224
  --dot-b: #8a8a8a;
194
225
 
195
226
  /* derived tints — read the element's own ink, so they work on any slot */
@@ -330,6 +361,11 @@
330
361
 
331
362
  /* Colored palettes trade wireframe outlines for soft elevation. */
332
363
  [data-palette] :is(.card, .band-stack) { box-shadow: var(--shadow-card); }
364
+ /* COLOURED MODE is the attribute, whatever its value: a project with colours of its own (the five
365
+ --color-* bases, or roles, in its theme.css) writes <html data-palette="own"> — no block here matches
366
+ "own", so only the three generic rules apply: the card's shadow, the soft outline, the field's soft
367
+ edge (forms.css); and the two wireframe borders step back, as in every stock palette. */
368
+ [data-palette] { --card-border: transparent; --action-border: transparent; }
333
369
  /* …but controls keep a faint edge so they still read as controls */
334
370
  [data-palette] { --outline: color-mix(in srgb, currentColor 25%, transparent); }
335
371
 
@@ -688,7 +724,7 @@ button { font: inherit; cursor: pointer; }
688
724
  and updates inside it; a browser without them (the floor: Chrome 105 / iOS 16)
689
725
  simply switches. Only the card is named; the rest of the page must not
690
726
  cross-fade. */
691
- .screen__centre > .card, .pages > .card[aria-current] { view-transition-name: centre-card; }
727
+ .screen__centre > .card, .pages > [aria-current] { view-transition-name: centre-card; }
692
728
 
693
729
  /* ---- pages: the cards of a walk, all one height ----
694
730
  A sequence the member walks (an introduction) reads steadier when every card
@@ -702,8 +738,14 @@ button { font: inherit; cursor: pointer; }
702
738
  <article class="card" inert>…</article>
703
739
  </div><div class="pager__at">…</div></div> */
704
740
  .pages { display: grid; }
705
- .pages > .card { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
706
- .pages > .card[inert] { visibility: hidden; }
741
+ .pages > :is(.card, .base-content) { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
742
+ .pages > [inert] { visibility: hidden; }
743
+ /* pages of WORDS (Lh, 2026-09-20): the same walk with .base-content pages, on the base — in a .step the
744
+ .pages is the reading block (step.css), the round pager in the bar turns it, the same slideTo().
745
+ <section class="step"><div class="pages">
746
+ <section class="base-content" aria-current="step">…</section>
747
+ <section class="base-content" inert>…</section>
748
+ </div></section> <div class="action-bar action-bar--pager">← →</div> */
707
749
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
708
750
  ::view-transition-old(centre-card), ::view-transition-new(centre-card) {
709
751
  animation-duration: var(--motion-slide);
@@ -1123,6 +1165,19 @@ button { font: inherit; cursor: pointer; }
1123
1165
  .sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
1124
1166
  .sheet--3q { --_y: calc(var(--sheet-3q) * 100%); --_cover: calc(var(--sheet-3q) * var(--_box)); }
1125
1167
  .sheet--peek { --_y: calc(100% - var(--sheet-peek)); --_cover: calc(var(--_box) - var(--sheet-peek)); }
1168
+ /* ---- called sheets (Lh, 2026-09-20): a .step whose .base-content has buttons that each CALL a sheet ----
1169
+ .sheet--away = not on the screen: slid all the way out (translate, like every state — the material never
1170
+ changes), and hidden once it has left so its shadow does not lie on the step's bottom edge; write it
1171
+ with `inert`. A button names its sheet — <button aria-controls="when"> … <article id="when" class="card
1172
+ sheet sheet--away" inert data-sheet-states="peek full"> — and cardds.js does the rest: a tap slides that
1173
+ sheet up to full and sends any other one away; the handle takes it down to peek, never further, so the
1174
+ words and the other buttons are back in reach; the same button again raises it from peek to full.
1175
+ Nothing waits at peek before the first call. No bar on such a screen (actions.css).
1176
+ WHAT IT IS FOR: the base offers WAYS IN — sign in · new phone · join — and each button calls a sheet
1177
+ that holds that way's WHOLE form, its submit on the sheet's foot, leading to the next page. The base
1178
+ carries no control. It is NOT for a form taken apart — one sheet per field, a "send" left on the base:
1179
+ a form lives whole in one sheet. */
1180
+ .sheet--away { --_y: 100%; --_cover: var(--_box); visibility: hidden; transition: translate var(--motion-sheet) ease, visibility 0s linear var(--motion-sheet); }
1126
1181
  .sheet.is-dragging { transition: none; } /* follows the finger; the snap gets the ease back */
1127
1182
 
1128
1183
  /* the body — everything under the head, the one thing in a sheet that
@@ -1148,6 +1203,31 @@ button { font: inherit; cursor: pointer; }
1148
1203
  cut is a share of the SHEET's height, not of the body's. */
1149
1204
  scroll-padding-bottom: var(--_cover);
1150
1205
  }
1206
+ /* ---- the sheet's foot: the button that FINISHES the sheet, on the sheet (Lh, 2026-09-20) ----
1207
+ A screen with a sheet has no bar in front of it: the button that completes what the sheet asks — the
1208
+ page's conclusion — is a .card__foot, the sheet's last child, on the sheet's own material. The sheet is
1209
+ full height at every state, so its real bottom edge is under the cut unless it stands at full. So the
1210
+ foot is a layer as tall as the sheet, its buttons at its bottom, slid UP by the very share the sheet
1211
+ slid down (--_y, + the drag): the two cancel, and the buttons stand on the box's bottom edge at half,
1212
+ 3q and full alike while the sheet slides behind them. A percentage of the same height on both — not a
1213
+ length — so a box that resizes (the keyboard) moves them as one, with nothing to catch up. The layer
1214
+ takes no taps, its buttons do; the strip behind them is the sheet's ground, --_foot tall, and the body
1215
+ stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
1216
+ One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
1217
+ .sheet { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad)); }
1218
+ .sheet > .card__foot {
1219
+ position: absolute; inset: calc(-1 * var(--border-w)) 0 0; z-index: 1; /* the sheet's border box: the same height the sheet's own % reads */
1220
+ align-items: end;
1221
+ padding: 0 var(--card-pad) var(--card-pad);
1222
+ background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
1223
+ pointer-events: none;
1224
+ translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
1225
+ transition: translate var(--motion-sheet) ease, opacity var(--motion-sheet) ease, visibility 0s;
1226
+ }
1227
+ .sheet > .card__foot > * { pointer-events: auto; }
1228
+ .sheet:has(> .card__foot) > .sheet__body { margin-bottom: calc(var(--_foot) - var(--card-pad)); } /* the body stops above the strip (the card's own padding is the rest) */
1229
+ .sheet.is-dragging > .card__foot { transition: none; }
1230
+ :is(.sheet--peek, .sheet--away) > .card__foot { opacity: 0; visibility: hidden; transition: translate var(--motion-sheet) ease, opacity var(--motion-sheet) ease, visibility 0s linear var(--motion-sheet); }
1151
1231
  .sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
1152
1232
  .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: var(--card-pad); }
1153
1233
 
@@ -1339,14 +1419,16 @@ button { font: inherit; cursor: pointer; }
1339
1419
  /* ============================================================
1340
1420
  cardds/step.css — .step: one screen, one task.
1341
1421
  What the member reads (words on the base — not a card, Lh 2026-09-16), what the member does when the
1342
- step asks for it (a sheet, sheet.css), and the screen's one
1343
- move (.action-bar, actions.css) OUTSIDE the step, below it.
1422
+ step asks for it (a sheet, sheet.css). The move: a page of words has
1423
+ an .action-bar (actions.css) OUTSIDE the step, below it — the base's;
1424
+ a step with a sheet has none: the sheet's own .card__foot finishes it
1425
+ (see "LAYERS" below).
1344
1426
 
1345
1427
  <section class="step">
1346
1428
  <section class="base-content">…the words…</section> ← first
1347
- <article class="card sheet sheet--half">…the controls…</article> ← last, optional
1429
+ <article class="card sheet sheet--half">…the controls… <div class="card__foot">…</div></article> ← last, optional
1348
1430
  </section>
1349
- <div class="action-bar">…</div>
1431
+ <div class="action-bar">…</div> ← only when there is no sheet
1350
1432
 
1351
1433
  The step is the box the sheet slides in. It fills the screen
1352
1434
  column between the top bar's reserve (.screen already pads for
@@ -1396,6 +1478,10 @@ button { font: inherit; cursor: pointer; }
1396
1478
  bar. Any parent that hosts the bar counts — <body>, an app shell, a demo
1397
1479
  frame — the bar is fixed to the viewport wherever it sits. */
1398
1480
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
1481
+ /* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
1482
+ move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
1483
+ (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. Should a bar be written
1484
+ beside a sheet anyway it stands down (actions.css + cardds.js) — a net for a mistake, not a layout. */
1399
1485
 
1400
1486
  /* the reading block: a .base-content (base.css) — the same words-on-the-base
1401
1487
  block as anywhere else (it was .step__card, and a .card before 2026-09-16;
@@ -1405,14 +1491,23 @@ button { font: inherit; cursor: pointer; }
1405
1491
  gets the flex rule: it hugs its content, and when the column runs out (a
1406
1492
  short phone, large text) it shrinks and scrolls inside itself instead of
1407
1493
  pushing the bar or the sheet */
1408
- .step > .base-content {
1494
+ .step > .base-content, .step > .pages > .base-content {
1409
1495
  --card-pad: 0rem; /* published for .bleed: nothing to cancel */
1410
1496
  --card-gap: var(--sp-4);
1411
1497
  gap: var(--card-gap);
1412
1498
  padding-block: 0; /* --_lead carries the top; the bar's room is the step's */
1413
1499
  align-content: start;
1414
1500
  color: var(--on-surface);
1501
+ }
1502
+ /* the reading block is the .base-content — or the .pages that holds several of them, a walk of words
1503
+ (card.css). --sheet-room: the strip a CALLED sheet keeps at peek (sheet.css, "called sheets"), published
1504
+ on the step by cardds.js while one is on the screen, so the buttons under it can scroll clear of it;
1505
+ nothing without one. */
1506
+ .step > :is(.base-content, .pages) {
1507
+ padding-bottom: var(--sheet-room, 0rem);
1415
1508
  flex: 0 1 auto;
1509
+ }
1510
+ .step > :is(.base-content, .pages) {
1416
1511
  min-height: 0;
1417
1512
  margin: var(--_lead, 0) var(--screen-pad) 0; /* the screen's gap, moved in here from above the step; the inset, taken back from the bleed */
1418
1513
  overflow-y: auto;
@@ -1784,6 +1879,14 @@ button { font: inherit; cursor: pointer; }
1784
1879
  <button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
1785
1880
  </div> — or the same two in an .action-bar__tier--pager under a control */
1786
1881
  .action-bar--pager { align-items: center; }
1882
+ /* The bar belongs to the BASE (Lh, 2026-09-20): it is the move of a page of words — a pager turning
1883
+ them, or one wide button. It is never in front of a sheet: a sheet that is on the screen covers it
1884
+ (step.css, "layers"), a peeking one included, and the button that finishes a sheet is the sheet's own
1885
+ foot (sheet.css). So a pager never works beside a sheet — that screen has work that cannot be walked
1886
+ past — and neither does any other bar: while a sheet is on (only .sheet--away, a sheet not called yet,
1887
+ is not), the bar takes no pointer here and cardds.js makes it inert, so the keyboard cannot reach what
1888
+ the eye cannot see. */
1889
+ :has(.step > .sheet:not(.sheet--away)) > .action-bar { pointer-events: none; }
1787
1890
  /* the page dots: under the card in a .screen__centre group; tight gaps */
1788
1891
  .pager__at { flex: 0 1 auto; min-width: 0; overflow: hidden; display: flex; align-items: center; gap: var(--sp-1); }
1789
1892
  .pager__at > * {
@@ -2547,6 +2650,56 @@ button { font: inherit; cursor: pointer; }
2547
2650
  .kv-cell__key { font: var(--type-caption); color: var(--card-muted); }
2548
2651
  .card--3 .kv-cell__key { color: inherit; opacity: 0.7; }
2549
2652
 
2653
+ /* ---- bubble: a chat message — theirs on the left, mine on the right (Lh 2026-09-18, replaces the chip's tail) ----
2654
+ No tail: a rounded block whose corner nearest the sender is square-ish
2655
+ (bottom-left for theirs, bottom-right for mine), the way messaging apps
2656
+ draw it. Under it a caption line (time · a tick), beside it small round
2657
+ actions (a heart, more). A .thread stacks them.
2658
+ <div class="thread">
2659
+ <div class="bubble"><p class="bubble__body">…</p><span class="bubble__actions">…icon-btns…</span><span class="bubble__meta">Dec 4 · 8:15</span></div>
2660
+ <div class="bubble bubble--right">…</div>
2661
+ </div> */
2662
+ .thread { display: grid; gap: var(--sp-4); }
2663
+ /* two columns: the message (max-content, but it gives way — minmax(0) — so a long one shrinks
2664
+ and wraps while the actions stay beside it) and the actions; the meta on a second row under the message */
2665
+ .bubble {
2666
+ display: grid;
2667
+ grid-template-columns: minmax(0, max-content) auto;
2668
+ justify-content: start;
2669
+ align-items: center;
2670
+ gap: var(--sp-1) var(--sp-3);
2671
+ max-width: 88%;
2672
+ margin-right: auto; /* theirs: hugs the left */
2673
+ }
2674
+ .bubble--right { grid-template-columns: auto minmax(0, max-content); justify-content: end; margin-right: 0; margin-left: auto; } /* mine: hugs the right, the actions on its left */
2675
+ .bubble__body, .bubble__meta { grid-column: 1; }
2676
+ .bubble__actions { grid-column: 2; grid-row: 1; }
2677
+ .bubble--right > :is(.bubble__body, .bubble__meta) { grid-column: 2; }
2678
+ .bubble--right > .bubble__actions { grid-column: 1; }
2679
+ .bubble__body {
2680
+ margin: 0;
2681
+ min-width: 0;
2682
+ padding: var(--sp-3) var(--sp-4);
2683
+ border-radius: var(--r-card);
2684
+ border-bottom-left-radius: var(--r-control);
2685
+ background: var(--card-2-bg);
2686
+ color: var(--card-2-ink);
2687
+ font: var(--type-body);
2688
+ overflow-wrap: anywhere;
2689
+ }
2690
+ .bubble--right > .bubble__body {
2691
+ border-bottom-left-radius: var(--r-card);
2692
+ border-bottom-right-radius: var(--r-control);
2693
+ background: var(--accent);
2694
+ color: var(--on-accent);
2695
+ text-align: right;
2696
+ }
2697
+ .bubble__actions { display: flex; gap: var(--sp-1); flex: none; opacity: 0.7; }
2698
+ .bubble__meta { display: flex; align-items: center; gap: var(--sp-1); font: var(--type-caption); color: var(--on-surface-muted); }
2699
+ .bubble--right > .bubble__meta { justify-self: end; }
2700
+ .bubble__meta > .icon { color: var(--accent); }
2701
+ .card--3 .bubble__meta { color: inherit; opacity: 0.7; }
2702
+
2550
2703
  /* ---- timeline: a hairline with date chips, entries to the right ----
2551
2704
  <ol class="timeline"><li class="timeline__item"><span class="timeline__mark">Wed 14</span><p class="t-body">…</p></li>…</ol> */
2552
2705
  .timeline {
@@ -3119,22 +3272,6 @@ button { font: inherit; cursor: pointer; }
3119
3272
  }
3120
3273
  .callout__title { font: var(--type-title); }
3121
3274
  .callout__sub { font: var(--type-caption); opacity: 0.7; }
3122
- /* speech chip: a chip with the same tail (mascot / greeting) */
3123
- .chip--bubble { position: relative; }
3124
- .chip--bubble::after {
3125
- content: "";
3126
- position: absolute;
3127
- left: var(--sp-4);
3128
- bottom: calc(-1 * var(--sp-2) + var(--border-w));
3129
- width: var(--sp-3);
3130
- height: var(--sp-3);
3131
- rotate: 45deg;
3132
- background: inherit;
3133
- border: inherit;
3134
- border-top: 0;
3135
- border-left: 0;
3136
- }
3137
-
3138
3275
  /* ---- deck: swipe cards; the last child is on top ----
3139
3276
  <div class="deck"><article class="card">…</article><article class="card">…</article><article class="card card--fold">front</article></div>
3140
3277
  <div class="deck__actions"><button class="icon-btn">×</button><button class="icon-btn icon-btn--invert">✓</button></div> */
package/dist/index.d.ts CHANGED
@@ -59,6 +59,7 @@ export * from './lists/Row.js';
59
59
  export * from './lists/Kv.js';
60
60
  export * from './lists/Timeline.js';
61
61
  export * from './lists/Legend.js';
62
+ export * from './lists/Bubble.js';
62
63
  export * from './media/Quote.js';
63
64
  export * from './media/Postcard.js';
64
65
  export * from './media/MapArea.js';
package/dist/index.js CHANGED
@@ -62,6 +62,7 @@ export * from './lists/Row.js';
62
62
  export * from './lists/Kv.js';
63
63
  export * from './lists/Timeline.js';
64
64
  export * from './lists/Legend.js';
65
+ export * from './lists/Bubble.js';
65
66
  export * from './media/Quote.js';
66
67
  export * from './media/Postcard.js';
67
68
  export * from './media/MapArea.js';
@@ -0,0 +1,18 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
+ export interface ThreadProps extends ComponentPropsWithoutRef<'div'> {
3
+ }
4
+ /** Thread — a stack of Bubbles: a conversation. */
5
+ export declare function Thread({ className, ...rest }: ThreadProps): import("react").JSX.Element;
6
+ export interface BubbleProps extends ComponentPropsWithoutRef<'div'> {
7
+ /** whose: theirs on the left (default), mine on the right */
8
+ side?: 'left' | 'right';
9
+ /** the caption under the message — a time, a tick (an Icon) */
10
+ meta?: ReactNode;
11
+ /** small round buttons beside the message (a heart, more) */
12
+ actions?: ReactNode;
13
+ }
14
+ /**
15
+ * Bubble — a chat message: a rounded block, its corner nearest the sender squared (bottom-left for theirs, bottom-right
16
+ * for mine), no tail. `meta` sits under it on the sender's side, `actions` beside it. Stack them in a Thread.
17
+ */
18
+ export declare function Bubble({ side, meta, actions, className, children, ...rest }: BubbleProps): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../cx.js';
3
+ /** Thread — a stack of Bubbles: a conversation. */
4
+ export function Thread({ className, ...rest }) {
5
+ return _jsx("div", { className: cx('thread', className), ...rest });
6
+ }
7
+ /**
8
+ * Bubble — a chat message: a rounded block, its corner nearest the sender squared (bottom-left for theirs, bottom-right
9
+ * for mine), no tail. `meta` sits under it on the sender's side, `actions` beside it. Stack them in a Thread.
10
+ */
11
+ export function Bubble({ side = 'left', meta, actions, className, children, ...rest }) {
12
+ return (_jsxs("div", { className: cx('bubble', side === 'right' && 'bubble--right', className), ...rest, children: [_jsx("p", { className: "bubble__body", children: children }), actions != null && _jsx("span", { className: "bubble__actions", children: actions }), meta != null && _jsx("span", { className: "bubble__meta", children: meta })] }));
13
+ }
@@ -5,10 +5,13 @@ export interface BaseContentProps extends ComponentPropsWithoutRef<'section'> {
5
5
  * BaseContent — words straight on the base, no card around them: an overline, a heading, a paragraph,
6
6
  * maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
7
7
  * Was Section (and, inside a Step, StepCard) until 2026-09-16 — one block, one name.
8
- * In a Step it is the reading block (first child; the buttons go to the ActionBar, not a CtaPack).
8
+ * In a Step it is the reading block (first child; the step's MOVE goes to the ActionBar, never here). It can move two ways
9
+ * (Lh 2026-09-20): several of them in a `Pages` = a walk of words the round pager turns (slideTo); or a CtaPack of CALLERS —
10
+ * `Btn aria-controls={a Sheet's id}` — WAYS IN (sign in · new phone · join), each calling a `Sheet state="away"` that holds that way's
11
+ * whole form and its submit. The base itself carries no control; a form is never taken apart across it. Never both: a pager never works beside a sheet.
9
12
  */
10
13
  export declare function BaseContent({ className, ...rest }: BaseContentProps): import("react").JSX.Element;
11
14
  export interface CtaPackProps extends ComponentPropsWithoutRef<'div'> {
12
15
  }
13
- /** CtaPack — the buttons under a BaseContent's words, wrapping on one line. Outside a Step only: a Step's move is its ActionBar. */
16
+ /** CtaPack — the buttons under a BaseContent's words, wrapping on one line. In a Step it holds CALLERS only (buttons that call a sheet, `aria-controls`) — the Step's move is its ActionBar. */
14
17
  export declare function CtaPack({ className, ...rest }: CtaPackProps): import("react").JSX.Element;
@@ -4,12 +4,15 @@ import { cx } from '../cx.js';
4
4
  * BaseContent — words straight on the base, no card around them: an overline, a heading, a paragraph,
5
5
  * maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
6
6
  * Was Section (and, inside a Step, StepCard) until 2026-09-16 — one block, one name.
7
- * In a Step it is the reading block (first child; the buttons go to the ActionBar, not a CtaPack).
7
+ * In a Step it is the reading block (first child; the step's MOVE goes to the ActionBar, never here). It can move two ways
8
+ * (Lh 2026-09-20): several of them in a `Pages` = a walk of words the round pager turns (slideTo); or a CtaPack of CALLERS —
9
+ * `Btn aria-controls={a Sheet's id}` — WAYS IN (sign in · new phone · join), each calling a `Sheet state="away"` that holds that way's
10
+ * whole form and its submit. The base itself carries no control; a form is never taken apart across it. Never both: a pager never works beside a sheet.
8
11
  */
9
12
  export function BaseContent({ className, ...rest }) {
10
13
  return _jsx("section", { className: cx('base-content', className), ...rest });
11
14
  }
12
- /** CtaPack — the buttons under a BaseContent's words, wrapping on one line. Outside a Step only: a Step's move is its ActionBar. */
15
+ /** CtaPack — the buttons under a BaseContent's words, wrapping on one line. In a Step it holds CALLERS only (buttons that call a sheet, `aria-controls`) — the Step's move is its ActionBar. */
13
16
  export function CtaPack({ className, ...rest }) {
14
17
  return _jsx("div", { className: cx('cta-pack', className), ...rest });
15
18
  }
@@ -14,9 +14,11 @@ export declare function Centre({ className, ...rest }: CentreProps): import("rea
14
14
  export interface PagesProps extends ComponentPropsWithoutRef<'div'> {
15
15
  }
16
16
  /**
17
- * Pages — the cards of a walk, all one height: every Card is rendered, stacked in one cell as tall as the
17
+ * Pages — the pages of a walk, all one height: every page is rendered, stacked in one cell as tall as the
18
18
  * tallest at any text size. Give the current one `aria-current="step"` and every other one `inert`
19
- * (hidden in place, out of the tab order). Goes inside a Centre, above the PagerAt; turn with slideTo().
19
+ * (hidden in place, out of the tab order). Turn with slideTo().
20
+ * Pages of cards: `Card`s, inside a Centre, above the PagerAt. Pages of words: `BaseContent`s, as a Step's
21
+ * reading block (no Sheet on that screen — a pager never works beside one), the round pager in the ActionBar.
20
22
  */
21
23
  export declare function Pages({ className, ...rest }: PagesProps): import("react").JSX.Element;
22
24
  /** the direction a Centre's card slides: `next` — out to the left, the new one in from the right; `back` — the reverse */
@@ -14,9 +14,11 @@ export function Centre({ className, ...rest }) {
14
14
  return _jsx("div", { className: cx('screen__centre', className), ...rest });
15
15
  }
16
16
  /**
17
- * Pages — the cards of a walk, all one height: every Card is rendered, stacked in one cell as tall as the
17
+ * Pages — the pages of a walk, all one height: every page is rendered, stacked in one cell as tall as the
18
18
  * tallest at any text size. Give the current one `aria-current="step"` and every other one `inert`
19
- * (hidden in place, out of the tab order). Goes inside a Centre, above the PagerAt; turn with slideTo().
19
+ * (hidden in place, out of the tab order). Turn with slideTo().
20
+ * Pages of cards: `Card`s, inside a Centre, above the PagerAt. Pages of words: `BaseContent`s, as a Step's
21
+ * reading block (no Sheet on that screen — a pager never works beside one), the round pager in the ActionBar.
20
22
  */
21
23
  export function Pages({ className, ...rest }) {
22
24
  return _jsx("div", { className: cx('pages', className), ...rest });
@@ -1,7 +1,7 @@
1
1
  import type { ComponentPropsWithoutRef } from 'react';
2
- export type SheetState = 'peek' | 'half' | '3q' | 'full';
2
+ export type SheetState = 'peek' | 'half' | '3q' | 'full' | 'away';
3
3
  export interface SheetProps extends ComponentPropsWithoutRef<'article'> {
4
- /** how far the sheet slid: `peek` (handle + title row) · `half` (default) · `3q` (three quarters showing) · `full` (its top edge at the top bar) — a class, never a height. A tap on the handle toggles between this state and peek; a drag snaps to any of `states` */
4
+ /** how far the sheet slid: `peek` (handle + title row) · `half` (default) · `3q` (three quarters showing) · `full` (its top edge at the top bar) — a class, never a height. `away` = a CALLED sheet not on the screen yet (slid out, inert): a `Btn aria-controls={its id}` in the Step's BaseContent calls it up to full and sends any other away. A tap on the handle toggles between this state and peek; a drag snaps to any of `states` */
5
5
  state?: SheetState;
6
6
  /** floats above a footnote strip on a dim stage (bottom corners and border back) */
7
7
  raised?: boolean;
@@ -15,7 +15,10 @@ export interface SheetProps extends ComponentPropsWithoutRef<'article'> {
15
15
  * content cards). Always the full height of its box — a Step or a SheetStage — a state is only how far it slid.
16
16
  * Shape: handle · head (a CardHead, a SheetHead — or ONE label Row carrying a Check/Toggle, when the sheet's
17
17
  * whole ask is that switch and peek must show it) · SheetBody (the only part that scrolls). Written top-down.
18
- * Inside a Step it never carries the step's action (the ActionBar is the one move); its own buttons only on a
19
- * screen with no bar (the map sheet). This is the single sheet; the menu of stacked sheets is `SheetStack`.
18
+ * It FINISHES on itself: a `CardFoot` as its last child holds the one button that completes it — standing at the bottom at
19
+ * half / 3q / full, gone at peek. Never an ActionBar in front of a sheet (a bar is the base's). Nothing in the body commits
20
+ * on its own. Older note — its own buttons only on a
21
+ * screen with no bar (the map sheet). Called sheets: several `Sheet state="away" states="peek full"` in one Step, each with an `id` a
22
+ * button names — one on the screen at a time; never beside a pager. This is the single sheet; the menu of stacked sheets is `SheetStack`.
20
23
  */
21
24
  export declare function Sheet({ state, raised, handle, states, className, children, ...rest }: SheetProps): import("react").JSX.Element;