@usableapp/cardds 0.7.103 → 0.7.105

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 (66) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/css/actions.css +112 -37
  3. package/css/base.css +139 -15
  4. package/css/card.css +331 -86
  5. package/css/choice.css +25 -33
  6. package/css/finish.css +29 -28
  7. package/css/forms.css +6 -14
  8. package/css/journey.css +8 -6
  9. package/css/layover.css +26 -24
  10. package/css/lists.css +8 -6
  11. package/css/media.css +13 -74
  12. package/css/numbers.css +37 -32
  13. package/css/palette.css +2 -2
  14. package/css/people.css +3 -8
  15. package/css/post.css +23 -19
  16. package/css/sheet.css +84 -56
  17. package/css/stack.css +40 -216
  18. package/css/stamp.css +41 -29
  19. package/css/step.css +1 -107
  20. package/css/sticky.css +43 -41
  21. package/css/theme.template.css +12 -0
  22. package/css/tokens.css +101 -26
  23. package/dist/actions/Chip.d.ts +1 -1
  24. package/dist/actions/Chip.js +3 -2
  25. package/dist/actions/IconBtn.d.ts +12 -3
  26. package/dist/actions/IconBtn.js +9 -3
  27. package/dist/cardds.css +1094 -881
  28. package/dist/cards/Card.d.ts +8 -6
  29. package/dist/cards/Card.js +4 -4
  30. package/dist/cards/CardFeed.js +7 -9
  31. package/dist/cards/CardFoot.d.ts +7 -2
  32. package/dist/cards/CardFoot.js +4 -3
  33. package/dist/catalog.json +1 -1
  34. package/dist/journey/JourneyBadge.d.ts +1 -1
  35. package/dist/journey/JourneyBadge.js +1 -1
  36. package/dist/layover/Snackbar.js +2 -2
  37. package/dist/lists/Bubble.d.ts +1 -1
  38. package/dist/lists/Bubble.js +1 -1
  39. package/dist/lists/Row.js +5 -7
  40. package/dist/numbers/Picker.js +11 -18
  41. package/dist/people/Avatar.d.ts +1 -1
  42. package/dist/people/Avatar.js +3 -2
  43. package/dist/post/PostDesk.js +6 -9
  44. package/dist/post/PostLetter.js +4 -4
  45. package/dist/pressable.d.ts +9 -0
  46. package/dist/pressable.js +6 -0
  47. package/dist/room.d.ts +5 -0
  48. package/dist/room.js +9 -0
  49. package/dist/scaffold/Panes.js +8 -14
  50. package/dist/sheets/Sheet.d.ts +1 -1
  51. package/dist/sheets/Sheet.js +2 -3
  52. package/dist/sheets/SheetSteps.js +3 -3
  53. package/dist/sheets/StackSheet.d.ts +1 -1
  54. package/dist/sheets/sheetStepsPile.js +2 -3
  55. package/dist/sheets/stackCard.js +67 -115
  56. package/dist/state.d.ts +21 -0
  57. package/dist/state.js +28 -1
  58. package/dist/sticky/Sticky.js +3 -7
  59. package/dist/templates/Home.d.ts +1 -1
  60. package/dist/templates/Splash.js +2 -2
  61. package/dist/templates/Walkthrough.js +4 -5
  62. package/docs/reference.md +76 -12
  63. package/lib/catalog.mjs +23 -7
  64. package/lint/rules.mjs +7 -5
  65. package/lint/values.mjs +144 -0
  66. package/package.json +6 -4
package/css/step.css CHANGED
@@ -65,132 +65,26 @@
65
65
  own screens are composed (the guide); the framework does not police it — a component never changes
66
66
  because another one is there. */
67
67
 
68
- /* the reading block: a .page (base.css) — the same words-on-the-base
69
- block as anywhere else (it was .step__card, and a .card before 2026-09-16;
70
- Lh: a step reads like a page, not a card in a page — so it IS the page's
71
- block, one name). Here it keeps a card's inner rhythm (a head, --card-gap
72
- between things, the tokens .card publishes for the head and .bleed) and
73
- gets the flex rule: it hugs its content, and when the column runs out (a
74
- short phone, large text) it shrinks and scrolls inside itself instead of
75
- pushing the bar or the sheet */
68
+ /* the reading block (base.css writes where it sits and why): a card's inner rhythm, set here */
76
69
  .step > .page, .step > .pages > .page {
77
70
  --card-pad: 0rem; /* published for .bleed: nothing to cancel */
78
71
  --card-gap: var(--sp-4);
79
- gap: var(--card-gap);
80
- padding-block: 0; /* --_lead carries the top; the bar's room is the step's */
81
- color: var(--on-surface); /* align-content: start is .page's own (base.css) — not restated here */
82
72
  }
83
73
  /* the reading block is the .page — or the .pages that holds several of them, a walk of words
84
74
  (card.css). Behind a PEEKED sheet the words keep the strip's room under them, so whatever is down there
85
75
  (the buttons that call the other sheets) scrolls clear of it — CSS alone: the box declares --sheet-peek. */
86
- .step:has(> .sheet--peek) > :is(.page, .pages) { padding-bottom: var(--sheet-peek); }
87
- .step > :is(.page, .pages) {
88
- flex: 0 1 auto;
89
- min-height: 0;
90
- overflow-y: auto;
91
- overscroll-behavior: contain;
92
- }
93
- /* a lone .page carries the margin a walk's .pages does not (its own .page children do, below) —
94
- the screen's gap, moved in here from above the step; the inset, taken back from the bleed.
95
- It ends with the bottom layer's room, as a walk's page does: it scrolls only when the layer would cover its words
96
- (timebank onboard-04, 2026-09-27 — its last Link lay under the bar; Lh: same rule as the walk) */
97
- .step > .page { margin: var(--_lead, 0) var(--screen-pad) 0; padding-bottom: var(--bottom-h, 0rem); }
98
- /* --bleed: the page's box runs to the screen's edges, the inset is its own padding — what leans, slides or casts a shadow past
99
- the inset (a postcard) is cut at the glass, not at the inset; a little room on top for a turned corner. Nothing pans sideways. */
100
- .step > .page--bleed { margin-inline: 0; padding-inline: var(--screen-pad); padding-block-start: var(--sp-2); overflow-x: hidden; }
101
76
 
102
- /* a WALK (Pages of Page) — Lh 2026-09-23, third call: no top; the picture and the words are ONE run that scrolls up together
103
- (the picture is never pinned). Each page starts at the screen's lead and ends with the same room under it — the lead as
104
- padding above AND below — so the words begin near the middle and, when a big text size runs them long, scroll up clear.
105
- The walk shows no scrollbar. */
106
- .step > .pages { touch-action: pan-y; } /* pan-y: the walk scrolls itself up and down; a sideways swipe is the Walkthrough's (it turns the page) */ /* the scrolling box is the whole content: from the step's top edge, edge to edge — no margin, the default (Lh 2026-09-23) */
107
- .step > .pages > .page {
108
- padding-inline: var(--screen-pad); /* the words keep the inset INSIDE it */
109
- margin-top: var(--_lead, 0rem); /* the lead is each PAGE's margin (15% with no top, the screen's gap under a bar): the words start there and scroll up to the box's edge */
110
- padding-bottom: var(--bottom-h, 0rem); /* exactly the bottom layer (measured by Screen): a page scrolls ONLY when the layer would cover its words, and then just far enough to clear them — at 100% nothing scrolls (Lh 2026-09-23) */
111
- }
112
77
 
113
78
  /* a SCROLLING (not fill) screen becomes a column at least one phone tall when it holds a bottom or a centre:
114
79
  an .action-bar as its last child (Lh 2026-09-24, 0.7.0: the bar is the column's last row — at the foot when the words are short,
115
80
  sticky to the viewport's foot while they scroll; actions.css) or a .screen__centre (Lh 2026-09-12: the group is in flow, takes the
116
81
  room after the top bar and centres its children in it; a long one makes the page longer and runs under the bar; card.css) */
117
- .screen:not(.screen--fill):has(> .action-bar:last-child, > .screen__centre) { display: flex; flex-direction: column; min-height: 100dvh; }
118
82
 
119
- /* ============================================================
120
- TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
121
- in a .screen--fill the content runs to the screen's bottom edge, and the bar lies over it at the foot — it covers what is
122
- under it, on purpose; nothing is added to the content to make room. The keyboard lifts the layer (--kb).
123
- (On a scrolling screen the bar is a sticky last row instead, actions.css.)
124
- ============================================================ */
125
- .screen--fill > .action-bar {
126
- position: absolute; inset-inline: 0; top: auto; bottom: var(--kb, 0rem);
127
- max-width: none;
128
- margin: 0;
129
- /* room between its top edge and its first row (the walk's dots): since it has a ground, a 0 here put the dots against the words
130
- running under it (Lh 2026-09-25) — the bar's own top padding */
131
- padding-top: var(--action-bar-fill-pad-top, var(--sp-3));
132
- /* its GROUND (Lh 2026-09-24, timebank card 141 — was "no ground of its own"): words that run under the layer mid-scroll
133
- showed through the dots, the controls and the buttons. The layer lies on the screen's own surface now; a project sets
134
- another (or `transparent`, the old see-through) with the knob */
135
- background: var(--action-bar-bg, var(--surface));
136
- }
137
- /* the walk's dots are the BOTTOM layer's too (Lh 2026-09-23): a row of the bar, over its buttons, centred */
138
- .action-bar > .pager__at, .action-bar__tier > .pager__at { align-self: center; }
139
- /* the TOP is in the column too (Lh 2026-09-23): an .appbar in a fill screen is its first row, not fixed over it — the screen
140
- reserves nothing for it; the breathing under it stays the one it had (--sp-2), not the screen's gap */
141
- .screen--fill > .appbar {
142
- position: relative; inset: auto; top: auto;
143
- flex: none;
144
- max-width: none;
145
- margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge; its own padding is the inset */
146
- margin-bottom: calc(var(--sp-2) - var(--screen-gap));
147
- }
148
- .screen.screen--fill:has(> .appbar) { padding-top: var(--safe-top); --header-gap: 0rem; }
149
- /* …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
150
- them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
151
- .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
152
- .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
153
- /* a TRACK as the screen's TOP (Lh 2026-10-03): the walk's progress stands where a bar would, a row of the column, in the screen's
154
- inset — and it never touches what comes up under it: the step (and so a sheet at full) starts --track-top-gap below it. That
155
- room is the track's own, not the screen's gap; --track-top-lead is the air over it, under the status bar. Both = the screen's side inset (16): the pill
156
- stands the same distance from everything around it (Lh 2026-10-03: 8 over was too little, 24 under too much). */
157
- .screen--fill > .track:first-child {
158
- flex: none;
159
- margin-top: var(--track-top-lead, var(--sp-4));
160
- margin-bottom: calc(var(--track-top-gap, var(--sp-4)) - var(--screen-gap));
161
- }
162
- /* …and over a STAGE (a map behind a sheet, Lh 2026-10-03): the stage is the whole screen, so the track is a LAYER on it — the map
163
- shows around the pill — and the stage keeps the track's line free itself: a sheet at full stops --track-top-gap under the
164
- track, as in a step. --top-h is the track's own height, measured by Screen (a numbered track grows with the text). */
165
- .screen--fill > .track:first-child:has(+ .sheet-stage) {
166
- position: absolute; z-index: 1;
167
- inset: calc(var(--safe-top) + var(--track-top-lead, var(--sp-4))) var(--screen-pad) auto;
168
- margin: 0;
169
- }
170
83
  .screen--fill > .track:first-child + .sheet-stage {
171
84
  --_top: calc(var(--track-top-lead, var(--sp-4)) + var(--top-h, var(--header-h)) + var(--track-top-gap, var(--sp-4)));
172
85
  }
173
- /* a walk page that OPENS on a picture (Lh 2026-09-23): the picture takes the lead's room — as wide as the content (edge to edge), from the top edge, as tall as
174
- the lead + the picture's own height (--page-media-h), square — so the words start where they always did */
175
- .step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
176
- .step > .pages > .page > :is(img, .ph):first-child {
177
- width: calc(100% + 2 * var(--screen-pad)); /* the content's width: the page's inset given back */
178
- max-width: none;
179
- margin-inline: calc(-1 * var(--screen-pad));
180
- height: calc(var(--_lead, 0rem) + var(--page-media-h, 14rem));
181
- max-height: none;
182
- border-radius: 0;
183
- border-inline: 0;
184
- border-top: 0;
185
- object-fit: cover;
186
- }
187
86
  /* an EMPTY STATE is a component in the content box too (Lh 2026-09-23 — it no longer floats in a Centre over the screen): alone in a
188
87
  Step it stands at the human centre of the content, 40% down (two parts of room above it, three below). --empty-state-at is the share of the
189
88
  free room that stands above it (a theme's 0.2 puts it a fifth down); a sheet's body holds an only EmptyState by the same share, but defaults to 0: at the top, as it always stood */
190
89
  .step:has(> .empty-state)::before { content: ""; flex: calc(var(--empty-state-at, 0.4) * 5) 1 0; }
191
90
  .step:has(> .empty-state)::after { content: ""; flex: calc(5 - var(--empty-state-at, 0.4) * 5) 1 0; }
192
- .step > .empty-state { flex: none; }
193
- .sheet__body:has(> .empty-state:only-child) { display: flex; flex-direction: column; gap: 0; }
194
- .sheet__body:has(> .empty-state:only-child)::before { content: ""; flex: calc(var(--empty-state-at, 0) * 5) 1 0; }
195
- .sheet__body:has(> .empty-state:only-child)::after { content: ""; flex: calc(5 - var(--empty-state-at, 0) * 5) 1 0; }
196
- .sheet__body > .empty-state:only-child { flex: none; }
package/css/sticky.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /* cardds — sticky notes: the paper note, the field written on it, the tone chooser, notes stuck on a page, a thread of posts, the answer note.
2
- A note is its own paper: it brings its colour and its ink, whatever card or palette it lies on. */
2
+ A note is pastel paper: its tone is one of the theme's nine pastels (--pastel-*, tokens.css), its ink the onlight set, whatever card it lies on. */
3
3
 
4
4
  /* ---- sticky: a paper note — a flat pastel SQUARE of one size, the bottom-right corner folded over ----
5
5
  ONE SIZE (Lh 2026-10-10): every note is --sticky-size square (168), whatever it says; words it cannot hold are written one size smaller
@@ -39,28 +39,28 @@
39
39
  .sticky::before, .sticky::after {
40
40
  content: "";
41
41
  position: absolute;
42
- z-index: -1;
42
+ z-index: var(--z-under);
43
43
  pointer-events: none;
44
44
  border-radius: var(--sticky-radius, 0rem);
45
45
  background-image: linear-gradient(-45deg, transparent calc(var(--_fold) * 0.7071), var(--_ink) 0); /* the folded corner, cut */
46
46
  }
47
- .sticky::after { --_ink: var(--sticky-edge-bg, rgb(0 0 0 / 0.14)); inset: calc(-1 * var(--border-w)); }
47
+ .sticky::after { --_ink: var(--sticky-edge-bg, color-mix(in srgb, var(--ink-onlight) 14%, transparent)); inset: calc(-1 * var(--border-w)); }
48
48
  .sticky::before {
49
- --_ink: var(--sticky-shadow-bg, rgb(0 0 0 / 0.28));
49
+ --_ink: var(--sticky-shadow-bg, color-mix(in srgb, var(--ink-onlight) 28%, transparent));
50
50
  inset: var(--sp-1) var(--sp-0) calc(-1 * var(--sp-0)) var(--sp-0);
51
51
  filter: blur(var(--sticky-shadow-blur, var(--sp-0)));
52
52
  }
53
53
  .sticky--flat::before, .sticky--flat::after { background: var(--_ink); }
54
54
  /* the tones — the names in src/sticky/tones.ts. A swatch of the chooser wears the same class. */
55
- .sticky--cotton { background-color: var(--sticky-cotton-bg, #f3efe2); }
56
- .sticky--blush { background-color: var(--sticky-blush-bg, #f6d5da); }
57
- .sticky--tea-leaf { background-color: var(--sticky-tea-leaf-bg, #d5e8c5); }
58
- .sticky--custard { background-color: var(--sticky-custard-bg, #f6e9a0); }
59
- .sticky--salmon { background-color: var(--sticky-salmon-bg, #f3c7b3); }
60
- .sticky--iceberg { background-color: var(--sticky-iceberg-bg, #cfe0ee); }
61
- .sticky--cocoa { background-color: var(--sticky-cocoa-bg, #dccbbd); }
62
- .sticky--apricot { background-color: var(--sticky-apricot-bg, #f6d49a); }
63
- .sticky--water { background-color: var(--sticky-water-bg, #bcd6f3); }
55
+ .sticky--cotton { background-color: var(--sticky-cotton-bg, var(--pastel-cotton)); }
56
+ .sticky--blush { background-color: var(--sticky-blush-bg, var(--pastel-blush)); }
57
+ .sticky--tea-leaf { background-color: var(--sticky-tea-leaf-bg, var(--pastel-tea-leaf)); }
58
+ .sticky--custard { background-color: var(--sticky-custard-bg, var(--pastel-custard)); }
59
+ .sticky--salmon { background-color: var(--sticky-salmon-bg, var(--pastel-salmon)); }
60
+ .sticky--iceberg { background-color: var(--sticky-iceberg-bg, var(--pastel-iceberg)); }
61
+ .sticky--cocoa { background-color: var(--sticky-cocoa-bg, var(--pastel-cocoa)); }
62
+ .sticky--apricot { background-color: var(--sticky-apricot-bg, var(--pastel-apricot)); }
63
+ .sticky--water { background-color: var(--sticky-water-bg, var(--pastel-water)); }
64
64
 
65
65
  .sticky__paper {
66
66
  position: relative;
@@ -77,12 +77,10 @@
77
77
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--_fold)), calc(100% - var(--_fold)) 100%, 0 100%);
78
78
  }
79
79
  .sticky__head { flex: none; display: flex; align-items: center; gap: var(--sticky-head-gap, var(--sp-2)); font: var(--sticky-head-font, var(--type-caption)); color: var(--sticky-muted-ink, var(--_muted)); }
80
- .sticky__head > .ph { flex: 1 1 0; min-width: 0; } /* a picture in the head takes the note's width */
81
80
  .sticky__head:has(> .sticky-date) { align-items: start; }
82
- .sticky__head:has(> .sticky-date) > .ph { flex: none; width: var(--sticky-pic-size, calc(var(--tap) * 1.5)); min-height: 0; aspect-ratio: 1; } /* beside a date the picture is a small square at the start */
83
81
  .sticky__body { flex: 1 1 auto; min-width: 0; min-height: 0; overflow: clip; /* words past the note's room are cut, the foot keeps its place */ font: var(--sticky-font, var(--type-title)); overflow-wrap: anywhere; }
84
82
  .sticky--small .sticky__body { font: var(--sticky-small-font, var(--type-label)); } /* the note measured its words and they did not fit at its size (src/sticky/fit.ts) */
85
- .sticky__paper > :not(.sticky__body) > .chip { border-color: transparent; background-color: var(--sticky-chip-bg, rgb(255 255 255 / 0.85)); --dark: inherit; color: var(--sticky-ink, var(--card-ink)); } /* a chip on a note is a label ON the paper (whose note it is): a pale ground nearly opaque (it may lie over words), no edge, the note's ink */
83
+ .sticky__paper > :not(.sticky__body) > .chip { --chip-border: transparent; --chip-bg: var(--sticky-chip-bg, color-mix(in srgb, var(--stamp-paper) 85%, transparent)); --dark: inherit; --chip-ink: var(--sticky-ink, var(--card-ink)); } /* a chip on a note is a label ON the paper (whose note it is): a pale ground nearly opaque (it may lie over words), no edge, the note's ink */
86
84
  .sticky__foot { flex: none; font: var(--sticky-foot-font, var(--type-caption)); color: var(--sticky-muted-ink, var(--_muted)); padding-inline-end: var(--_fold); } /* the line stops before the fold */
87
85
  /* the flap: a square in the corner — the paper's cut takes its outer half, what is left is the triangle lying on the note */
88
86
  .sticky__fold {
@@ -93,7 +91,7 @@
93
91
  height: var(--_fold);
94
92
  border-top-left-radius: var(--sp-1);
95
93
  background-color: var(--sticky-fold-bg, color-mix(in srgb, currentColor 14%, transparent)); /* a darker shade of whatever the paper is */
96
- box-shadow: calc(-1 * var(--sp-0)) calc(-1 * var(--sp-0)) var(--sp-1) rgb(0 0 0 / 0.08);
94
+ box-shadow: var(--shadow-fold);
97
95
  }
98
96
  /* things stuck ON the note (a stamp, placed by hand): a layer over the whole paper. A tap between them still reaches the words under it.
99
97
  A stamp on a note is the stamp's small size whatever the note's (48 × 60, --stamp-sm-size: stamp.css; `StampOn main` is the main size), and may lie over the words. */
@@ -105,8 +103,8 @@
105
103
  /* a note that stands alone in a page's column or in a centre stands in its middle */
106
104
  .page > .sticky, .screen__centre > .sticky { margin-inline: auto; }
107
105
  .sticky--tilt-left, .sticky--tilt-right { margin-block: var(--sp-4); } /* the turned corners and the shadow need room: a box that scrolls (a centre) would cut them */
108
- .sticky--tilt-left { rotate: calc(-1 * var(--sticky-tilt, 5deg)); }
109
- .sticky--tilt-right { rotate: var(--sticky-tilt, 5deg); }
106
+ .sticky--tilt-left { rotate: calc(-1 * var(--sticky-tilt, calc(var(--tilt-base) * 5))); }
107
+ .sticky--tilt-right { rotate: var(--sticky-tilt, calc(var(--tilt-base) * 5)); }
110
108
 
111
109
  /* ---- sticky date: the day, big, the month under it — at the end of the note's head (its top-right corner) ----
112
110
  <div class="sticky__head">…<time class="sticky-date" datetime="2026-02-14"><span class="sticky-date__day">14</span><span class="sticky-date__month">ก.พ.</span></time></div> */
@@ -148,7 +146,6 @@
148
146
  /* a name label in the foot (a Chip, `sm`) stands at the START, what follows it (the time) at the end, on one line: a long name is cut
149
147
  with an ellipsis past its share of the row and never wraps; the time is never cut (a long one may run a little onto the fold) */
150
148
  .sticky__paper > .sticky__foot:has(> .chip) { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); white-space: nowrap; text-align: end; }
151
- .sticky__foot > .chip { display: block; flex: none; min-width: 0; max-width: var(--sticky-foot-chip-max-w, 62%); overflow: hidden; text-overflow: ellipsis; text-align: start; }
152
149
 
153
150
  /* ---- sticky field: the words written ON a note — a bare textarea in the note's type and ink, filling it ----
154
151
  <div class="sticky__body"><textarea class="sticky-field" aria-label="Note" placeholder="Add text to this note"></textarea></div> */
@@ -243,12 +240,12 @@
243
240
  .sticky-thread--lapped > .sticky-post > .sticky-post__who { display: none; }
244
241
  .sticky-thread--lapped > .sticky-post > .sticky-post__notes { grid-area: 1 / 1; }
245
242
  .sticky-thread--lapped > .sticky-post > .sticky-post__head { grid-area: 2 / 1; }
246
- .sticky-thread--lapped > .sticky-post > .sticky-post__head > .sticky-post__name { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } /* as .t-sr: heard, not seen */
243
+ .sticky-thread--lapped > .sticky-post > .sticky-post__head > .sticky-post__name { position: absolute; width: var(--px); height: var(--px); margin: calc(-1 * var(--px)); overflow: hidden; clip-path: inset(50%); white-space: nowrap; } /* as .t-sr: heard, not seen */
247
244
  .sticky-thread--lapped > .sticky-post > .sticky-post__head > .sticky-post__time { margin-inline: 0; }
248
245
  .sticky-thread--lapped > .sticky-post > .sticky-post__head:not(:has(> .sticky-post__time)) { display: none; }
249
246
  /* each note leans a little, one way then the other; a post counts among posts only, so a heading before the first changes no note's lean */
250
- .sticky-thread--lapped > .sticky-post > .sticky-post__notes > .sticky { rotate: calc(-1 * var(--sticky-thread-tilt, 2deg)); }
251
- .sticky-thread--lapped > .sticky-post:nth-child(even of .sticky-post) > .sticky-post__notes > .sticky { rotate: var(--sticky-thread-tilt, 2deg); }
247
+ .sticky-thread--lapped > .sticky-post > .sticky-post__notes > .sticky { rotate: calc(-1 * var(--sticky-thread-tilt, calc(var(--tilt-base) * 2))); }
248
+ .sticky-thread--lapped > .sticky-post:nth-child(even of .sticky-post) > .sticky-post__notes > .sticky { rotate: var(--sticky-thread-tilt, calc(var(--tilt-base) * 2)); }
252
249
  /* under an answer note the thread ends with the room that note stands in: the last turn scrolls up clear of it */
253
250
  .screen:has(> .sticky-answer) .sticky-thread--lapped { padding-block-end: calc(var(--sticky-size, calc(var(--sp-12) * 3.5)) * var(--sticky-answer-show, 0.75) + var(--sp-6)); }
254
251
 
@@ -281,11 +278,12 @@
281
278
  .sticky-board .sticky__paper > .sticky__foot { position: absolute; inset: auto var(--sticky-board-foot-inset, var(--sp-3)) var(--sticky-board-foot-inset, var(--sp-3)); text-align: end; }
282
279
  /* how far each is turned: FOUR unlike angles in turn (Lh 2026-10-11). At most three notes meet, so four keep the lean from ever looking the
283
280
  same: two in a row, a note lies over the one two before it and those lean opposite ways; three in a row, over the one three before it,
284
- never the same angle. (A run of seven left one column at 0.75° and −0.5°: its edges read as one straight line and the lean was lost.) */
285
- .sticky-board > :nth-child(4n + 1) { rotate: var(--sticky-board-turn-1, -3deg); }
286
- .sticky-board > :nth-child(4n + 2) { rotate: var(--sticky-board-turn-2, 1.5deg); }
287
- .sticky-board > :nth-child(4n + 3) { rotate: var(--sticky-board-turn-3, 2.75deg); }
288
- .sticky-board > :nth-child(4n) { rotate: var(--sticky-board-turn-4, -1.75deg); }
281
+ never the same angle. (A run of seven left one column at 0.75° and −0.5°: its edges read as one straight line and the lean was lost.)
282
+ A pile (below) turns its notes by the same four, by their place among its children. */
283
+ .sticky-board > :nth-child(4n + 1), .sticky-pile__notes > .sticky:nth-child(4n + 1) { rotate: var(--sticky-board-turn-1, calc(var(--tilt-base) * -3)); }
284
+ .sticky-board > :nth-child(4n + 2), .sticky-pile__notes > .sticky:nth-child(4n + 2) { rotate: var(--sticky-board-turn-2, calc(var(--tilt-base) * 1.5)); }
285
+ .sticky-board > :nth-child(4n + 3), .sticky-pile__notes > .sticky:nth-child(4n + 3) { rotate: var(--sticky-board-turn-3, calc(var(--tilt-base) * 2.75)); }
286
+ .sticky-board > :nth-child(4n), .sticky-pile__notes > .sticky:nth-child(4n) { rotate: var(--sticky-board-turn-4, calc(var(--tilt-base) * -1.75)); }
289
287
 
290
288
  /* ---- sticky pile: one to six notes lying one over another — the newest on top (Lh 2026-10-11) ----
291
289
  <div class="sticky-pile sticky-pile--tap" style="--_n: 3">
@@ -316,17 +314,13 @@
316
314
  grid-template: var(--_size) / var(--_size);
317
315
  padding: var(--_pad) calc(var(--_pad) + (var(--_n) - 1) * var(--_step-x)) calc(var(--_pad) + (var(--_n) - 1) * var(--_step-y)) var(--_pad);
318
316
  }
319
- .sticky-pile__notes > .sticky { --_depth: 0; grid-area: 1 / 1; z-index: calc(7 - var(--_depth)); translate: calc(var(--_depth) * var(--_step-x)) calc(var(--_depth) * var(--_step-y)); }
317
+ .sticky-pile__notes > .sticky { --_depth: 0; grid-area: 1 / 1; z-index: calc(var(--z-pile) - var(--_depth)); translate: calc(var(--_depth) * var(--_step-x)) calc(var(--_depth) * var(--_step-y)); }
320
318
  .sticky-pile__notes > .sticky:nth-child(2) { --_depth: 1; }
321
319
  .sticky-pile__notes > .sticky:nth-child(3) { --_depth: 2; }
322
320
  .sticky-pile__notes > .sticky:nth-child(4) { --_depth: 3; }
323
321
  .sticky-pile__notes > .sticky:nth-child(5) { --_depth: 4; }
324
322
  .sticky-pile__notes > .sticky:nth-child(6) { --_depth: 5; }
325
323
  .sticky-pile__notes > .sticky:nth-child(n + 2) .sticky__foot { display: none; }
326
- .sticky-pile__notes > .sticky:nth-child(4n + 1) { rotate: var(--sticky-board-turn-1, -3deg); }
327
- .sticky-pile__notes > .sticky:nth-child(4n + 2) { rotate: var(--sticky-board-turn-2, 1.5deg); }
328
- .sticky-pile__notes > .sticky:nth-child(4n + 3) { rotate: var(--sticky-board-turn-3, 2.75deg); }
329
- .sticky-pile__notes > .sticky:nth-child(4n) { rotate: var(--sticky-board-turn-4, -1.75deg); }
330
324
  .sticky-pile__press { position: absolute; inset: 0; margin: 0; padding: 0; border: 0; appearance: none; background: none; -webkit-tap-highlight-color: transparent; }
331
325
  .sticky-pile--tap .sticky-pile__notes > .sticky { pointer-events: none; }
332
326
 
@@ -352,16 +346,16 @@
352
346
  pointer-events: none;
353
347
  }
354
348
  .screen--fill > .sticky-answer { position: absolute; }
355
- .sticky-answer--writing { z-index: calc(var(--z-top) + 1); }
356
- .sticky-answer__scrim { position: absolute; inset: 0; background: var(--sticky-answer-scrim-bg, color-mix(in srgb, var(--surface) 82%, transparent)); opacity: 0; visibility: hidden; transition: opacity var(--motion-sheet) ease, visibility 0s var(--motion-sheet); }
349
+ .sticky-answer--writing { z-index: calc(var(--z-top) + var(--z-over)); }
350
+ .sticky-answer__scrim { position: absolute; inset: 0; background: var(--sticky-answer-scrim-bg, color-mix(in srgb, var(--surface) 82%, transparent)); opacity: 0; visibility: hidden; transition: opacity var(--motion-sheet) var(--ease), visibility 0s var(--motion-sheet); }
357
351
  .sticky-answer--writing > .sticky-answer__scrim { opacity: 1; visibility: visible; pointer-events: auto; transition-delay: 0s; }
358
352
  .sticky-answer__note {
359
353
  position: absolute;
360
- top: calc(100% - var(--_size) * var(--sticky-answer-show, 0.75) - max(var(--safe-bottom), var(--kb, 0px)));
354
+ top: calc(100% - var(--_size) * var(--sticky-answer-show, 0.75) - max(var(--safe-bottom), var(--kb, 0rem)));
361
355
  right: var(--sticky-answer-end, var(--screen-pad));
362
- rotate: var(--sticky-answer-tilt, -3deg);
356
+ rotate: var(--sticky-answer-tilt, calc(var(--tilt-base) * -3));
363
357
  pointer-events: auto;
364
- transition: top var(--motion-sheet) ease, right var(--motion-sheet) ease, rotate var(--motion-sheet) ease;
358
+ transition: top var(--motion-sheet) var(--ease), right var(--motion-sheet) var(--ease), rotate var(--motion-sheet) var(--ease);
365
359
  }
366
360
  .sticky-answer--writing > .sticky-answer__note { top: calc(var(--safe-top) + var(--header-h) + var(--sticky-answer-top, var(--sp-5))); right: calc(50% - var(--_size) / 2); rotate: 0deg; }
367
361
  .sticky-answer__under {
@@ -369,7 +363,7 @@
369
363
  top: calc(var(--safe-top) + var(--header-h) + var(--sticky-answer-top, var(--sp-5)) + var(--_size) + var(--sticky-answer-gap, var(--sp-4)));
370
364
  display: grid; gap: var(--sticky-answer-gap, var(--sp-4));
371
365
  opacity: 0; visibility: hidden;
372
- transition: opacity var(--motion-sheet-rest) ease, visibility 0s var(--motion-sheet-rest);
366
+ transition: opacity var(--motion-sheet-rest) var(--ease), visibility 0s var(--motion-sheet-rest);
373
367
  }
374
368
  .sticky-answer--writing > .sticky-answer__under { opacity: 1; visibility: visible; pointer-events: auto; transition-delay: var(--motion-sheet-rest), 0s; }
375
369
  .sticky-answer__tools { display: flex; align-items: center; justify-content: space-between; gap: var(--sticky-answer-tools-gap, var(--sp-3)); }
@@ -393,7 +387,7 @@
393
387
  border-block-end: var(--rule-w) dotted currentColor;
394
388
  font: inherit;
395
389
  }
396
- .sticky-heading__title { min-width: 0; max-width: 100%; font: var(--sticky-heading-title-font, var(--type-label)); line-height: 1.35; /* Thai marks stack taller than the label's 1.2, and overflow clips them */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
390
+ .sticky-heading__title { min-width: 0; max-width: 100%; font: var(--sticky-heading-title-font, var(--type-label)); line-height: var(--lh-caption); /* Thai marks stack taller than the label's 1.2, and overflow clips them */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
397
391
  .sticky-heading__caption { font: var(--sticky-heading-caption-font, var(--type-caption)); color: var(--sticky-heading-caption-ink, var(--card-muted)); white-space: nowrap; } /* the muted words of what it stands on: a note, or the screen */
398
392
  /* on a note: across the top of its paper, and the note's words start under it */
399
393
  .sticky__stuck > .sticky-heading { position: absolute; inset: var(--sp-2) var(--sp-4) auto; }
@@ -402,5 +396,13 @@
402
396
  .sticky-board .sticky:has(> .sticky__stuck > .sticky-heading) .sticky__body { flex: none; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
403
397
  /* in a thread: across its top on the screen's ground, staying there while the notes run under it; the room before the first note is the turned corner's, as the thread's own top */
404
398
  /* the ground is drawn a little wider than the heading each way: a turned note's corner and its shadow stand outside the thread's width */
405
- .sticky-thread > .sticky-heading { position: sticky; inset-block-start: 0; z-index: 2; padding-block-start: var(--sp-2); background: var(--surface); color: var(--on-surface); box-shadow: calc(-1 * var(--sp-3)) 0 0 var(--surface), var(--sp-3) 0 0 var(--surface); }
399
+ .sticky-thread > .sticky-heading { position: sticky; inset-block-start: 0; z-index: var(--z-over-2); padding-block-start: var(--sp-2); background: var(--surface); color: var(--on-surface); box-shadow: calc(-1 * var(--sp-3)) 0 0 var(--surface), var(--sp-3) 0 0 var(--surface); }
406
400
  .sticky-thread--lapped > .sticky-heading { margin-block-end: var(--sp-4); }
401
+
402
+ /* ==== the pile pressed whole, as PRESS in actions.css answers every other control (#62: a block is written in its own file) ==== */
403
+ /* a pile of notes pressed whole (Lh 2026-10-11): the button lies invisible UNDER the notes, so it is the notes that answer — DIM, as a card does (the group dims as one, the
404
+ notes do not show through one another); the ring is the button's own, round the whole pile */
405
+ .sticky-pile__press { cursor: pointer; touch-action: manipulation; }
406
+ .sticky-pile__notes:has(> .sticky-pile__press) { transition: opacity var(--motion-press) var(--ease-out); }
407
+ .sticky-pile__notes:has(> .sticky-pile__press:active) { opacity: var(--press-dim); }
408
+ .sticky-pile__press:focus-visible { outline: var(--focus); outline-offset: 0; }
@@ -61,4 +61,16 @@
61
61
  --signal-ok-onlight --signal-warn-onlight --signal-error-onlight --signal-info-onlight (the signal set: green · yellow · red ·
62
62
  blue — retune the shade, keep the meaning; 4.5:1 on every light ground) and --signal-*-ondark (on the dark side; the ondark ink
63
63
  unless set). --signal-*-soft, the light ground of each, follows by itself */
64
+
65
+ /* ---- pastels: the nine papers of a sticky note (and a stamp's picture ground), and a stamp's paper; light grounds, onlight words ----
66
+ --pastel-cotton --pastel-blush --pastel-tea-leaf --pastel-custard --pastel-salmon --pastel-iceberg --pastel-cocoa --pastel-apricot
67
+ --pastel-water --stamp-paper */
68
+
69
+ /* ---- the rest of what a component shows, each a token (docs/reference.md "A component brings no value of its own") ----
70
+ motion: --motion-* (times) --ease --ease-in --ease-out --ease-in-out --ease-linear --ease-settle
71
+ angle: --tilt-base (1deg; 0deg lays every note, stamp and letter straight)
72
+ layer: --z-under-2 … --z-over-3 fade: --off --fade-faint … --fade-most
73
+ type: per kind (display h1 h2 title body label caption stat) --face-<kind> --fw-<kind> --fs-<kind> --lh-<kind> --track-<kind>;
74
+ the scales --fw-regular --fw-medium --fw-semibold --fw-strong --fw-bold --fw-heavy --fw-black
75
+ --lh-mark --lh-band --lh-badge --lh-solid --lh-tight --lh-field --lh-hand --lh-ruled --track-loose --track-caps */
64
76
  }
package/css/tokens.css CHANGED
@@ -5,8 +5,8 @@
5
5
 
6
6
  Every length is rem so the whole system scales with the root
7
7
  font size (base.css steps it down on narrow phones and it follows
8
- the user's text-size setting). Only borders, rules and focus
9
- rings stay in px — they must stay crisp, not scale.
8
+ the user's text-size setting). Borders, rules and focus rings
9
+ are rem too (Lh 2026-10-20: "units are always rem").
10
10
  4pt grid: 1rem = 16px → --sp-1 = 4px.
11
11
  ============================================================ */
12
12
 
@@ -28,7 +28,7 @@
28
28
  --tap-base: 3rem; /* 48 the size of THINGS: touch targets and control heights (--tap*, --btn-h, --chip-h, --bar-h,
29
29
  --header-h, --fab), icons, avatars, dots, the ring and the tiles. --sp-base is the room BETWEEN things.
30
30
  Text-holders follow the type instead: --badge and --icon-xs (inline with captions) are on --fs-base.
31
- Not on a base: --screen-w (the column), borders / rules / focus (px, crisp), shadows, motion. */
31
+ Not on a base: --screen-w (the column), borders / rules / focus (fixed rem), shadows, motion. */
32
32
 
33
33
  /* ---- spacing (4pt grid): the number in the name is the multiplier ---- */
34
34
  --sp-0: calc(var(--sp-base) * 0.5); /* 2 hairline gaps (title→meta) */
@@ -47,6 +47,7 @@
47
47
  --tap: max(var(--tap-floor), var(--tap-base)); /* 48 primary touch target */
48
48
  --tap-sm: max(var(--tap-floor), calc(var(--tap-base) * 11 / 12)); /* 44 minimum touch target */
49
49
  --tap-lg: max(var(--tap-floor), calc(var(--tap-base) * 7 / 6)); /* 56 hero controls: send, field with action */
50
+ --tap-xl: max(var(--tap-floor), calc(var(--tap-base) * 17 / 12)); /* 68 the biggest round control: a foot of a few big round buttons (IconBtn xl) */
50
51
  --btn-h: calc(var(--tap-base) * 13 / 12); /* 52 button height */
51
52
  --btn-xl-h: calc(var(--tap-base) * 11 / 6); /* 88 the one giant CTA */
52
53
  --chip-h: calc(var(--tap-base) * 19 / 24); /* 38 chip height */
@@ -78,7 +79,10 @@
78
79
  --tile-h: calc(var(--tap-base) * 11 / 4); /* 132 tile / band height */
79
80
 
80
81
  /* ---- radius ---- */
81
- --r-chip: 999px; /* pills, chips, buttons — always fully round */
82
+ /* TILT (Lh 2026-10-20: no unit a component writes for itself): every paper laid at an angle (a note, a stamp, a letter, a card
83
+ sent away) turns by a multiple of this. A theme that wants everything straight sets it to 0deg */
84
+ --tilt-base: 1deg;
85
+ --r-chip: 62.4375rem; /* pills, chips, buttons — always fully round */
82
86
  --r-card: calc(var(--r-base) * 2); /* 41.6 the one card radius: every card, tile, menu, sheet, fab — but a row card (--r-card-row) */
83
87
  --r-card-row: calc(var(--r-base) * 0.625); /* 13 a row card's corner (Lh 2026-10-17: a small card's corner shrinks to keep the big card's PROPORTION). Measured at 375:
84
88
  whole cards stand 186–437 tall, corner/height 0.095–0.224 (Card · Skeleton 0.122); a two-line row stands 88, so its
@@ -87,13 +91,19 @@
87
91
  --r-control: calc(var(--r-base) * 0.875); /* 18.2 fields, thumbs, menu rows */
88
92
  --r-badge: calc(var(--r-base) * 0.625); /* 13 tile badge, bars */
89
93
 
90
- /* ---- borders / rules / elevation (px on purpose: crisp at any scale) ---- */
91
- --border-w: 1.5px;
92
- --hairline-w: 2px; /* the thinnest mark: a sheet's grab line (Lh 2026-10-06: 1px, then 2px the same day) */
93
- --rule-w: 2px; /* dotted connectors, picker rules */
94
- --scroll-w: 1px; /* the scrollbar of a .bleed track: a hairline of ink */
95
- --focus: 2px solid currentColor; /* :focus-visible ring */
96
- --shadow-card: 0 1px 2px rgb(0 0 0 / 0.06), 0 0.5rem 1.5rem rgb(0 0 0 / 0.08);
94
+ /* ---- borders / rules / elevation (rem, like every length) ---- */
95
+ /* one device-independent pixel in rem: what the platform needs to keep a hidden word in the accessibility tree, and the half-width
96
+ of a drawn line. The browser's own text size: a field under it makes iOS zoom in on focus */
97
+ --px: 0.0625rem;
98
+ --browser-text: 1rem;
99
+ /* the wireframe map's two street directions (.map) */
100
+ --street-1: 75deg; --street-2: -18deg;
101
+ --border-w: 0.09375rem;
102
+ --hairline-w: 0.125rem; /* the thinnest mark: a sheet's grab line (Lh 2026-10-06: 1px, then 2px the same day) */
103
+ --rule-w: 0.125rem; /* dotted connectors, picker rules */
104
+ --scroll-w: 0.0625rem; /* the scrollbar of a .bleed track: a hairline of ink */
105
+ --focus: 0.125rem solid currentColor; /* :focus-visible ring */
106
+ --shadow-card: 0 0.0625rem 0.125rem rgb(0 0 0 / 0.06), 0 0.5rem 1.5rem rgb(0 0 0 / 0.08);
97
107
  --shadow-float: 0 0.25rem 0.75rem rgb(0 0 0 / 0.14);
98
108
  --shadow-sheet: 0 -0.5rem 2rem rgb(0 0 0 / 0.16); /* sheets, lifted cards */
99
109
 
@@ -103,11 +113,44 @@
103
113
  --motion-quick: 0.2s; /* a small state flip that isn't a press: a chevron turning, a popover/banner opening */
104
114
  --motion-slide: 0.22s; /* a centred card sliding out and the next in (card.css) — quick, a page turn (Lh, 2026-09-12) */
105
115
  --motion-flip: 0.45s; /* a postcard turning over (media.css) — slow enough to read as a card in the hand */
116
+ --motion-spin: 0.9s; /* a busy ring's one turn (Spinner, a busy FormNote) */
117
+ --motion-blink: 1.1s; /* a code cell's caret, on and off once */
118
+ --motion-flash: 0.5s; /* a secret code's digit seen before its dot takes it */
119
+ --motion-shake: 0.4s; /* a wrong code shaking once */
120
+ --motion-pulse: 0.4s; /* a sheet's grip, ink to ink and back, while the app reloads */
121
+ --motion-run: 6s; /* a long title running sideways, until its own length is measured (--_run) */
122
+ --motion-stagger: 70ms; /* one stacked sheet after another as they tuck or untuck */
123
+ /* LAYERS (Lh 2026-10-20: no number a component writes for itself): the order of things INSIDE one piece, which keeps its own
124
+ stacking context. Under its ground, flat, over it, over that, the picked one on top */
125
+ --z-under-2: -2; --z-under: -1; --z-flat: 0; --z-over: 1; --z-over-2: 2; --z-over-3: 3;
126
+ --z-pile: 7; /* a sticky pile's top note: one over the six it can show, each note under it one lower (StickyPile) */
127
+ /* EASING (Lh 2026-10-20: a component writes no curve of its own): the six ways a thing moves, read by every transition and animation.
128
+ --ease-settle is the one a moved row or card lands with (Row, CardFeed: Web Animations read it) */
129
+ --ease: ease;
130
+ --ease-in: ease-in;
131
+ --ease-out: ease-out;
132
+ --ease-in-out: ease-in-out;
133
+ --ease-linear: linear;
134
+ --ease-settle: cubic-bezier(0.2, 0.8, 0.2, 1);
135
+ --ease-step: steps(1); /* on or off with nothing between: a code cell's caret blinking, a secret digit before its dot */
106
136
  --motion-press: 0.12s; /* a control giving under the finger and springing back (actions.css PRESS) */
107
137
  --press-scale: 0.96; /* how far a finger-sized control gives — a share of its size */
138
+ --scale-lift: 1.06; /* a stamp lifted off the paper while the finger holds it */
139
+ --scale-pick: 1.7; /* a mood's dot, the one picked */
140
+ --scale-arrive: 0.6; /* a stamp's remove button, the size it grows from */
108
141
  --press-dim: 0.6; /* how far a card-wide one (a row, a summary line, a card) fades under the finger */
109
142
  --press-shade: 0.93; /* how far something with a thing behind it (a peeked sheet, a postcard) darkens under the finger */
110
143
  --off: 0.4; /* a disabled control's fade */
144
+ /* FADES (Lh 2026-10-20: no opacity a component writes for itself): what each lets through. Nine uses had eight values; one name each
145
+ until the system settles on fewer */
146
+ --fade-faint: 0.25; /* a halo behind an avatar */
147
+ --fade-dim: 0.3; /* a sheet's grip at rest, a day that cannot be picked */
148
+ --fade-soft: 0.4; /* the dots between a trio's answers */
149
+ --fade-half: 0.5; /* a step's button while the step is busy */
150
+ --fade-hint: 0.55; /* a field's placeholder */
151
+ --fade-mild: 0.65; /* a route's line after its code */
152
+ --fade-near: 0.85; /* an icon row */
153
+ --fade-most: 0.9; /* a quote's mark */
111
154
 
112
155
  /* ---- z-index: the few layers that stack ABOVE ordinary content, low to high ---- */
113
156
  --z-bar: 30; /* .action-bar, .float-bar — above .sheet-stack sheets (--level 1-5) */
@@ -143,18 +186,31 @@
143
186
  --fs-hero: calc(var(--fs-base) * 2.5); /* 55 the one number on a sheet */
144
187
  --fs-giant: calc(var(--fs-base) * 2.9); /* 64 data bands */
145
188
 
146
- /* level weight size / line-height family */
147
- --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) */
148
- --track-display: -0.02em;
149
- --type-h1: 900 var(--fs-h1) / 1.2 var(--font-display);
150
- --track-h1: -0.015em;
151
- --type-h2: 700 var(--fs-h2) / 1.25 var(--font-display);
152
- --track-h2: -0.01em;
153
- --type-title: 650 var(--fs-title) / 1.3 var(--font-ui);
154
- --type-body: 400 var(--fs-body) / 1.5 var(--font-ui);
155
- --type-label: 600 var(--fs-label) / 1.2 var(--font-ui); /* chips, buttons */
156
- --type-caption: 500 var(--fs-caption) / 1.35 var(--font-ui);
157
- --type-stat: 800 var(--fs-stat) / 1.05 var(--font-display); /* big numbers */
189
+ /* TYPE (Lh 2026-10-20: every kind and every size has its tokens; a component writes no type value of its own).
190
+ THE SCALES: weight and leading, each step named once. A kind below picks a step of each; a piece that needs a step alone reads it. */
191
+ --fw-regular: 400; --fw-medium: 500; --fw-semibold: 600; --fw-strong: 650; --fw-bold: 700; --fw-heavy: 800; --fw-black: 900;
192
+ --lh-mark: 0.55; /* a quotation mark three lines tall, set into the first line */
193
+ --lh-band: 0.9; /* a data band's giant number */
194
+ --lh-badge: 0.95; /* a date box's two lines (JourneyBadge) */
195
+ --lh-solid: 1; /* a line as tall as its letters: a number in a disc, an icon's glyph, one big number */
196
+ --lh-tight: 1.1; /* a day's number, a tile's value */
197
+ --lh-field: 1.4; /* the words in a field */
198
+ --lh-hand: 1.6; /* a letter written by hand */
199
+ --lh-ruled: 1.7; /* a ruled sheet: the line under every line of writing */
200
+ --track-loose: 0.02em; /* initials in an avatar */
201
+ --track-caps: 0.08em; /* a kicker in capitals */
202
+ /* THE EIGHT KINDS: each has its face, weight, size (--fs-* above), leading and tracking, and its style --type-<kind> is made of them */
203
+ --fw-display: var(--fw-heavy); --lh-display: 1.15; --track-display: -0.02em; /* leading: Thai marks stack to 1.1em up and 0.3em down in FC Pride — 1.02 clipped them (2026-09-10) */
204
+ --fw-h1: var(--fw-black); --lh-h1: 1.2; --track-h1: -0.015em;
205
+ --fw-h2: var(--fw-bold); --lh-h2: 1.25; --track-h2: -0.01em;
206
+ --fw-title: var(--fw-strong); --lh-title: 1.3; --track-title: normal;
207
+ --fw-body: var(--fw-regular); --lh-body: 1.5; --track-body: normal;
208
+ --fw-label: var(--fw-semibold); --lh-label: 1.2; --track-label: normal; /* chips, buttons */
209
+ --fw-caption: var(--fw-medium); --lh-caption: 1.35; --track-caption: normal;
210
+ --fw-stat: var(--fw-heavy); --lh-stat: 1.05; --track-stat: normal; /* big numbers */
211
+ --face-display: var(--font-display); --face-h1: var(--font-display); --face-h2: var(--font-display); --face-stat: var(--font-display);
212
+ --face-title: var(--font-ui); --face-body: var(--font-ui); --face-label: var(--font-ui); --face-caption: var(--font-ui);
213
+ /* --type-<kind> is set where a palette or a mode is (below), so a weight or a face tuned there reaches the words inside */
158
214
 
159
215
  /* ============================================================
160
216
  COLOR ROLES — wireframe defaults (grayscale).
@@ -237,6 +293,12 @@
237
293
  --ink-rail: color-mix(in srgb, currentColor 30%, transparent); /* todo steps, ghost edges */
238
294
  --accent-soft: color-mix(in srgb, var(--accent) 22%, transparent); /* calendar marks */
239
295
  --accent-mid: color-mix(in srgb, var(--accent) 50%, transparent);
296
+ /* PASTELS (Lh 2026-10-20: a component never brings a colour of its own; every colour is the theme's): the nine pastel papers of a
297
+ sticky note, a note chooser's swatch and a stamp's picture ground (src/sticky/tones.ts names them), and a stamp's paper. Light
298
+ grounds in every palette: their words are the onlight set. A theme retunes them here; a knob (--sticky-<tone>-bg) retunes one place */
299
+ --pastel-cotton: #f3efe2; --pastel-blush: #f6d5da; --pastel-tea-leaf: #d5e8c5; --pastel-custard: #f6e9a0; --pastel-salmon: #f3c7b3;
300
+ --pastel-iceberg: #cfe0ee; --pastel-cocoa: #dccbbd; --pastel-apricot: #f6d49a; --pastel-water: #bcd6f3;
301
+ --stamp-paper: #fffdf8;
240
302
  --scrim: color-mix(in srgb, #000 50%, transparent); /* dim stage, cover gradient — ALWAYS black, never the brand (Lh 2026-09-24) */
241
303
  }
242
304
 
@@ -261,6 +323,19 @@
261
323
  sit on <body>: a custom property is resolved where it is declared. A signal's SOFT ground (what a Snackbar stands on) is its
262
324
  onlight shade washed 14% into the surface (tone 2's formula) */
263
325
  :root, [data-palette], [data-mode] {
326
+ /* each kind's style, from its tokens: set here so a palette or a mode that tunes a kind's weight, size, leading or face reaches .t-<kind> */
327
+ --type-display: var(--fw-display) var(--fs-display) / var(--lh-display) var(--face-display);
328
+ --type-h1: var(--fw-h1) var(--fs-h1) / var(--lh-h1) var(--face-h1);
329
+ --type-h2: var(--fw-h2) var(--fs-h2) / var(--lh-h2) var(--face-h2);
330
+ --type-title: var(--fw-title) var(--fs-title) / var(--lh-title) var(--face-title);
331
+ --type-body: var(--fw-body) var(--fs-body) / var(--lh-body) var(--face-body);
332
+ --type-label: var(--fw-label) var(--fs-label) / var(--lh-label) var(--face-label);
333
+ --type-caption: var(--fw-caption) var(--fs-caption) / var(--lh-caption) var(--face-caption);
334
+ --type-stat: var(--fw-stat) var(--fs-stat) / var(--lh-stat) var(--face-stat);
335
+ /* shadows of paper laid on paper: mixed from the onlight ink, so re-derived where a palette is set */
336
+ --shadow-paper: 0 var(--sp-1) var(--sp-2) color-mix(in srgb, var(--ink-onlight) 16%, transparent); /* a stamp lying on paper: follows its teeth (drop-shadow) */
337
+ --shadow-lift: 0 var(--sp-0) var(--sp-1) color-mix(in srgb, var(--ink-onlight) 35%, transparent); /* a small disc lifted over a picture */
338
+ --shadow-fold: calc(-1 * var(--sp-0)) calc(-1 * var(--sp-0)) var(--sp-1) color-mix(in srgb, var(--ink-onlight) 8%, transparent); /* a note's folded corner */
264
339
  --on-surface: color-mix(in srgb, var(--ink-ondark) calc(var(--surface-dark) * 100%), var(--ink-onlight));
265
340
  --on-surface-muted: color-mix(in srgb, var(--ink-ondark-muted) calc(var(--surface-dark) * 100%), var(--ink-onlight-muted));
266
341
  --action-ink: color-mix(in srgb, var(--ink-ondark) calc(var(--action-dark) * 100%), var(--ink-onlight));
@@ -281,7 +356,7 @@
281
356
  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
357
  tile) takes its ground's side: `--dark: inherit`. inkGrounds() measures it; docs/reference.md "Ink follows its ground" */
283
358
  :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, .map__pin, .sheet--paper > .sheet__body,
359
+ .float-bar__pill, .snackbar__body, .delta, .track, .tile-badge, .avatar, .bubble__body, .map__hint, .map__pin, .sheet--paper > :is(.sheet__body, .card__foot),
285
360
  .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
361
  --card-ink: color-mix(in srgb, var(--ink-ondark) calc(var(--dark) * 100%), var(--ink-onlight));
287
362
  --card-muted: color-mix(in srgb, var(--ink-ondark-muted) calc(var(--dark) * 100%), var(--ink-onlight-muted));
@@ -294,8 +369,8 @@
294
369
  }
295
370
  :where(body, [data-palette], [data-mode]) { --dark: var(--surface-dark); }
296
371
 
297
- :root { --safe-top: calc(env(safe-area-inset-top, 0px) + var(--vv-top, 0px)); /* + the phone's pan while a keyboard is up (cardds.js) */ --safe-bottom: env(safe-area-inset-bottom, 0px); }
298
- * { --_safe-b-open: max(0px, var(--safe-bottom) - var(--kb, 0px)); }
372
+ :root { --safe-top: calc(env(safe-area-inset-top, 0rem) + var(--vv-top, 0rem)); /* + the phone's pan while a keyboard is up (cardds.js) */ --safe-bottom: env(safe-area-inset-bottom, 0rem); }
373
+ * { --_safe-b-open: max(0rem, var(--safe-bottom) - var(--kb, 0rem)); }
299
374
 
300
375
  @property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
301
376
  @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 */
@@ -298,9 +298,9 @@ export declare function Chip({ on, muted, dot, sm, as, href, className, children
298
298
  onTransitionRunCapture?: import("react").TransitionEventHandler<HTMLSpanElement> | undefined;
299
299
  onTransitionStart?: import("react").TransitionEventHandler<HTMLSpanElement> | undefined;
300
300
  onTransitionStartCapture?: import("react").TransitionEventHandler<HTMLSpanElement> | undefined;
301
+ className: string;
301
302
  href: string | undefined;
302
303
  type: string | undefined;
303
- className: string;
304
304
  }, HTMLSpanElement>;
305
305
  export interface ChipPickProps extends Omit<ComponentProps<'label'>, 'onChange'> {
306
306
  /** radio for pick-one (a time slot), checkbox for pick-many (skills); `sign` defaults to checkbox */
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createElement } from 'react';
3
3
  import { cx } from '../cx.js';
4
+ import { tagged } from '../pressable.js';
4
5
  import { Dot } from './Dot.js';
5
6
  /** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `sm` = a small label only; `on` when it is the live one. For a choice use ChipPick (`sign` for the +/× interest picker). */
6
7
  export function Chip({ on, muted, dot, sm, as, href, className, children, ...rest }) {
7
- const el = as ?? (href ? 'a' : 'span');
8
- return createElement(el, { href, type: el === 'button' ? 'button' : undefined, className: cx('chip', on && 'chip--on', muted && 'chip--muted', sm && 'chip--sm', className), ...rest }, dot && _jsx(Dot, { b: dot === 'b', tone: dot === 'a' || dot === 'b' ? undefined : dot }), children);
8
+ const { el, props } = tagged(as, href);
9
+ return createElement(el, { ...props, className: cx('chip', on && 'chip--on', muted && 'chip--muted', sm && 'chip--sm', className), ...rest }, dot && _jsx(Dot, { b: dot === 'b', tone: dot === 'a' || dot === 'b' ? undefined : dot }), children);
9
10
  }
10
11
  /** ChipPick — a chip that is a choice, on a sheet and never in a Card (the sheet is the paper the member fills in, Lh 2026-10-18): full-width in a ChipGrid (9:00 · 9:30 · …), or `sign` on a ChipRow (+ off, × on — the
11
12
  * interest picker); the picked one fills with accent. A native radio/checkbox inside the label carries the state. */