@usableapp/cardds 0.7.18 → 0.7.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  > For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
4
4
 
5
+ ## 0.7.19 — 2026-09-28
6
+
7
+ ### Fixed
8
+ - SAFE AREA (Lh, timebank on Android Chrome: the bar's ← → buttons lay half under the gesture bar). With `viewport-fit=cover` the phone draws the page under its own bars; cardds kept nothing clear of them. Now it publishes `--safe-top` / `--safe-bottom` (`env(safe-area-inset-*)`, 0 without cover) and keeps clear of them: the `ActionBar`'s buttons (its ground still runs to the edge), a peeked `Sheet`'s head, a sheet's foot button and the end of its body, `FloatBar`, `Fab`, a stage's foot; a fill `Screen` starts under the status bar. A keyboard up covers the gesture bar, so the bottom room then goes to the keyboard alone. `--bar-reserve` includes it.
9
+ - README says the ONE viewport meta cardds expects: `width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content`.
10
+ - Nothing to change in a screen. A component of the app's own pinned to an edge reads `--safe-top` / `--safe-bottom`.
11
+
5
12
  ## 0.7.18 — 2026-09-27
6
13
 
7
14
  ### Fixed
package/README.md CHANGED
@@ -42,6 +42,19 @@ import '@usableapp/cardds/dist/cardds.css';
42
42
  import './theme.css'; // the project's own look, after the system's — see "A project's theme"
43
43
  ```
44
44
 
45
+ **The viewport meta cardds expects** (one way for every app):
46
+
47
+ ```html
48
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
49
+ ```
50
+
51
+ `viewport-fit=cover` = the app runs edge to edge under the phone's own bars; cardds publishes their room as
52
+ `--safe-top` / `--safe-bottom` (from `env(safe-area-inset-*)`, 0 without cover) and keeps what the member reads or taps
53
+ clear of them — the bottom bar's buttons, a sheet's peeked head, its foot and the end of its body, the float bar, the FAB,
54
+ a stage's foot; a fill `Screen` starts under the status bar — while every ground still runs to the edge. A
55
+ component of the app's own pinned to an edge reads the same two tokens. `interactive-widget=resizes-content` =
56
+ Android shrinks the page for the keyboard (`--kb` stays 0 there; iOS publishes it).
57
+
45
58
  Every behaviour is the component's own React code — the `Sheet`'s handle (tap, drag,
46
59
  keyboard) with the focus and on-screen-keyboard raises, the `SheetStack` opening and
47
60
  closing, the `Pin` auto-advance, the `Modal`'s leaving — each controlled like an input
package/css/actions.css CHANGED
@@ -94,7 +94,7 @@
94
94
  .icon-btn--invert { background: var(--icon-btn-invert-bg, var(--accent)); color: var(--icon-btn-invert-ink, var(--on-accent)); border-color: transparent; }
95
95
  .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); }
96
96
  /* fab: the square floating action, bottom-right of a stage or screen */
97
- .fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3)); box-shadow: var(--shadow-float); z-index: var(--z-fab); }
97
+ .fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3) + var(--safe-bottom)); box-shadow: var(--shadow-float); z-index: var(--z-fab); }
98
98
 
99
99
  /* ---- buttons ---- */
100
100
  .btn {
@@ -220,13 +220,13 @@
220
220
  (step.css); on a scrolling screen a sticky last row (below). --bar-reserve stays published — the bar's nominal
221
221
  height — for the one rule that still needs it without measuring: a .card--centre's cap (card.css).
222
222
  --kb is the on-screen keyboard's height (cardds.js): the bar rides above it. */
223
- :root { --bar-reserve: calc(var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
223
+ :root { --bar-reserve: calc(var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
224
224
  .action-bar {
225
225
  position: relative;
226
226
  z-index: var(--z-bar); /* above .sheet-stack sheets, which paint at --level 1-5 */
227
227
  max-width: var(--screen-w);
228
228
  margin-inline: auto;
229
- padding: var(--sp-3) var(--sp-4) var(--sp-5);
229
+ padding: var(--sp-3) var(--sp-4) calc(var(--sp-5) + var(--_safe-b-open)); /* its ground runs under the phone's gesture bar, its buttons stand above it */
230
230
  display: flex;
231
231
  gap: var(--sp-2);
232
232
  }
@@ -293,12 +293,12 @@
293
293
  .action-bar__tier { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--tap); }
294
294
  .action-bar__tier > .btn { flex: 1; }
295
295
  .action-bar__tier--link { justify-content: center; min-height: 0; } /* a Link under the buttons, e.g. "details" that opens a Modal — its row as tall as a small link, not a tap (Lh 2026-09-23) */
296
- :has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
296
+ :has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
297
297
  /* more rows (Lh 2026-09-23: a bar may hold several rows of controls — keeping it short is the designer's job): each row past two
298
298
  adds a tap-tall row and its gap to the reserve, so the words above never run under the bar */
299
- :has(> .action-bar--tiers > .action-bar__tier:nth-child(3)) { --bar-reserve: calc(2 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
300
- :has(> .action-bar--tiers > .action-bar__tier:nth-child(4)) { --bar-reserve: calc(3 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
301
- :has(> .action-bar--tiers > .action-bar__tier:nth-child(5)) { --bar-reserve: calc(4 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
299
+ :has(> .action-bar--tiers > .action-bar__tier:nth-child(3)) { --bar-reserve: calc(2 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
300
+ :has(> .action-bar--tiers > .action-bar__tier:nth-child(4)) { --bar-reserve: calc(3 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
301
+ :has(> .action-bar--tiers > .action-bar__tier:nth-child(5)) { --bar-reserve: calc(4 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
302
302
 
303
303
  /* the first round button sits at the left edge, the last at the right; a skip link between them stays centred */
304
304
  :is(.action-bar--pager, .action-bar__tier--pager) > .icon-btn:first-child { margin-right: auto; }
package/css/base.css CHANGED
@@ -105,6 +105,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
105
105
  display: flex;
106
106
  flex-direction: column;
107
107
  height: 100dvh;
108
+ padding-top: var(--safe-top); /* under the phone's status bar when the app runs edge to edge (Lh 2026-09-28) */
108
109
  overflow: clip;
109
110
  position: relative; /* the box a .card--centre or a .screen__centre floats in (card.css) */
110
111
  container-type: size; /* a share of the SCREEN's height (a no-bar step's lead, step.css) reads it as cqh — a real phone's height, or the gallery's frame */
package/css/layover.css CHANGED
@@ -198,7 +198,7 @@
198
198
  .float-bar {
199
199
  position: fixed;
200
200
  inset-inline: 0;
201
- bottom: var(--sp-5);
201
+ bottom: calc(var(--sp-5) + var(--safe-bottom));
202
202
  max-width: var(--screen-w);
203
203
  margin-inline: auto;
204
204
  padding-inline: var(--float-bar-pad-x, var(--sp-4));
package/css/sheet.css CHANGED
@@ -81,7 +81,7 @@
81
81
  border-radius: var(--r-card) var(--r-card) 0 0;
82
82
  border-bottom: 0;
83
83
  box-shadow: var(--sheet-shadow, var(--shadow-sheet));
84
- --_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad)); /* the foot strip's height (below) — merged here, one bare .sheet rule */
84
+ --_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad) + var(--_safe-b-open)); /* the foot strip's height (below) — merged here, one bare .sheet rule */
85
85
  }
86
86
  .sheet--full { --_y: 0%; --_cover: 0rem; }
87
87
  .sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
@@ -90,7 +90,7 @@
90
90
  /* --sheet-peek is declared on the BOX the sheet slides in, not on the sheet: the words behind a peeked sheet
91
91
  have to read it too (step.css keeps that much room under them), and a token only flows down. The sheet
92
92
  inherits it. A sheet whose head is taller retunes it on the box. */
93
- :is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--sheet-head-h)); }
93
+ :is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--sheet-head-h) + var(--safe-bottom)); } /* + the gesture bar: the peeked head stands above it */
94
94
  /* the head's floor: a tap, or an h2 line with its Thai-mark padding when the type is bigger than that (a large text-size
95
95
  setting on a narrow phone) — the strip and the head read the same number, so the title row is always inside the strip */
96
96
  :root { --sheet-head-h: max(var(--tap), calc(var(--fs-h2) * 1.25 + var(--sp-2))); }
@@ -148,12 +148,12 @@
148
148
  stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
149
149
  One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
150
150
  /* …and the link row under the button, when there is one (its line + the gap over it) */
151
- .sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) + var(--card-pad)); }
151
+ .sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) + var(--card-pad) + var(--_safe-b-open)); }
152
152
  .sheet > .card__foot {
153
153
  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 */
154
154
  align-items: end;
155
155
  align-content: end;
156
- padding: 0 var(--card-pad) var(--card-pad);
156
+ padding: 0 var(--card-pad) calc(var(--card-pad) + var(--_safe-b-open));
157
157
  background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
158
158
  pointer-events: none;
159
159
  translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
@@ -169,7 +169,7 @@
169
169
  .sheet > :not(.sheet__body) { flex-shrink: 0; }
170
170
  :is(.sheet--peek, .sheet--away) > .sheet__body { padding-bottom: 0; }
171
171
  .sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
172
- .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover)); }
172
+ .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover) + var(--_safe-b-open)); }
173
173
 
174
174
  /* grab handle — first child of the sheet. Drawn as a small bar, but it is
175
175
  the thumb's grab area for the whole strip above the title: a pseudo widens
@@ -349,7 +349,9 @@
349
349
  position: absolute;
350
350
  inset-inline: 0;
351
351
  bottom: 0;
352
- height: var(--tap-lg);
352
+ height: calc(var(--tap-lg) + var(--_safe-b-open));
353
+ padding-bottom: var(--_safe-b-open);
354
+ box-sizing: border-box;
353
355
  margin: 0;
354
356
  display: grid;
355
357
  place-items: center;
package/css/step.css CHANGED
@@ -136,7 +136,7 @@
136
136
  margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge; its own padding is the inset */
137
137
  margin-bottom: calc(var(--sp-2) - var(--screen-gap));
138
138
  }
139
- .screen.screen--fill:has(> .appbar) { padding-top: 0; --header-gap: 0rem; }
139
+ .screen.screen--fill:has(> .appbar) { padding-top: var(--safe-top); --header-gap: 0rem; }
140
140
  /* …and a STEP touches it, as it touches a TopBar: a full sheet's top edge on the bar's bottom edge (0.6.5 left 8px of base between
141
141
  them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
142
142
  .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
package/css/tokens.css CHANGED
@@ -238,6 +238,13 @@
238
238
  sheet.css sets the values; these initial values are what a sheet
239
239
  falls back to when its own declaration cannot resolve.
240
240
  ============================================================ */
241
+ /* SAFE AREA (Lh 2026-09-28, timebank on Android: the bar's buttons lay under the gesture bar) — the room the phone's own
242
+ bars take over the page when the app runs edge to edge (viewport-fit=cover: Android Chrome, an iPhone home-screen app).
243
+ PUBLISHED: backgrounds still run to the edge; what the member reads or taps stands clear of them. 0 without cover.
244
+ --_safe-b-open = the bottom inset the keyboard does NOT already cover (a keyboard up hides the gesture bar). */
245
+ :root { --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); }
246
+ * { --_safe-b-open: max(0px, var(--safe-bottom) - var(--kb, 0px)); }
247
+
241
248
  @property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
242
249
  @property --sheet-head-h { syntax: "<length>"; inherits: true; initial-value: 48px; } /* the sheet head's floor (sheet.css) — registered so the Sheet reads it resolved */
243
250
  @property --_lead { syntax: "<length>"; inherits: true; initial-value: 0px; } /* the room above a step's words: registered so a cqh share (a no-bar step, 15% of the SCREEN) resolves where it is declared — on the step, against the screen — not on the words, against the step */
package/dist/cardds.css CHANGED
@@ -279,6 +279,13 @@
279
279
  sheet.css sets the values; these initial values are what a sheet
280
280
  falls back to when its own declaration cannot resolve.
281
281
  ============================================================ */
282
+ /* SAFE AREA (Lh 2026-09-28, timebank on Android: the bar's buttons lay under the gesture bar) — the room the phone's own
283
+ bars take over the page when the app runs edge to edge (viewport-fit=cover: Android Chrome, an iPhone home-screen app).
284
+ PUBLISHED: backgrounds still run to the edge; what the member reads or taps stands clear of them. 0 without cover.
285
+ --_safe-b-open = the bottom inset the keyboard does NOT already cover (a keyboard up hides the gesture bar). */
286
+ :root { --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); }
287
+ * { --_safe-b-open: max(0px, var(--safe-bottom) - var(--kb, 0px)); }
288
+
282
289
  @property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
283
290
  @property --sheet-head-h { syntax: "<length>"; inherits: true; initial-value: 48px; } /* the sheet head's floor (sheet.css) — registered so the Sheet reads it resolved */
284
291
  @property --_lead { syntax: "<length>"; inherits: true; initial-value: 0px; } /* the room above a step's words: registered so a cqh share (a no-bar step, 15% of the SCREEN) resolves where it is declared — on the step, against the screen — not on the words, against the step */
@@ -553,6 +560,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
553
560
  display: flex;
554
561
  flex-direction: column;
555
562
  height: 100dvh;
563
+ padding-top: var(--safe-top); /* under the phone's status bar when the app runs edge to edge (Lh 2026-09-28) */
556
564
  overflow: clip;
557
565
  position: relative; /* the box a .card--centre or a .screen__centre floats in (card.css) */
558
566
  container-type: size; /* a share of the SCREEN's height (a no-bar step's lead, step.css) reads it as cqh — a real phone's height, or the gallery's frame */
@@ -1355,7 +1363,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1355
1363
  border-radius: var(--r-card) var(--r-card) 0 0;
1356
1364
  border-bottom: 0;
1357
1365
  box-shadow: var(--sheet-shadow, var(--shadow-sheet));
1358
- --_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad)); /* the foot strip's height (below) — merged here, one bare .sheet rule */
1366
+ --_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad) + var(--_safe-b-open)); /* the foot strip's height (below) — merged here, one bare .sheet rule */
1359
1367
  }
1360
1368
  .sheet--full { --_y: 0%; --_cover: 0rem; }
1361
1369
  .sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
@@ -1364,7 +1372,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1364
1372
  /* --sheet-peek is declared on the BOX the sheet slides in, not on the sheet: the words behind a peeked sheet
1365
1373
  have to read it too (step.css keeps that much room under them), and a token only flows down. The sheet
1366
1374
  inherits it. A sheet whose head is taller retunes it on the box. */
1367
- :is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--sheet-head-h)); }
1375
+ :is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--sheet-head-h) + var(--safe-bottom)); } /* + the gesture bar: the peeked head stands above it */
1368
1376
  /* the head's floor: a tap, or an h2 line with its Thai-mark padding when the type is bigger than that (a large text-size
1369
1377
  setting on a narrow phone) — the strip and the head read the same number, so the title row is always inside the strip */
1370
1378
  :root { --sheet-head-h: max(var(--tap), calc(var(--fs-h2) * 1.25 + var(--sp-2))); }
@@ -1422,12 +1430,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1422
1430
  stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
1423
1431
  One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
1424
1432
  /* …and the link row under the button, when there is one (its line + the gap over it) */
1425
- .sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) + var(--card-pad)); }
1433
+ .sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) + var(--card-pad) + var(--_safe-b-open)); }
1426
1434
  .sheet > .card__foot {
1427
1435
  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 */
1428
1436
  align-items: end;
1429
1437
  align-content: end;
1430
- padding: 0 var(--card-pad) var(--card-pad);
1438
+ padding: 0 var(--card-pad) calc(var(--card-pad) + var(--_safe-b-open));
1431
1439
  background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
1432
1440
  pointer-events: none;
1433
1441
  translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
@@ -1443,7 +1451,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1443
1451
  .sheet > :not(.sheet__body) { flex-shrink: 0; }
1444
1452
  :is(.sheet--peek, .sheet--away) > .sheet__body { padding-bottom: 0; }
1445
1453
  .sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
1446
- .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover)); }
1454
+ .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover) + var(--_safe-b-open)); }
1447
1455
 
1448
1456
  /* grab handle — first child of the sheet. Drawn as a small bar, but it is
1449
1457
  the thumb's grab area for the whole strip above the title: a pseudo widens
@@ -1623,7 +1631,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1623
1631
  position: absolute;
1624
1632
  inset-inline: 0;
1625
1633
  bottom: 0;
1626
- height: var(--tap-lg);
1634
+ height: calc(var(--tap-lg) + var(--_safe-b-open));
1635
+ padding-bottom: var(--_safe-b-open);
1636
+ box-sizing: border-box;
1627
1637
  margin: 0;
1628
1638
  display: grid;
1629
1639
  place-items: center;
@@ -1832,7 +1842,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1832
1842
  margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge; its own padding is the inset */
1833
1843
  margin-bottom: calc(var(--sp-2) - var(--screen-gap));
1834
1844
  }
1835
- .screen.screen--fill:has(> .appbar) { padding-top: 0; --header-gap: 0rem; }
1845
+ .screen.screen--fill:has(> .appbar) { padding-top: var(--safe-top); --header-gap: 0rem; }
1836
1846
  /* …and a STEP touches it, as it touches a TopBar: a full sheet's top edge on the bar's bottom edge (0.6.5 left 8px of base between
1837
1847
  them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
1838
1848
  .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
@@ -2098,7 +2108,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2098
2108
  .icon-btn--invert { background: var(--icon-btn-invert-bg, var(--accent)); color: var(--icon-btn-invert-ink, var(--on-accent)); border-color: transparent; }
2099
2109
  .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); }
2100
2110
  /* fab: the square floating action, bottom-right of a stage or screen */
2101
- .fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3)); box-shadow: var(--shadow-float); z-index: var(--z-fab); }
2111
+ .fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3) + var(--safe-bottom)); box-shadow: var(--shadow-float); z-index: var(--z-fab); }
2102
2112
 
2103
2113
  /* ---- buttons ---- */
2104
2114
  .btn {
@@ -2224,13 +2234,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2224
2234
  (step.css); on a scrolling screen a sticky last row (below). --bar-reserve stays published — the bar's nominal
2225
2235
  height — for the one rule that still needs it without measuring: a .card--centre's cap (card.css).
2226
2236
  --kb is the on-screen keyboard's height (cardds.js): the bar rides above it. */
2227
- :root { --bar-reserve: calc(var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
2237
+ :root { --bar-reserve: calc(var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
2228
2238
  .action-bar {
2229
2239
  position: relative;
2230
2240
  z-index: var(--z-bar); /* above .sheet-stack sheets, which paint at --level 1-5 */
2231
2241
  max-width: var(--screen-w);
2232
2242
  margin-inline: auto;
2233
- padding: var(--sp-3) var(--sp-4) var(--sp-5);
2243
+ padding: var(--sp-3) var(--sp-4) calc(var(--sp-5) + var(--_safe-b-open)); /* its ground runs under the phone's gesture bar, its buttons stand above it */
2234
2244
  display: flex;
2235
2245
  gap: var(--sp-2);
2236
2246
  }
@@ -2297,12 +2307,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2297
2307
  .action-bar__tier { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--tap); }
2298
2308
  .action-bar__tier > .btn { flex: 1; }
2299
2309
  .action-bar__tier--link { justify-content: center; min-height: 0; } /* a Link under the buttons, e.g. "details" that opens a Modal — its row as tall as a small link, not a tap (Lh 2026-09-23) */
2300
- :has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
2310
+ :has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
2301
2311
  /* more rows (Lh 2026-09-23: a bar may hold several rows of controls — keeping it short is the designer's job): each row past two
2302
2312
  adds a tap-tall row and its gap to the reserve, so the words above never run under the bar */
2303
- :has(> .action-bar--tiers > .action-bar__tier:nth-child(3)) { --bar-reserve: calc(2 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
2304
- :has(> .action-bar--tiers > .action-bar__tier:nth-child(4)) { --bar-reserve: calc(3 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
2305
- :has(> .action-bar--tiers > .action-bar__tier:nth-child(5)) { --bar-reserve: calc(4 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
2313
+ :has(> .action-bar--tiers > .action-bar__tier:nth-child(3)) { --bar-reserve: calc(2 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
2314
+ :has(> .action-bar--tiers > .action-bar__tier:nth-child(4)) { --bar-reserve: calc(3 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
2315
+ :has(> .action-bar--tiers > .action-bar__tier:nth-child(5)) { --bar-reserve: calc(4 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
2306
2316
 
2307
2317
  /* the first round button sits at the left edge, the last at the right; a skip link between them stays centred */
2308
2318
  :is(.action-bar--pager, .action-bar__tier--pager) > .icon-btn:first-child { margin-right: auto; }
@@ -3717,7 +3727,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3717
3727
  .float-bar {
3718
3728
  position: fixed;
3719
3729
  inset-inline: 0;
3720
- bottom: var(--sp-5);
3730
+ bottom: calc(var(--sp-5) + var(--safe-bottom));
3721
3731
  max-width: var(--screen-w);
3722
3732
  margin-inline: auto;
3723
3733
  padding-inline: var(--float-bar-pad-x, var(--sp-4));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.18",
3
+ "version": "0.7.19",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth); gallery/ shows every story live (npm run dev), tests/ measures the geometry.",