@usableapp/cardds 0.7.100 → 0.7.102

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/css/base.css +20 -0
  3. package/css/card.css +93 -0
  4. package/css/choice.css +22 -0
  5. package/css/forms.css +15 -3
  6. package/css/journey.css +23 -1
  7. package/css/media.css +1 -10
  8. package/css/sheet.css +21 -1
  9. package/css/tokens.css +2 -2
  10. package/dist/cardds.css +197 -17
  11. package/dist/cards/Card.d.ts +31 -3
  12. package/dist/cards/Card.js +33 -8
  13. package/dist/cards/CardParts.d.ts +9 -3
  14. package/dist/cards/CardParts.js +41 -3
  15. package/dist/cards/cardGo.d.ts +2 -0
  16. package/dist/cards/cardGo.js +6 -1
  17. package/dist/catalog.json +1 -1
  18. package/dist/choice/Calendar.js +5 -1
  19. package/dist/choice/CalendarWeeks.d.ts +41 -0
  20. package/dist/choice/CalendarWeeks.js +64 -0
  21. package/dist/choice/calendarDay.d.ts +13 -0
  22. package/dist/choice/calendarDay.js +6 -0
  23. package/dist/forms/Composer.d.ts +1 -1
  24. package/dist/forms/Composer.js +1 -1
  25. package/dist/forms/Field.d.ts +9 -1
  26. package/dist/forms/Field.js +35 -24
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1 -0
  29. package/dist/journey/JourneyBadge.d.ts +5 -2
  30. package/dist/journey/JourneyBadge.js +3 -3
  31. package/dist/media/MapArea.d.ts +10 -4
  32. package/dist/media/MapArea.js +8 -3
  33. package/dist/media/Postcard.d.ts +1 -1
  34. package/dist/media/Postcard.js +2 -2
  35. package/dist/post/PostDesk.d.ts +2 -1
  36. package/dist/post/PostDesk.js +11 -13
  37. package/dist/room.d.ts +12 -0
  38. package/dist/room.js +38 -0
  39. package/dist/sticky/Sticky.js +5 -38
  40. package/dist/type/Icon.d.ts +1 -1
  41. package/dist/type/Icon.js +1 -1
  42. package/dist/type/icons.d.ts +1 -1
  43. package/dist/type/icons.js +2 -2
  44. package/dist/words.d.ts +1 -1
  45. package/dist/words.js +2 -2
  46. package/icons.svg +1 -0
  47. package/lint/rules.mjs +2 -2
  48. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,41 @@
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. Each release section after 0.7.97 carries one `Components:` line, written by `node scripts/components-changed.mjs <previous tag>`: the components the release changed, or `all` when a file changed that any component can read (a token, the build), or `none`; check only the screens that import those.
4
4
 
5
+ ## 0.7.102 — 2026-10-19
6
+
7
+ A framed write-in of several lines: `Field rows`; an icon among words stays on the line; a date badge in a card's corner takes the card's corner; a meeting place on a map is a pin at the centre, not the ring; a day picked from a run of weeks that scrolls, two in sight, the month and year on a head that stays; a card that turns over, a two-tone card, and a date badge that takes its form from the title beside it (Lh 2026-10-19, timebank's request card, approved design).
8
+
9
+ Components: all
10
+
11
+ ### Added
12
+ - **`Field rows={n}`: several lines in the same frame** (Lh 2026-10-19, timebank's ask sheet: the request and, under it, its reason). Before → after: a member's words had only two homes, a one-line `Field` (framed) and a `Composer` (bare, display type, the one big write-in), so two things to write on the ask sheet were a `Composer` with a `ComposerArea`, which Lh rejected as the wrong meaning: each must read as a framed box → `<Field rows={4} />` over `<Field rows={2} />`: the same outlined label wrapper (`.field.field--rows`) round a `textarea` of that many lines, the placeholder still the label, the request box taller than the reason box. `error`, `required`, `icon` and `action` work as on one line (the icon on the first line, the action at the top); `mask`, `format` and `digits` are for one line and are ignored with `rows` (warns once). A frame says "tap me", so a framed write-in is a `Field`, not a `Composer`: the `Composer` stays the one big bare write-in. Knob `--field-rows-pad-y` (`--sp-3`, the room above and below the lines). Gallery `Field/Rows` · `Field/RowsDark`.
13
+ - **A `JourneyBadge` first or last in a `CardHead` takes a corner concentric with the card's** (timebank's date box in the card's top corner, which set `--tile-badge-radius` by hand). Before → after: a badge in the head's corner kept the badge's own corner (13), so it sat loosely in the card's big one → the card's corner less its padding (`--r-card` minus `--card-pad`: 21.6 with the defaults), set by the card's first head on the badge at its edge. A theme's `--tile-badge-radius` still wins; no new knob. A badge in the middle of a head, on a media or on a sheet keeps `--r-badge`. Every badge that already stood last in a card's first head follows (the style snapshot names them: `Card/CardHeadCustom`, `Tile/JourneyBadgeGate`, `Route/Flight`, `Route/TwoLegs`, `Route/Dark`, `NoteRow/Transit`, `NoteRow/Dark`, and the unfilled badge of `Mosaic/PhotoCard`, whose corner shows nothing); it is the same rule, not a second one. Gallery `Tile/JourneyBadgeInHeadCorner` shows first and last.
14
+ - **`MapArea mark="pin"`: an exact point at the centre instead of the ring** (Lh 2026-10-19: every meeting position is a clear pin at the centre of the map, never the circle). Before → after: `MapArea` always drew the ring, the rough area, at the centre of its box (behind a stage, of the band a half sheet leaves open) → `mark="pin"` draws a pin there (`<span class="map__pin">` with the new `map-pin` icon), its TIP on the point the ring's centre sat, so a map engine reads the pin's bottom-centre (`ringRef` and `ringLabel` keep their names and now name the centre mark, whatever its shape; `ringLabel` defaults to "จุดนัด" for a pin, "ย่านของคุณโดยประมาณ" for the ring). The pin's ink is the map's own ground's (`--card-ink` on the tone-2 side it stands on, like `.map__hint`), not the inherited `--accent`: in a dark sheet that accent is the card's ondark ink, cream on clay's orange map. The default `mark="ring"` changes nothing; the ring stays for a rough area. Knobs `--map-pin-size` · `--map-pin-ink` · `--map-pin-drop`. New icon `map-pin`. Gallery `MapArea/Pin`, `MapArea/PinStage`.
15
+ - **`CalendarWeeks`: a run of weeks that scrolls week by week, `show` rows in sight (2 by default), the month · year of the first row in sight on a sticky head** (timebank, Lh 2026-10-19: the member picks a day for the help on a step with little height). Before → after: a day was picked from `Calendar` (one month in detail, too tall for a step, turning pages with prev/next) or `DayStrip` (one week, no scroll, no month) → `<CalendarWeeks weeks={…} selected={key} onSelect={…} />` shows two week rows under the weekday heads, snaps one week per scroll, and the head's label follows the first row in sight; `onLabelClick` makes the label a button for the app's own month / year pick. It opens at the week that holds today (Lh 2026-10-19: "ปฏิทินต้องแสดงวันนี้เสมอ"), that row the first in sight at once, no scroll to watch; with no `today` in the data it opens at the first week. The app builds every date: `weeks: { label, days: { key, num, today?, disabled?, mark?: 'soft' | 'strong' }[] }[]`. The cells are Calendar's (`.calendar__day`, every `--calendar-*` knob reaches both); a day with `today` carries `aria-current="date"` (underlined). Knobs `--calendar-weeks-head-bg` · `--calendar-label-font` · `--calendar-label-ink`. Gallery `CalendarWeeks/TwoWeeks` · `Controlled` · `LabelPick` · `Thai`. On a sheet, never in a Card.
16
+ - **`Card back`: the whole card turns over.** Before → after: a card had one face; a thing read on both sides (a request, its QR on the back) needed two cards or a hand-built flip → `back={…}` gives the card a back middle; a tap anywhere on the card off a control turns the whole card (3D, `--motion-flip`; an instant swap under prefers-reduced-motion) and taps again turn it back. The app owns the side: `face` + `onFaceChange`, or `defaultFace`. The head and the foot are given once and drawn on both faces (so give them no `id`); the hidden face is `inert`; a hidden turn button on each face is the keyboard's way (`toBackLabel` / `toFrontLabel`, default the words' `turnToBack` / new `turnToFront`). The middle is one fixed height on both faces, knob `--card-middle-h` (default 18.75rem), so the card keeps its height. Not on `row`, `sm` or `stack` cards (warns, ignored). Gallery `Card/Turns`, `Card/TurnsTwoLineTitle`, `Card/TurnsControlled`.
17
+ - **`Card paper="bands"`: the two-tone card.** Before → after: `paper` made the content white and kept the head's colour → `paper="bands"` is the opposite: the head and the foot are white paper bands edge to edge over the card's padding (the foot band slim), no hairline, and the middle keeps the card's tone lightened, knob `--card-middle-bg` (default `color-mix(in srgb, var(--card-bg) 35%, var(--card-1-bg))`); the middle's words take the paper's side (35%, not timebank's 40%: at 40% the words on clay's tone 3 fall to 4.29:1). Gallery `Card/PaperBands`.
18
+ - **`JourneyBadge stretch`: a date in a head.** As tall as the title block beside it and no taller; two lines beside a one-line title (day over number · month), three beside a two-line title. Knob `--tile-badge-line` (0.95).
19
+ - **A ChipRow in a card head** takes a line of its own under the title, at most two lines, a chip's words cut with an ellipsis.
20
+
21
+ ### Changed
22
+ - **The flip mechanic is shared.** The turn (`.flip`, `.flip__face`, `.flip__face--front`, `.flip__face--back`, `.is-flipped`) now lives once in base.css; `Postcard` and `Card back` both read it. Postcard is unchanged on screen (style snapshot: 0 property differences over 477 stories).
23
+
24
+ ### Fixed
25
+ - **An `Icon` among words stays on the line** (timebank 2026-10-19: "clock 12:30 hourglass 1 ชั่วโมง" could not sit on one line). Before → after: `img, svg { display: block }` made an `Icon` inside a `Text` a block, so the story `Icon/InContext` put the map icon on a line above "Telok Ayer Park · 1.2 km" (a caption of 23 became 40 tall, a body of 33 became 80 with two icons) → an icon that is a direct child of a type level (`Text`) is `inline-block`, centred on the words' x-height, and the line keeps its height. An icon in a `Btn`, `Chip`, `Link`, `Row` or `IconBtn` is a flex item and does not move. Gallery `Text/IconInLine`.
26
+
27
+ ## 0.7.101 — 2026-10-19
28
+
29
+ A list that keeps only a few past entries in sight, a cross that takes a row card off its list, and a postcard that keeps the caret where it was when its paper is full.
30
+
31
+ Components: CallSheet, Card, CardBody, CardEnd, CardFeed, CardFoot, CardHead, CardList, CardMedia, CardThumb, Delta, FeedCard, Home, IconRow, NumberBand, NumberBandStack, Placeholder, PostDesk, PostDeskLine, Sheet, SheetStack, StackSheet, Stat, StatRow, Sticky, Walkthrough
32
+
33
+ ### Added
34
+ - **`CardList show`: a short window of past entries that scrolls in itself** (Lh 2026-10-19, the past requests under a request form). Before → after: a `CardList` was as long as its rows and scrolled only with what held it, so a list of past requests under a form pushed the form's buttons away → `show={2.5}` keeps two whole rows and half of the third in sight (counted in rows, measured, so rows of any height work) and the rest scrolls inside the list. Knob `--card-list-window-pad` (the room inside the scroll box for the focus ring). Gallery `Card/CardListWindow`.
35
+ - **`Card row onRemove` + `removeLabel`: a small cross at a row card's top right corner** that takes the row off its list. Smaller than a button (half `--tap-base`, 24, growing with the size of things), its touch target 44; a touch on it never picks the row, and a row card with only the cross is not pressed whole. The app removes the entry from its own list. Knobs `--card-remove-size` · `--card-remove-inset` · `--card-remove-ink` · `--card-remove-bg`.
36
+
37
+ ### Fixed
38
+ - **`PostDesk` keeps the caret where it was when the paper refuses words** (the paper-is-full rule, now one with `Sticky`'s). Before → after: a line typed in the middle of a full message was refused but the caret jumped to the end of the message, and a Thai or Japanese composition that would not fit could slip in mid-composition → the addition is refused where it was typed, the caret stays, a composition that would not fit is put back whole when it ends (the app is told), `onMessageChange` never hears a refused input and `onFull` is still called. A `StickyField` on a `Sticky` now also refuses a same-length replacement that would overflow (it took it before). No prop, class or knob changes.
39
+
5
40
  ## 0.7.100 — 2026-10-19
6
41
 
7
42
  The check only (`cardds check` and the ESLint plugin); no component, prop, class or token changes. The line below reads `all` only because a script of the build changed (`scripts/ci.mjs`, the release's own check); no screen needs a new look.
package/css/base.css CHANGED
@@ -78,6 +78,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
78
78
  .icon--sm { width: var(--icon-sm-size, var(--icon-sm)); height: var(--icon-sm-size, var(--icon-sm)); }
79
79
  .icon--xs { width: var(--icon-xs-size, var(--icon-xs)); height: var(--icon-xs-size, var(--icon-xs)); } /* inside captions, chips, links */
80
80
  .icon--lg { width: var(--icon-lg-size, var(--icon-lg)); height: var(--icon-lg-size, var(--icon-lg)); } /* empty states */
81
+ /* an icon among words sits ON the line: `img, svg { display: block }` above would put it on a line of its own. Only a direct child of a
82
+ type level is running text; an icon in a button, chip, link or row is a flex/grid item and never matches. */
83
+ :is(.t-display, .t-h1, .t-h2, .t-title, .t-body, .t-label, .t-caption) > .icon { display: inline-block; vertical-align: middle; }
81
84
 
82
85
  /* mobile screen column — the standard horizontal inset, and nothing vertical.
83
86
  Top is zero on purpose: room there is only ever a RESERVE for something
@@ -209,3 +212,20 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
209
212
  .empty-state__action { margin-top: var(--sp-2); }
210
213
  /* inside a card (a card's own "nothing yet"): the card's ink */
211
214
  .card .empty-state { color: var(--card-ink); padding-block: var(--sp-6); }
215
+
216
+ /* ---- the turn: one flip mechanic for everything with two faces (a Postcard, a Card's `back`).
217
+ The faces share one grid cell and turn on Y at --motion-flip; where motion is off they swap at once.
218
+ The owner sets the box's size and the faces' look; .is-flipped on .flip shows the back. */
219
+ .flip { perspective: 60rem; display: grid; }
220
+ .flip__face {
221
+ grid-area: 1 / 1;
222
+ backface-visibility: hidden;
223
+ -webkit-backface-visibility: hidden;
224
+ transition: transform var(--motion-flip) ease-in-out;
225
+ }
226
+ .flip__face--back { transform: rotateY(180deg); }
227
+ .flip.is-flipped > .flip__face--front { transform: rotateY(-180deg); }
228
+ .flip.is-flipped > .flip__face--back { transform: rotateY(0); }
229
+ @media (prefers-reduced-motion: reduce) {
230
+ .flip__face { transition: none; }
231
+ }
package/css/card.css CHANGED
@@ -96,6 +96,56 @@
96
96
  .card__head:has(> .stamp) > :is(.t-caption, .t-body) { grid-column: 1; grid-row: 2; align-self: start; }
97
97
  .card__head > .stamp { grid-column: 2; grid-row: 1 / span 2; margin-inline: 0; }
98
98
 
99
+ /* 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
100
+ 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,
101
+ so a theme that sets that knob still wins. */
102
+ .card > .card__head:first-child > .tile-badge:is(:first-child, :last-child) { --_badge-radius: calc(var(--r-card) - var(--card-pad)); }
103
+
104
+ /* ---- paper bands: the two-tone card (`Card paper="bands"`, Lh 2026-10-19): the head and the foot are white paper bands edge to
105
+ edge over the card's padding, no hairline; the middle between them keeps the card's tone, lightened (--card-middle-bg). The card
106
+ clips, so the bands keep its corners. A two-faced card (below) carries the look on each face. */
107
+ :is(.card--bands, .card--bands > .card__face) {
108
+ background: var(--card-middle-bg, color-mix(in srgb, var(--card-bg) 35%, var(--card-1-bg)));
109
+ --dark: var(--card-1-dark); /* the middle is mostly paper: its words take the paper's side */
110
+ overflow: hidden;
111
+ }
112
+ :is(.card--bands, .card--bands > .card__face) > :is(.card__head, .card__foot) {
113
+ --card-bg: var(--card-1-bg);
114
+ --dark: var(--card-1-dark);
115
+ background: var(--card-bg);
116
+ color: var(--card-ink);
117
+ margin-inline: calc(-1 * var(--card-pad));
118
+ padding-inline: var(--card-pad);
119
+ }
120
+ :is(.card--bands, .card--bands > .card__face) > .card__head { margin-top: calc(-1 * var(--card-pad)); padding-block: var(--card-pad); }
121
+ :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 */
122
+
123
+ /* ---- two faces: the WHOLE card turns (`Card back`, Lh 2026-10-19). The article is the turning box (.flip, base.css) and only that;
124
+ 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
125
+ 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
126
+ face is inert. Each face holds an unseen turn button: the keyboard's way to turn it; its ring is drawn on the face. */
127
+ .card--turns { background: none; border: 0; padding: 0; cursor: pointer; touch-action: manipulation; }
128
+ .card__face {
129
+ display: grid;
130
+ align-content: start;
131
+ gap: var(--card-gap);
132
+ min-width: 0;
133
+ padding: var(--card-pad);
134
+ background: var(--card-bg);
135
+ border: var(--border-w) solid var(--card-border);
136
+ border-radius: inherit;
137
+ overflow: hidden;
138
+ position: relative;
139
+ }
140
+ .card__face > * { min-width: 0; }
141
+ .card__middle { display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, 18.75rem); overflow: hidden; }
142
+ .card__turn { position: absolute; inset: 0; width: 1px; height: 1px; padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); }
143
+ .card__face:has(> .card__turn:focus-visible) { outline: var(--focus); outline-offset: calc(-1 * var(--sp-1)); }
144
+ /* 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 */
145
+ .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; }
146
+ .card__head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.5; }
147
+ .card__head > .chip-row > .chip > .icon { display: inline-block; vertical-align: -0.15em; margin-inline-end: var(--chip-sm-gap, var(--sp-1)); }
148
+
99
149
  /* ---- bleed: a scrolling track that runs to the container's edges ----
100
150
  For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
101
151
  a tile grid: the TRACK reaches the edges so it reads as scrollable, while
@@ -302,6 +352,49 @@
302
352
  }
303
353
  .card--row .card__body { flex: 1; display: grid; gap: var(--sp-0); }
304
354
 
355
+ /* ---- a short window of past entries (`CardList show`, Lh 2026-10-19): the list scrolls IN ITSELF, a few rows in sight and the next one cut
356
+ in half to say there is more. The height is counted in rows by the component (`--_card-list-h`, px: rows wrap to any height); the room
357
+ round the rows (`--card-list-window-pad`) keeps a picked row's focus ring and a card's shadow inside the scroll box, and the margin gives
358
+ that room back so the rows stand where an ordinary list's would. ---- */
359
+ .card-list--window {
360
+ --_pad: var(--card-list-window-pad, var(--sp-2));
361
+ max-height: var(--_card-list-h, none);
362
+ overflow-y: auto;
363
+ overscroll-behavior: contain;
364
+ scrollbar-width: none;
365
+ padding: var(--_pad);
366
+ margin: calc(-1 * var(--_pad));
367
+ }
368
+
369
+ /* ---- the cross that takes a row off the list (`Card row onRemove`): small, at the row's top right corner, inside it ----
370
+ <label class="card card--row"><input type="radio">…<div class="card__body">…</div><button class="card__remove" aria-label="Remove"><svg class="icon">x</svg></button></label>
371
+ Smaller than any button (a ring half a thing's size, `--tap-base` / 2 = 24) and it grows with the size of things; the touch target is an
372
+ unseen ring round it, never under --tap-floor (44). It stands in the row's flow, so the title wraps before it, tucked into the corner
373
+ by the row's own padding; it sits over the row's radio (which covers the whole card), so a touch on it removes and never picks. */
374
+ .card__remove {
375
+ --_size: var(--card-remove-size, calc(var(--tap-base) / 2));
376
+ position: relative;
377
+ z-index: 1;
378
+ flex: none;
379
+ align-self: flex-start;
380
+ display: grid;
381
+ place-items: center;
382
+ width: var(--_size);
383
+ height: var(--_size);
384
+ margin-block-start: calc(var(--card-remove-inset, var(--sp-2)) - var(--card-row-pad, var(--sp-4)));
385
+ margin-inline-end: calc(var(--card-remove-inset, var(--sp-2)) - var(--card-row-pad, var(--sp-4)));
386
+ padding: 0;
387
+ border: var(--border-w) solid currentColor;
388
+ border-radius: 50%;
389
+ background: var(--card-remove-bg, transparent);
390
+ color: var(--card-remove-ink, currentColor);
391
+ font: inherit;
392
+ cursor: pointer;
393
+ }
394
+ .card__remove::after { content: ""; position: absolute; inset: 50% auto auto 50%; translate: -50% -50%; width: max(var(--tap-floor), var(--_size)); aspect-ratio: 1; border-radius: 50%; }
395
+ .card__remove > .icon { width: 62%; height: 62%; }
396
+ .card__remove:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
397
+
305
398
  /* leading thumb/icon in a row */
306
399
  .card__thumb {
307
400
  width: var(--card-thumb-size, var(--tap-sm)); height: var(--card-thumb-size, var(--tap-sm));
package/css/choice.css CHANGED
@@ -154,6 +154,28 @@
154
154
  .calendar__day--mark2 { background: var(--calendar-day-mark2-bg, color-mix(in srgb, var(--accent) 12%, transparent)); color: var(--calendar-day-mark2-ink, inherit); box-shadow: var(--calendar-day-mark2-shadow, inset 0 0 0 var(--border-w) var(--accent)); }
155
155
  .calendar__day[aria-selected="true"] { background: var(--calendar-day-selected-bg, var(--accent)); color: var(--calendar-day-selected-ink, var(--on-accent)); font-weight: 700; }
156
156
  .calendar__day:disabled { opacity: 0.3; }
157
+ .calendar__day[aria-current="date"] { text-decoration: underline; text-underline-offset: 0.2em; }
158
+
159
+ /* ---- calendar weeks: a run of week rows that scrolls week by week, `show` rows in sight; the head sticks at the top of the box
160
+ <div class="calendar calendar--weeks"><div class="calendar__weeks-head"><span class="calendar__label">June 2026</span><div class="calendar__grid">…wd×7</div></div>
161
+ <div class="calendar__grid calendar__week"><button class="calendar__day">…</button>×7</div>…</div>
162
+ --_weeks-h (head + `show` rows) and --_weeks-lead (the head and the gap under it, where a row snaps) are measured by src/choice/CalendarWeeks.tsx; the cells and every --calendar-* knob are Calendar's */
163
+ .calendar--weeks {
164
+ position: relative;
165
+ max-height: var(--_weeks-h, none);
166
+ overflow-y: auto;
167
+ overscroll-behavior: contain;
168
+ scroll-snap-type: y mandatory;
169
+ scroll-padding-block-start: var(--_weeks-lead, 0px);
170
+ overflow-anchor: none; /* a pick rerenders the rows; the browser must not shift the scroll to keep an anchor */
171
+ scrollbar-width: none;
172
+ }
173
+ .calendar--weeks::-webkit-scrollbar { display: none; }
174
+ .calendar__weeks-head { position: sticky; inset-block-start: 0; z-index: 1; display: grid; gap: var(--calendar-head-gap, var(--sp-2)); background: var(--calendar-weeks-head-bg, var(--card-bg)); }
175
+ .calendar__label { font: var(--calendar-label-font, var(--type-label)); color: var(--calendar-label-ink, inherit); justify-self: start; }
176
+ button.calendar__label { appearance: none; border: 0; padding: 0; background: none; cursor: pointer; border-radius: var(--sp-1); }
177
+ button.calendar__label:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
178
+ .calendar__week { scroll-snap-align: start; }
157
179
 
158
180
  /* ---- mood: five dots in a pill, the picked one grows ----
159
181
  <fieldset class="mood"><label class="mood__opt"><input type="radio" name="mood"><span class="mood__dot"></span></label>×5</fieldset> */
package/css/forms.css CHANGED
@@ -2,6 +2,7 @@
2
2
  cardds/forms.css — inputs + inline form actions
3
3
  .field: flat outlined input, the label IS the placeholder.
4
4
  Markup: <label class="field"><svg?/><input placeholder="Passport Number"></label>
5
+ Several lines (Field rows): <label class="field field--rows"><svg?/><textarea rows="4" placeholder="…"></textarea></label>
5
6
  ============================================================ */
6
7
 
7
8
  .field {
@@ -19,17 +20,28 @@
19
20
  }
20
21
 
21
22
  .field input,
22
- .field select {
23
+ .field select,
24
+ .field textarea,
25
+ .field--rows {
26
+ font: var(--field-font, 400 var(--fs-lg) / 1.4 var(--font-ui)); /* 26: the words being typed read a size up from body (Lh 2026-09-22); the Composer already does. .field--rows too, so 1lh is the textarea's line for its icon */
27
+ }
28
+ .field input,
29
+ .field select,
30
+ .field textarea {
23
31
  border: 0;
24
32
  background: transparent;
25
33
  color: inherit;
26
- font: var(--field-font, 400 var(--fs-lg) / 1.4 var(--font-ui)); /* 26: the words being typed read a size up from body (Lh 2026-09-22); the Composer already does */
27
34
  width: 100%;
28
35
  min-width: 0;
29
36
  outline: none; /* the browser's ring drew a second box inside the field (Lh 2026-09-22) — the field itself tells focus */
30
37
  caret-color: currentColor;
31
38
  }
32
- .field input::placeholder { color: inherit; opacity: 0.55; font-size: calc(1em * var(--_ph-fit, 1)); } /* --_ph-fit: Field's measure — a placeholder longer than the box shrinks to show whole (Lh 2026-09-25) */
39
+ .field input::placeholder,
40
+ .field textarea::placeholder { color: inherit; opacity: 0.55; font-size: calc(1em * var(--_ph-fit, 1)); } /* --_ph-fit: Field's measure — a placeholder longer than the box shrinks to show whole (Lh 2026-09-25) */
41
+ /* several lines in the same frame (Field rows): the textarea is as tall as its `rows` × line, the frame grows round it, the icon and the action stay at the top (Lh 2026-10-19) */
42
+ .field--rows { align-items: flex-start; padding-block: var(--field-rows-pad-y, var(--sp-3)); }
43
+ .field textarea { flex: 1; resize: none; display: block; }
44
+ .field--rows > .icon { margin-top: calc((1lh - var(--icon-sm)) / 2); } /* centred on the first line */
33
45
  /* the field the cursor is in: its own border goes full ink, in every palette (the coloured ones fade it to 25% at rest) */
34
46
  .field:has(:focus, [data-cursor]) { border-color: var(--field-active-border, currentColor); } /* [data-cursor]: SheetSteps holds the phone's keyboard for this field */
35
47
 
package/css/journey.css CHANGED
@@ -35,7 +35,7 @@
35
35
  justify-items: center;
36
36
  gap: var(--tile-badge-gap, var(--sp-0));
37
37
  padding: var(--tile-badge-pad-y, var(--sp-1)) var(--tile-badge-pad-x, var(--sp-2));
38
- border-radius: var(--tile-badge-radius, var(--r-badge));
38
+ border-radius: var(--tile-badge-radius, var(--_badge-radius, var(--r-badge))); /* --_badge-radius: card.css, a badge in a card's corner */
39
39
  background: var(--tile-badge-bg, var(--card-3-bg));
40
40
  --dark: var(--card-3-dark);
41
41
  color: var(--tile-badge-ink, var(--card-ink));
@@ -49,6 +49,28 @@
49
49
  .tile-badge--plain { background: transparent; --dark: inherit; color: inherit; padding: 0; min-width: var(--tile-badge-plain-min-w, var(--tap-sm)); }
50
50
  .tile-badge--plain .tile-badge__value { font: 800 var(--fs-h1)/1 var(--font-display); }
51
51
 
52
+ /* 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
53
+ containment keeps its own words from adding height). Its height picks the form: short (a one-line title) = two lines, the day name
54
+ over the number and the month on one line; tall (a two-line title) = three lines, day · number · month. */
55
+ .tile-badge--stretch {
56
+ align-self: stretch;
57
+ container-type: size;
58
+ inline-size: calc(var(--fs-lg) * 3.25);
59
+ grid-template-columns: auto auto;
60
+ align-content: center;
61
+ justify-content: center;
62
+ align-items: baseline;
63
+ column-gap: var(--sp-1);
64
+ row-gap: 0;
65
+ padding-block: var(--sp-3);
66
+ line-height: var(--tile-badge-line, 0.95);
67
+ }
68
+ .tile-badge--stretch > * { line-height: inherit; }
69
+ .tile-badge--stretch > :first-child { grid-column: 1 / -1; }
70
+ @container (min-height: 5.5rem) {
71
+ .tile-badge--stretch > * { grid-column: 1 / -1; }
72
+ }
73
+
52
74
  /* corner badge: pin a tile badge on media (rating, price) — .card__media is relative */
53
75
  .tile-badge--corner { position: absolute; right: var(--sp-3); bottom: var(--sp-3); }
54
76
 
package/css/media.css CHANGED
@@ -197,8 +197,8 @@
197
197
  liked it, a voice note, a reply live OUTSIDE the postcard, in a Row under it. */
198
198
  .postcard {
199
199
  position: relative;
200
+ display: block; /* its faces lie absolute over the box (.flip's grid cell is not used) */
200
201
  min-height: var(--postcard-min-h, var(--_cover-min-h));
201
- perspective: 60rem;
202
202
  border-radius: var(--postcard-radius, var(--r-card));
203
203
  isolation: isolate;
204
204
  }
@@ -211,9 +211,6 @@
211
211
  border-radius: inherit;
212
212
  overflow: hidden;
213
213
  isolation: isolate;
214
- backface-visibility: hidden;
215
- -webkit-backface-visibility: hidden;
216
- transition: transform var(--motion-flip) ease-in-out;
217
214
  box-shadow: var(--postcard-face-shadow, var(--shadow-float));
218
215
  }
219
216
  /* the turn: the button that fills the face, under the band */
@@ -253,7 +250,6 @@
253
250
  background: var(--postcard-back-bg, var(--card-2-bg));
254
251
  --dark: var(--card-2-dark);
255
252
  color: var(--postcard-back-ink, var(--card-ink));
256
- transform: rotateY(180deg);
257
253
  }
258
254
  .postcard__back .postcard__turn { align-content: start; padding-top: var(--postcard-back-top, 20%); gap: var(--postcard-back-gap, var(--sp-5)); } /* the name a fifth down the paper, the words after it */
259
255
  .postcard__caption { margin: 0; font: var(--postcard-caption-font, var(--type-title)); text-wrap: pretty; }
@@ -272,13 +268,8 @@
272
268
  .postcard__words { display: grid; gap: var(--postcard-words-gap, var(--sp-1)); flex: 1; min-width: 0; pointer-events: none; }
273
269
  .postcard__front .postcard__band { --accent: var(--postcard-front-ink, var(--card-ink)); --accent-dark: calc(1 - var(--dark)); }
274
270
  .postcard__band .icon-btn[aria-pressed="true"] .icon { fill: currentColor; }
275
- .postcard.is-flipped > .postcard__front { transform: rotateY(-180deg); }
276
- .postcard.is-flipped > .postcard__back { transform: rotateY(0); }
277
271
  /* a voice note on the back: no words to read — its timeline (a Progress), its length and the control to hear it
278
272
  under them, at the middle over the turn, on the face's inset; the app plays and keeps the time */
279
273
  .postcard__voice { position: absolute; inset: 0; z-index: 2; display: grid; align-content: center; justify-items: center; gap: var(--postcard-voice-gap, var(--sp-3)); padding: var(--postcard-face-pad, var(--card-pad-rest)); pointer-events: none; }
280
274
  .postcard__voice > * { pointer-events: auto; }
281
275
  .postcard__voice > .progress { justify-self: stretch; pointer-events: none; }
282
- @media (prefers-reduced-motion: reduce) {
283
- .postcard__face { transition: none; }
284
- }
package/css/sheet.css CHANGED
@@ -355,7 +355,9 @@
355
355
  wireframe's street pattern, so a map is never a hole. The ring marks the
356
356
  rough area the member is choosing: fixed at the box's centre, so dragging
357
357
  the map underneath is what moves it (the centre IS the choice), never a
358
- pin to place. The hint rides at the top, under the bar's line. */
358
+ pin to place. The hint rides at the top, under the bar's line.
359
+ For a MEETING place (Lh 2026-10-19) the centre mark is a pin instead of the ring: `.map__pin`, below the ring's rule,
360
+ its tip on the point the ring's centre sits. */
359
361
  .map {
360
362
  position: absolute;
361
363
  inset: 0;
@@ -383,6 +385,24 @@
383
385
  pointer-events: none;
384
386
  z-index: 1; /* above the engine's panes (Leaflet's tiles sit at 200, but inside their own stacking context) */
385
387
  }
388
+ .map__pin { /* mark="pin": an exact point — the pin's TIP is where the ring's centre sits, so the engine reads the element's bottom-centre */
389
+ position: absolute;
390
+ left: 50%;
391
+ top: var(--_band-centre);
392
+ width: var(--map-pin-size, var(--tap));
393
+ height: var(--map-pin-size, var(--tap));
394
+ translate: -50% -100%;
395
+ /* ink follows its ground: the pin stands on the map's own tone-2 fill, not on the sheet round it — in a dark sheet the inherited
396
+ --accent is the card's ondark ink, cream on clay's orange map (2.8:1); so the pin takes the tone's side and its ink (tokens.css) */
397
+ --dark: var(--card-2-dark);
398
+ color: var(--map-pin-ink, var(--card-ink));
399
+ filter: drop-shadow(var(--map-pin-drop, var(--shadow-float)));
400
+ pointer-events: none;
401
+ z-index: 1;
402
+ }
403
+ /* the lucide tip stops 2.2 of 24 short of the box's bottom: lowered so the tip IS the bottom-centre; filled with the surface so the
404
+ street pattern never shows through the pin's body */
405
+ .map__pin > .icon { width: 100%; height: 100%; translate: 0 9%; fill: var(--surface); }
386
406
  .map__credit { /* the tile provider's credit — required by OSM; a caption, not the engine's own control */
387
407
  position: absolute;
388
408
  right: var(--sp-2);
package/css/tokens.css CHANGED
@@ -281,8 +281,8 @@
281
281
  surface turns everything on it to ondark with no rule per piece. A piece with no fill that stands in for a surface (a stat
282
282
  tile) takes its ground's side: `--dark: inherit`. inkGrounds() measures it; docs/reference.md "Ink follows its ground" */
283
283
  :where(body, [data-palette], [data-mode], .card, .tile, .modal__drawer, .postcard__front, .postcard__back, .sticky, .post-paper,
284
- .float-bar__pill, .snackbar__body, .delta, .track, .tile-badge, .avatar, .bubble__body, .map__hint, .sheet--paper > .sheet__body,
285
- .postcard__band, .track__step, .toggle, .chip) {
284
+ .float-bar__pill, .snackbar__body, .delta, .track, .tile-badge, .avatar, .bubble__body, .map__hint, .map__pin, .sheet--paper > .sheet__body,
285
+ .postcard__band, .track__step, .toggle, .chip, .card__face, .card--bands > :is(.card__head, .card__foot), .card--bands > .card__face > :is(.card__head, .card__foot)) {
286
286
  --card-ink: color-mix(in srgb, var(--ink-ondark) calc(var(--dark) * 100%), var(--ink-onlight));
287
287
  --card-muted: color-mix(in srgb, var(--ink-ondark-muted) calc(var(--dark) * 100%), var(--ink-onlight-muted));
288
288
  --on-accent: color-mix(in srgb, var(--ink-ondark) calc(var(--accent-dark) * 100%), var(--ink-onlight));