@usableapp/cardds 0.1.7 → 0.2.0

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/README.md CHANGED
@@ -105,8 +105,8 @@ cp -R ~/Sites/cardds/css ~/Sites/cardds/fonts ~/Sites/cardds/cardds.css \
105
105
  folder next to it, and `fonts/` beside that (`css/fonts.css` reaches the faces
106
106
  as `../fonts/`). `icons.svg` is referenced by path from your markup
107
107
  (`<use href="icons.svg#bell">`), so put it where those references resolve.
108
- Then follow the rules below — the short version is: tune `palette.css` and
109
- nothing else, and never write a raw hex or px in a component.
108
+ Then follow the rules below — the short version is: tune your own `theme.css`
109
+ (a copy of `css/theme.template.css`, see "A project's theme") and nothing else, and never write a raw hex or px in a component.
110
110
 
111
111
  The repo is private, so cloning needs an account with access (`everysundays`
112
112
  on GitHub) — set that up as an SSH host alias if you already use a different
@@ -297,6 +297,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
297
297
  |---|---|---|
298
298
  | `css/fonts.css` + `fonts/` | the two `@font-face`s: PK Nonthaburi (body, `--font-ui`), FC Pride (display, `--font-display`); the font files and their licences | when swapping a face |
299
299
  | `css/tokens.css` | spacing, radius, type levels, motion (`--motion-sheet`), the human centre (`--screen-centre`), grayscale role defaults; registers the two sheet tokens (`--sheet-peek`, `--sheet-half`) so script can read them resolved | rarely |
300
+ | `css/theme.template.css` | **a project's theme** — the four size bases + the colour roles, at their defaults; NOT in the bundle: copied into the project as its `theme.css`, loaded after `cardds.css` | copy it, don't edit it |
300
301
  | `css/palette.css` | **color settings — palettes as role-token overrides** | yes, this one |
301
302
  | `css/base.css` | reset, surface, typography classes, `.base-content` | rarely |
302
303
  | `css/card.css` | `.card` + its skeleton (`.card__head` / content / `.card__foot`), `.card-list`, `.card--row`, `.icon-row`, stats, `.card--centre` (one card at the human centre of a fill screen) and `.screen__centre` (a small group there) | when adding variants |
@@ -309,9 +310,9 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
309
310
  | `css/numbers.css` | `.ring` gauge, `.track` steps, `.card--band` + `.band-stack`, `.dotgrid`, `.picker`, `.badge`, `.bars` | big numbers |
310
311
  | `css/people.css` | `.avatar` (sm/lg/xl, outline, add, on, halo), `.avatar-stack`, `.avatar-pick` | people |
311
312
  | `css/choice.css` | `.check`, `.toggle`, `.chip-grid` + `.chip--pick` (+ `--sign`), `.day-strip`, `.calendar`, `.mood`, `.pin`, `.composer`, `.slider` | choice controls |
312
- | `css/lists.css` | `.row` / `.rows` (frameless rows in a card), `.kv`, `.kv-grid`, `.timeline`, `.legend`, `.link` | lists inside cards |
313
+ | `css/lists.css` | `.row` / `.rows` (frameless rows in a card), `.kv`, `.kv-grid`, `.thread` + `.bubble` (chat: theirs left, mine right, no tail), `.timeline`, `.legend`, `.link` | lists inside cards |
313
314
  | `css/media.css` | `.card--cover`, `.quote`, `.tile-grid` + `.tile`, `.card--fold`, `.mosaic`, `.wave` | media & display |
314
- | `css/layover.css` | `.modal` (the ask: `__lift` + `__drawer`, closes on any drawer button → `cardds:modal`), `.menu` (popover), `.banner` (details), `.float-bar`, `.callout`, `.chip--bubble`, `.deck` | cards over content |
315
+ | `css/layover.css` | `.modal` (the ask: `__lift` + `__drawer`, closes on any drawer button → `cardds:modal`), `.menu` (popover), `.banner` (details), `.float-bar`, `.callout`, `.deck` | cards over content |
315
316
  | `icons.svg` | Lucide sprite (ISC), 56 minimal stroke icons | add symbols as needed |
316
317
  | `src/` → `dist/` | the React library: `src/<group>/<Name>.tsx`, one thin component per pattern, JSDoc = the rule; built by `npm run build` (tsc) into `dist/` with `.d.ts`; `src/type/icons.ts` is generated from `icons.svg` | when adding a component |
317
318
  | `gallery/` | the gallery site: `npm run dev` → `/` every story, `/:Name` one component (`gallery/src/Gallery.tsx` globs `.design-sync/previews/`) | the demo |
@@ -502,6 +503,63 @@ Demo: the *skeleton* block at the top of `elements.html`.
502
503
  `--on-accent` to its own ink and bg, so every accent consumer (buttons,
503
504
  checks, toggles, rings, selected days) inverts without extra rules.
504
505
 
506
+ ## A project's theme
507
+
508
+ A project retunes cardds from ONE file of its own, and nothing else. Copy
509
+ `css/theme.template.css` into the project as `theme.css`, load it **after**
510
+ `cardds.css`, change values:
511
+
512
+ ```css
513
+ :root {
514
+ --sp-base: 0.3rem; /* roomier */
515
+ --r-base: 0.5rem; /* tighter corners */
516
+ --color-surface: #faf6ee; --color-ink: #1f1b16; --color-paper: #fff;
517
+ --color-brand: #c2410c; --color-on-brand: #fff;
518
+ }
519
+ ```
520
+
521
+ ```html
522
+ <html data-palette="own"> <!-- colours of your own = coloured mode; any value no stock palette uses -->
523
+ ```
524
+
525
+ That file is the whole public surface:
526
+
527
+ | what | tokens | moves |
528
+ |---|---|---|
529
+ | room **between** things | `--sp-base` (0.25rem = 4) | every `--sp-N` = base × N → paddings, gaps, the screen's inset, the bar's reserve, the sheet's peek |
530
+ | type | `--fs-base` (1rem = 16) | every `--fs-*`, `--badge`, `--icon-xs` |
531
+ | corners | `--r-base` (1rem = 16) | every `--r-*`; `0` = square. `--r-chip` stays fully round |
532
+ | the size **of** things | `--tap-base` (3rem = 48) | `--tap*`, `--btn-h`, `--chip-h`, `--bar-h`, `--header-h`, `--fab`, `--icon*`, `--avatar*`, `--dot*`, `--ring*`, `--tile-*`. `--tap*` never under `--tap-floor` (44px) |
533
+ | faces | `--font-ui`, `--font-display` | |
534
+ | colour | `--color-surface`, `--color-ink`, `--color-paper`, `--color-brand`, `--color-on-brand` — no default: unset = the wireframe | every role: surface / words, the three card tones (paper · the brand washed 14% into the surface · the brand), the muted inks (the ink washed into their ground), accent, neutral actions, `--dot-a` |
535
+ | colour roles (the escape hatch) | `--surface`, `--on-surface(-muted)`, `--card-{1,2,3}-{bg,ink}`, `--card-border`, `--card-muted`, `--card-2-muted`, `--accent`, `--on-accent`, `--action-{bg,ink,border}`, `--pos`, `--neg`, `--dot-{a,b}` | a role set directly wins over its derivation; the tints derive from the roles |
536
+
537
+ Rules:
538
+
539
+ - **A base, never a multiplier.** Every size token is `calc(base × multiplier)`
540
+ in `tokens.css`; the multipliers are the system's proportions. A project
541
+ never sets a derived token (`--sp-4`, `--fs-h1`, `--r-card`, `--tap`…), a
542
+ private one (`--_*`), or a published one (`--card-pad`, `--bar-reserve`, `--sheet-*`).
543
+ - **On `:root` only.** The derived tokens are computed where they are declared
544
+ (`:root`), so a base set on a subtree does not reach them.
545
+ - **After `cardds.css`.** A stock palette's block and a project's `:root` weigh
546
+ the same; the later file wins.
547
+ - **Coloured mode is the attribute.** `data-palette`, whatever its value, is what
548
+ trades the wireframe's borders for the card's shadow and the controls' soft
549
+ edge (`palette.css`, `forms.css`). Colours of your own → `<html data-palette="own">`.
550
+ Without it the colours still apply, on wireframe borders.
551
+ - **cardds does not judge the colours.** No contrast check, no warning — a
552
+ theme's values are the project's call. The derivations are only starting points;
553
+ any role can be set outright.
554
+ - `--text-scale` (below) is the member's setting, not the theme's: it scales
555
+ everything, the bases included.
556
+
557
+ Try it live: the gallery's nav (`npm run dev`) has a slider per size base, and
558
+ palette `own` shows a colour well per colour base.
559
+ `tests/tokens.spec.js` holds the defaults to the px they always were and the
560
+ template to the defaults; the step / centre geometry runs at two extreme
561
+ themes (`tight`, `loose`).
562
+
505
563
  ## New palette
506
564
 
507
565
  Copy any block in `palette.css`, rename, retune:
@@ -517,9 +575,10 @@ Copy any block in `palette.css`, rename, retune:
517
575
 
518
576
  ## Scale: everything is rem
519
577
 
520
- Every length token is rem (`--sp-4: 1rem`, `--tap: 3rem`, `--r-card: 2rem`,
521
- `--fs-h1: 1.75rem` …), so the whole system resizes from one number: the root
522
- font size. `base.css` keeps it at `100%` (the user's text-size setting, 16px by
578
+ Every length token resolves to rem — a multiplier × one of four rem bases
579
+ (`--sp-4` = `--sp-base` × 4 = 1rem, `--tap` = `--tap-base` = 3rem, `--r-card` =
580
+ `--r-base` × 2, `--fs-h1` = `--fs-base` × 1.75; see "A project's theme") — so the
581
+ whole system still resizes from one number: the root font size. `base.css` keeps it at `100%` (the user's text-size setting, 16px by
523
582
  default) and steps narrow phones (≤360px) to `93.75%`, which keeps the 25rem
524
583
  column, its 48px taps and its type in proportion on 320–360 screens. Only
525
584
  `--border-w`, `--rule-w` and the focus ring stay in px, so edges stay crisp.
package/css/layover.css CHANGED
@@ -271,22 +271,6 @@
271
271
  }
272
272
  .callout__title { font: var(--type-title); }
273
273
  .callout__sub { font: var(--type-caption); opacity: 0.7; }
274
- /* speech chip: a chip with the same tail (mascot / greeting) */
275
- .chip--bubble { position: relative; }
276
- .chip--bubble::after {
277
- content: "";
278
- position: absolute;
279
- left: var(--sp-4);
280
- bottom: calc(-1 * var(--sp-2) + var(--border-w));
281
- width: var(--sp-3);
282
- height: var(--sp-3);
283
- rotate: 45deg;
284
- background: inherit;
285
- border: inherit;
286
- border-top: 0;
287
- border-left: 0;
288
- }
289
-
290
274
  /* ---- deck: swipe cards; the last child is on top ----
291
275
  <div class="deck"><article class="card">…</article><article class="card">…</article><article class="card card--fold">front</article></div>
292
276
  <div class="deck__actions"><button class="icon-btn">×</button><button class="icon-btn icon-btn--invert">✓</button></div> */
package/css/lists.css CHANGED
@@ -87,6 +87,56 @@
87
87
  .kv-cell__key { font: var(--type-caption); color: var(--card-muted); }
88
88
  .card--3 .kv-cell__key { color: inherit; opacity: 0.7; }
89
89
 
90
+ /* ---- bubble: a chat message — theirs on the left, mine on the right (Lh 2026-09-18, replaces the chip's tail) ----
91
+ No tail: a rounded block whose corner nearest the sender is square-ish
92
+ (bottom-left for theirs, bottom-right for mine), the way messaging apps
93
+ draw it. Under it a caption line (time · a tick), beside it small round
94
+ actions (a heart, more). A .thread stacks them.
95
+ <div class="thread">
96
+ <div class="bubble"><p class="bubble__body">…</p><span class="bubble__actions">…icon-btns…</span><span class="bubble__meta">Dec 4 · 8:15</span></div>
97
+ <div class="bubble bubble--right">…</div>
98
+ </div> */
99
+ .thread { display: grid; gap: var(--sp-4); }
100
+ /* two columns: the message (max-content, but it gives way — minmax(0) — so a long one shrinks
101
+ and wraps while the actions stay beside it) and the actions; the meta on a second row under the message */
102
+ .bubble {
103
+ display: grid;
104
+ grid-template-columns: minmax(0, max-content) auto;
105
+ justify-content: start;
106
+ align-items: center;
107
+ gap: var(--sp-1) var(--sp-3);
108
+ max-width: 88%;
109
+ margin-right: auto; /* theirs: hugs the left */
110
+ }
111
+ .bubble--right { grid-template-columns: auto minmax(0, max-content); justify-content: end; margin-right: 0; margin-left: auto; } /* mine: hugs the right, the actions on its left */
112
+ .bubble__body, .bubble__meta { grid-column: 1; }
113
+ .bubble__actions { grid-column: 2; grid-row: 1; }
114
+ .bubble--right > :is(.bubble__body, .bubble__meta) { grid-column: 2; }
115
+ .bubble--right > .bubble__actions { grid-column: 1; }
116
+ .bubble__body {
117
+ margin: 0;
118
+ min-width: 0;
119
+ padding: var(--sp-3) var(--sp-4);
120
+ border-radius: var(--r-card);
121
+ border-bottom-left-radius: var(--r-control);
122
+ background: var(--card-2-bg);
123
+ color: var(--card-2-ink);
124
+ font: var(--type-body);
125
+ overflow-wrap: anywhere;
126
+ }
127
+ .bubble--right > .bubble__body {
128
+ border-bottom-left-radius: var(--r-card);
129
+ border-bottom-right-radius: var(--r-control);
130
+ background: var(--accent);
131
+ color: var(--on-accent);
132
+ text-align: right;
133
+ }
134
+ .bubble__actions { display: flex; gap: var(--sp-1); flex: none; opacity: 0.7; }
135
+ .bubble__meta { display: flex; align-items: center; gap: var(--sp-1); font: var(--type-caption); color: var(--on-surface-muted); }
136
+ .bubble--right > .bubble__meta { justify-self: end; }
137
+ .bubble__meta > .icon { color: var(--accent); }
138
+ .card--3 .bubble__meta { color: inherit; opacity: 0.7; }
139
+
90
140
  /* ---- timeline: a hairline with date chips, entries to the right ----
91
141
  <ol class="timeline"><li class="timeline__item"><span class="timeline__mark">Wed 14</span><p class="t-body">…</p></li>…</ol> */
92
142
  .timeline {
package/css/palette.css CHANGED
@@ -108,5 +108,10 @@
108
108
 
109
109
  /* Colored palettes trade wireframe outlines for soft elevation. */
110
110
  [data-palette] :is(.card, .band-stack) { box-shadow: var(--shadow-card); }
111
+ /* COLOURED MODE is the attribute, whatever its value: a project with colours of its own (the five
112
+ --color-* bases, or roles, in its theme.css) writes <html data-palette="own"> — no block here matches
113
+ "own", so only the three generic rules apply: the card's shadow, the soft outline, the field's soft
114
+ edge (forms.css); and the two wireframe borders step back, as in every stock palette. */
115
+ [data-palette] { --card-border: transparent; --action-border: transparent; }
111
116
  /* …but controls keep a faint edge so they still read as controls */
112
117
  [data-palette] { --outline: color-mix(in srgb, currentColor 25%, transparent); }
@@ -0,0 +1,49 @@
1
+ /* ============================================================
2
+ cardds/theme.template.css — A PROJECT'S THEME. Copy, rename, retune.
3
+
4
+ Not part of the bundle: copy this file into the project as its own
5
+ `theme.css` and load it AFTER cardds.css. It is the whole public
6
+ surface of the system — the four size bases and the colour roles.
7
+ Everything else in tokens.css is derived from these; a project never
8
+ sets a derived token (--sp-4, --fs-h1, --r-card, --tap, --sheet-*…)
9
+ and never a multiplier.
10
+
11
+ The values below are the defaults (the wireframe): as shipped, this
12
+ file changes nothing (tests/tokens.spec.js holds it to that).
13
+
14
+ On :root only. The size tokens are computed where tokens.css declares
15
+ them (:root), so a base set on a subtree does not reach them.
16
+ A stock palette's colours and this file's have the same weight — the
17
+ later file wins, so: after cardds.css.
18
+ ============================================================ */
19
+
20
+ :root {
21
+ /* ---- size: four bases, the multipliers stay the system's ---- */
22
+ --sp-base: 0.25rem; /* 4 the room BETWEEN things: every --sp-N = base × N, so paddings and gaps */
23
+ --fs-base: 1rem; /* 16 the type: every --fs-*, the count badge, the inline icon */
24
+ --r-base: 1rem; /* 16 the corners: every --r-* (0 = square; chips and buttons stay fully round) */
25
+ --tap-base: 3rem; /* 48 the size of THINGS: touch targets, control heights, icons, avatars, ring, tiles
26
+ (a touch target never goes under 44px, whatever this says) */
27
+ /* the whole system at once (a member's text-size setting) is --text-scale, a unitless number — not a theme's job */
28
+
29
+ /* ---- faces ---- */
30
+ --font-ui: "PK Nonthaburi", -apple-system, system-ui, sans-serif;
31
+ --font-display: "FC Pride", var(--font-ui);
32
+
33
+ /* ---- colour: five bases, the roles derive (tone 2, the muted inks, the tints) ----
34
+ No default — unset = the wireframe. With colours of your own, put the page in coloured mode:
35
+ <html data-palette="own"> (any value no stock palette uses): cards take a shadow for their
36
+ wireframe border, controls a soft edge. */
37
+ /* --color-surface: #f9e8dc; */ /* the app's ground */
38
+ /* --color-ink: #241f1b; */ /* the words */
39
+ /* --color-paper: #ffffff; */ /* the plain card, neutral buttons and fields */
40
+ /* --color-brand: #ef4e0b; */ /* the primary action, the strong card */
41
+ /* --color-on-brand: #fff4ec; */ /* the words on the brand */
42
+
43
+ /* ---- colour roles: only where a derivation is not what you want — a role set here wins over it ----
44
+ --surface --on-surface --on-surface-muted
45
+ --card-1-bg --card-1-ink --card-2-bg --card-2-ink --card-3-bg --card-3-ink (three card tones)
46
+ --card-border --card-muted --card-2-muted
47
+ --accent --on-accent --action-bg --action-ink --action-border
48
+ --pos --neg --dot-a --dot-b */
49
+ }
package/css/tokens.css CHANGED
@@ -15,56 +15,69 @@
15
15
  --screen-w: 25rem; /* 400: mobile column — .screen, .action-bar, .sheet-stage */
16
16
  --screen-centre: 40%; /* the human centre of a screen: the middle of its top four fifths — the bottom fifth is the thumb's (Lh, 2026-09-10). .card--centre sits here */
17
17
 
18
- /* ---- spacing (4pt grid) ---- */
19
- --sp-0: 0.125rem; /* 2 hairline gaps (title→meta) */
20
- --sp-1: 0.25rem; /* 4 */
21
- --sp-2: 0.5rem; /* 8 */
22
- --sp-3: 0.75rem; /* 12 */
23
- --sp-4: 1rem; /* 16 */
24
- --sp-5: 1.25rem; /* 20 */
25
- --sp-6: 1.5rem; /* 24 */
26
- --sp-8: 2rem; /* 32 */
27
- --sp-10: 2.5rem; /* 40 */
28
- --sp-12: 3rem; /* 48 */
18
+ /* ---- the four bases: what a project retunes (Lh, 2026-09-20) ----
19
+ Every size token below is multiplier × its family's base. A project sets a BASE
20
+ and never a multiplier, so the proportions of the system hold at any size.
21
+ The px in the comments are at these defaults (1rem = 16px). */
22
+ --sp-base: 0.25rem; /* 4 spacing: --sp-N = base × N */
23
+ --fs-base: 1rem; /* 16 type: --fs-* */
24
+ --r-base: 1rem; /* 16 radius: --r-* (--r-chip stays fully round) */
25
+ --tap-base: 3rem; /* 48 the size of THINGS: touch targets and control heights (--tap*, --btn-h, --chip-h, --bar-h,
26
+ --header-h, --fab), icons, avatars, dots, the ring and the tiles. --sp-base is the room BETWEEN things.
27
+ Text-holders follow the type instead: --badge and --icon-xs (inline with captions) are on --fs-base.
28
+ Not on a base: --screen-w (the column), borders / rules / focus (px, crisp), shadows, motion. */
29
+
30
+ /* ---- spacing (4pt grid): the number in the name is the multiplier ---- */
31
+ --sp-0: calc(var(--sp-base) * 0.5); /* 2 hairline gaps (title→meta) */
32
+ --sp-1: var(--sp-base); /* 4 */
33
+ --sp-2: calc(var(--sp-base) * 2); /* 8 */
34
+ --sp-3: calc(var(--sp-base) * 3); /* 12 */
35
+ --sp-4: calc(var(--sp-base) * 4); /* 16 */
36
+ --sp-5: calc(var(--sp-base) * 5); /* 20 */
37
+ --sp-6: calc(var(--sp-base) * 6); /* 24 */
38
+ --sp-8: calc(var(--sp-base) * 8); /* 32 */
39
+ --sp-10: calc(var(--sp-base) * 10); /* 40 */
40
+ --sp-12: calc(var(--sp-base) * 12); /* 48 */
29
41
 
30
42
  /* ---- touch & control sizes ---- */
31
- --tap: 3rem; /* 48 primary touch target */
32
- --tap-sm: 2.75rem; /* 44 minimum touch target (Apple HIG floor) */
33
- --tap-lg: 3.5rem; /* 56 hero controls: track steps, send, field with action */
34
- --btn-h: 3.25rem; /* 52 button height */
35
- --btn-xl-h: 5.5rem; /* 88 the one giant CTA */
36
- --chip-h: 2.375rem; /* 38 chip height */
37
- --bar-h: 4rem; /* 64 float bar, day cell, deck actions */
38
- --header-h: 3.75rem;/* 60 top bar — .topbar and .appbar alike */
43
+ --tap-floor: 2.75rem; /* 44 the HIG floor — no --tap-base takes a touch target under it */
44
+ --tap: max(var(--tap-floor), var(--tap-base)); /* 48 primary touch target */
45
+ --tap-sm: max(var(--tap-floor), calc(var(--tap-base) * 11 / 12)); /* 44 minimum touch target */
46
+ --tap-lg: max(var(--tap-floor), calc(var(--tap-base) * 7 / 6)); /* 56 hero controls: track steps, send, field with action */
47
+ --btn-h: calc(var(--tap-base) * 13 / 12); /* 52 button height */
48
+ --btn-xl-h: calc(var(--tap-base) * 11 / 6); /* 88 the one giant CTA */
49
+ --chip-h: calc(var(--tap-base) * 19 / 24); /* 38 chip height */
50
+ --bar-h: calc(var(--tap-base) * 4 / 3); /* 64 float bar, day cell, deck actions */
51
+ --header-h: calc(var(--tap-base) * 5 / 4); /* 60 top bar — .topbar and .appbar alike */
39
52
  --screen-pad: var(--sp-4); /* 16 the screen's inset — .sheet-stack cancels it */
40
- --map-ring: 10rem; /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
41
- --fab: 4.5rem; /* 72 square floating action */
42
- --badge: 1.625rem; /* 26 count badge */
43
- --dot: 0.625rem; /* 10 signal dot */
44
- --dot-xs: 0.375rem; /* 6 marker dot */
53
+ --map-ring: calc(var(--tap-base) * 10 / 3); /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
54
+ --fab: calc(var(--tap-base) * 3 / 2); /* 72 square floating action */
55
+ --badge: calc(var(--fs-base) * 1.625); /* 26 count badge */
56
+ --dot: calc(var(--tap-base) * 5 / 24); /* 10 signal dot */
57
+ --dot-xs: calc(var(--tap-base) / 8); /* 6 marker dot */
45
58
 
46
59
  /* ---- icons (icons.svg sprite, Lucide) ---- */
47
- --icon: 1.75rem; /* 28 default */
48
- --icon-sm: 1.75rem; /* one size everywhere; retune if small contexts appear */
49
- --icon-lg: 2rem; /* 32 empty states */
50
- --icon-xs: 1rem; /* 16 inline with captions/chips only (row meta, link arrows) */
60
+ --icon: calc(var(--tap-base) * 7 / 12); /* 28 default */
61
+ --icon-sm: calc(var(--tap-base) * 7 / 12); /* one size everywhere; retune if small contexts appear */
62
+ --icon-lg: calc(var(--tap-base) * 2 / 3); /* 32 empty states */
63
+ --icon-xs: var(--fs-base); /* 16 inline with captions/chips only (row meta, link arrows) */
51
64
 
52
65
  /* ---- people & big elements ---- */
53
- --avatar-sm: 2.5rem; /* 40 row lead */
54
- --avatar: 3.5rem; /* 56 card head, person row */
55
- --avatar-lg: 5rem; /* 80 chooser row */
56
- --avatar-xl: 6rem; /* 96 sheet head icon ring */
57
- --ring: 11rem; /* 176 .ring gauge diameter */
58
- --ring-w: 1.375rem; /* 22 .ring stroke */
59
- --tile-min: 7.5rem; /* 120 .tile-grid column floor */
60
- --tile-h: 8.25rem; /* 132 tile / band height */
66
+ --avatar-sm: calc(var(--tap-base) * 5 / 6); /* 40 row lead */
67
+ --avatar: calc(var(--tap-base) * 7 / 6); /* 56 card head, person row */
68
+ --avatar-lg: calc(var(--tap-base) * 5 / 3); /* 80 chooser row */
69
+ --avatar-xl: calc(var(--tap-base) * 2); /* 96 sheet head icon ring */
70
+ --ring: calc(var(--tap-base) * 11 / 3); /* 176 .ring gauge diameter */
71
+ --ring-w: calc(var(--tap-base) * 11 / 24); /* 22 .ring stroke */
72
+ --tile-min: calc(var(--tap-base) * 5 / 2); /* 120 .tile-grid column floor */
73
+ --tile-h: calc(var(--tap-base) * 11 / 4); /* 132 tile / band height */
61
74
 
62
75
  /* ---- radius ---- */
63
76
  --r-chip: 999px; /* pills, chips, buttons — always fully round */
64
- --r-card: 2rem; /* 32 the one card radius: every card, row, tile, menu, sheet, fab */
65
- --r-media: 1rem; /* 16 images inside cards */
66
- --r-control: 0.875rem; /* 14 fields, thumbs, menu rows */
67
- --r-badge: 0.625rem; /* 10 tile badge, bars */
77
+ --r-card: calc(var(--r-base) * 2); /* 32 the one card radius: every card, row, tile, menu, sheet, fab */
78
+ --r-media: var(--r-base); /* 16 images inside cards */
79
+ --r-control: calc(var(--r-base) * 0.875); /* 14 fields, thumbs, menu rows */
80
+ --r-badge: calc(var(--r-base) * 0.625); /* 10 tile badge, bars */
68
81
 
69
82
  /* ---- borders / rules / elevation (px on purpose: crisp at any scale) ---- */
70
83
  --border-w: 1.5px;
@@ -85,22 +98,22 @@
85
98
  --font-display: "FC Pride", var(--font-ui); /* the character face for headings and big numbers (fonts.css) */
86
99
  --font-mono: "SF Mono", "JetBrains Mono", ui-monospace, monospace;
87
100
 
88
- --fs-xs: 0.6875rem; /* 11 overline */
89
- --fs-caption: 0.75rem; /* 12 */
90
- --fs-label: 0.8125rem; /* 13 */
91
- --fs-sm: 0.875rem; /* 14 chips, segments */
92
- --fs-body: 0.9375rem; /* 15 */
93
- --fs-md: 1rem; /* 16 buttons */
94
- --fs-title: 1.0625rem; /* 17 */
95
- --fs-lg: 1.25rem; /* 20 day numbers, tile values */
96
- --fs-h2: 1.375rem; /* 22 */
97
- --fs-2xl: 1.5rem; /* 24 composer, small ring */
98
- --fs-h1: 1.75rem; /* 28 */
99
- --fs-3xl: 2rem; /* 32 xl button */
100
- --fs-display: clamp(2.125rem, 9vw, 2.625rem); /* 34–42 */
101
- --fs-stat: clamp(1.875rem, 8vw, 2.375rem); /* 30–38 big numbers */
102
- --fs-hero: 3.5rem; /* 56 the one number on a sheet */
103
- --fs-giant: clamp(3rem, 15vw, 4rem); /* 48–64 data bands */
101
+ --fs-xs: calc(var(--fs-base) * 0.6875); /* 11 overline */
102
+ --fs-caption: calc(var(--fs-base) * 0.75); /* 12 */
103
+ --fs-label: calc(var(--fs-base) * 0.8125); /* 13 */
104
+ --fs-sm: calc(var(--fs-base) * 0.875); /* 14 chips, segments */
105
+ --fs-body: calc(var(--fs-base) * 0.9375); /* 15 */
106
+ --fs-md: var(--fs-base); /* 16 buttons */
107
+ --fs-title: calc(var(--fs-base) * 1.0625); /* 17 */
108
+ --fs-lg: calc(var(--fs-base) * 1.25); /* 20 day numbers, tile values */
109
+ --fs-h2: calc(var(--fs-base) * 1.375); /* 22 */
110
+ --fs-2xl: calc(var(--fs-base) * 1.5); /* 24 composer, small ring */
111
+ --fs-h1: calc(var(--fs-base) * 1.75); /* 28 */
112
+ --fs-3xl: calc(var(--fs-base) * 2); /* 32 xl button */
113
+ --fs-display: clamp(calc(var(--fs-base) * 2.125), 9vw, calc(var(--fs-base) * 2.625)); /* 34–42 */
114
+ --fs-stat: clamp(calc(var(--fs-base) * 1.875), 8vw, calc(var(--fs-base) * 2.375)); /* 30–38 big numbers */
115
+ --fs-hero: calc(var(--fs-base) * 3.5); /* 56 the one number on a sheet */
116
+ --fs-giant: clamp(calc(var(--fs-base) * 3), 15vw, calc(var(--fs-base) * 4)); /* 48–64 data bands */
104
117
 
105
118
  /* level weight size / line-height family */
106
119
  --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) */
@@ -120,35 +133,53 @@
120
133
  /* ============================================================
121
134
  COLOR ROLES — wireframe defaults (grayscale).
122
135
  palette.css overrides these under [data-palette="…"].
136
+
137
+ THE FIVE COLOUR BASES (Lh, 2026-09-20) — a project's own colours, the
138
+ same idea as the size bases: set a base, the roles follow.
139
+ --color-surface the app's ground
140
+ --color-ink the words
141
+ --color-paper the plain card, neutral buttons and fields
142
+ --color-brand the primary action, the strong card, the signal dot
143
+ --color-on-brand the words on the brand
144
+ They have NO default: unset, every role below is its exact wireframe
145
+ value. That is what the fallbacks are for — a private mix (--_*) that
146
+ reads an unset base is invalid, so var(--_x, #grey) lands on the grey.
147
+ Set, the roles derive: tone 2 = the brand washed into the surface, the
148
+ muted inks = the ink washed into their ground. A role set directly
149
+ (a palette, or a project's theme.css) always wins over its derivation.
123
150
  ============================================================ */
151
+ --_muted-on-surface: color-mix(in srgb, var(--color-ink) 66%, var(--color-surface, #f2f2f0));
152
+ --_muted-on-paper: color-mix(in srgb, var(--color-ink) 66%, var(--color-paper, #ffffff));
153
+ --_tone-2: color-mix(in srgb, var(--color-brand) 14%, var(--color-surface, #f2f2f0));
154
+ --_muted-on-tone-2: color-mix(in srgb, var(--color-ink, #141414) 72%, var(--_tone-2));
124
155
 
125
156
  /* app background ("earth-toned surface or shiny color, up to the app") */
126
- --surface: #f2f2f0;
127
- --on-surface: #141414;
128
- --on-surface-muted: #6b6b6b;
157
+ --surface: var(--color-surface, #f2f2f0);
158
+ --on-surface: var(--color-ink, #141414);
159
+ --on-surface-muted: var(--_muted-on-surface, #6b6b6b);
129
160
 
130
161
  /* card boundaries — 3 slots so stacked/adjacent cards can differ */
131
- --card-1-bg: #ffffff; --card-1-ink: #141414;
132
- --card-2-bg: #e6e6e3; --card-2-ink: #141414;
133
- --card-3-bg: #141414; --card-3-ink: #ffffff;
162
+ --card-1-bg: var(--color-paper, #ffffff); --card-1-ink: var(--color-ink, #141414);
163
+ --card-2-bg: var(--_tone-2, #e6e6e3); --card-2-ink: var(--color-ink, #141414);
164
+ --card-3-bg: var(--color-brand, #141414); --card-3-ink: var(--color-on-brand, #ffffff);
134
165
 
135
166
  --card-border: #141414; /* wireframe: visible outline; palettes may set transparent */
136
- --card-muted: #6b6b6b; /* secondary text inside cards (the default, tone-1 card) */
137
- --card-2-muted: #5f5f5f; /* the same text on the tone-2 card — 5.1:1 on its grey (#6b6b6b is 4.3:1, under AA);
167
+ --card-muted: var(--_muted-on-paper, #6b6b6b); /* secondary text inside cards (the default, tone-1 card) */
168
+ --card-2-muted: var(--_muted-on-tone-2, #5f5f5f); /* the same text on the tone-2 card — 5.1:1 on its grey (#6b6b6b is 4.3:1, under AA);
138
169
  a palette whose tone-2 ground is strong
139
170
  sets this to its tone-2 ink — a muted shade cannot reach AA there (clay) */
140
171
 
141
172
  /* actions */
142
- --accent: #141414; /* primary CTA fill */
143
- --on-accent: #ffffff;
144
- --action-bg: #ffffff; /* neutral buttons/chips/inputs */
145
- --action-ink: #141414;
173
+ --accent: var(--color-brand, #141414); /* primary CTA fill */
174
+ --on-accent: var(--color-on-brand, #ffffff);
175
+ --action-bg: var(--color-paper, #ffffff); /* neutral buttons/chips/inputs */
176
+ --action-ink: var(--color-ink, #141414);
146
177
  --action-border: #141414;
147
178
 
148
179
  /* signals (chip dots, deltas) */
149
180
  --pos: #8a8a8a;
150
181
  --neg: #3d3d3d;
151
- --dot-a: #141414;
182
+ --dot-a: var(--color-brand, #141414);
152
183
  --dot-b: #8a8a8a;
153
184
 
154
185
  /* derived tints — read the element's own ink, so they work on any slot */
@@ -4,15 +4,13 @@ export interface ChipProps extends ComponentPropsWithoutRef<'span'> {
4
4
  on?: boolean;
5
5
  /** a leading signal dot: `a` (the accent signal) or `b` (the secondary) */
6
6
  dot?: 'a' | 'b';
7
- /** a speech tail under the chip (a mascot's greeting) */
8
- bubble?: boolean;
9
7
  /** the element: span (default), a link, or a button */
10
8
  as?: 'span' | 'a' | 'button';
11
9
  href?: string;
12
10
  children?: ReactNode;
13
11
  }
14
12
  /** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `on` when it is the live one. For a choice use ChipPick (`sign` for the +/× interest picker). */
15
- export declare function Chip({ on, dot, bubble, as, href, className, children, ...rest }: ChipProps): import("react").DetailedReactHTMLElement<{
13
+ export declare function Chip({ on, dot, as, href, className, children, ...rest }: ChipProps): import("react").DetailedReactHTMLElement<{
16
14
  slot?: string | undefined | undefined;
17
15
  style?: import("react").CSSProperties | undefined;
18
16
  title?: string | undefined | undefined;
@@ -3,9 +3,9 @@ import { createElement } from 'react';
3
3
  import { cx } from '../cx.js';
4
4
  import { Dot } from './Dot.js';
5
5
  /** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `on` when it is the live one. For a choice use ChipPick (`sign` for the +/× interest picker). */
6
- export function Chip({ on, dot, bubble, as, href, className, children, ...rest }) {
6
+ export function Chip({ on, dot, as, href, className, children, ...rest }) {
7
7
  const el = as ?? (href ? 'a' : 'span');
8
- return createElement(el, { href, type: el === 'button' ? 'button' : undefined, className: cx('chip', on && 'chip--on', bubble && 'chip--bubble', className), ...rest }, dot && _jsx(Dot, { b: dot === 'b' }), children);
8
+ return createElement(el, { href, type: el === 'button' ? 'button' : undefined, className: cx('chip', on && 'chip--on', className), ...rest }, dot && _jsx(Dot, { b: dot === 'b' }), children);
9
9
  }
10
10
  /** ChipPick — a chip that is a choice: full-width in a ChipGrid (9:00 · 9:30 · …), or `sign` on a ChipRow (+ off, × on — the
11
11
  * interest picker); the picked one fills with accent. A native radio/checkbox inside the label carries the state. */
package/dist/cardds.css CHANGED
@@ -56,56 +56,69 @@
56
56
  --screen-w: 25rem; /* 400: mobile column — .screen, .action-bar, .sheet-stage */
57
57
  --screen-centre: 40%; /* the human centre of a screen: the middle of its top four fifths — the bottom fifth is the thumb's (Lh, 2026-09-10). .card--centre sits here */
58
58
 
59
- /* ---- spacing (4pt grid) ---- */
60
- --sp-0: 0.125rem; /* 2 hairline gaps (title→meta) */
61
- --sp-1: 0.25rem; /* 4 */
62
- --sp-2: 0.5rem; /* 8 */
63
- --sp-3: 0.75rem; /* 12 */
64
- --sp-4: 1rem; /* 16 */
65
- --sp-5: 1.25rem; /* 20 */
66
- --sp-6: 1.5rem; /* 24 */
67
- --sp-8: 2rem; /* 32 */
68
- --sp-10: 2.5rem; /* 40 */
69
- --sp-12: 3rem; /* 48 */
59
+ /* ---- the four bases: what a project retunes (Lh, 2026-09-20) ----
60
+ Every size token below is multiplier × its family's base. A project sets a BASE
61
+ and never a multiplier, so the proportions of the system hold at any size.
62
+ The px in the comments are at these defaults (1rem = 16px). */
63
+ --sp-base: 0.25rem; /* 4 spacing: --sp-N = base × N */
64
+ --fs-base: 1rem; /* 16 type: --fs-* */
65
+ --r-base: 1rem; /* 16 radius: --r-* (--r-chip stays fully round) */
66
+ --tap-base: 3rem; /* 48 the size of THINGS: touch targets and control heights (--tap*, --btn-h, --chip-h, --bar-h,
67
+ --header-h, --fab), icons, avatars, dots, the ring and the tiles. --sp-base is the room BETWEEN things.
68
+ Text-holders follow the type instead: --badge and --icon-xs (inline with captions) are on --fs-base.
69
+ Not on a base: --screen-w (the column), borders / rules / focus (px, crisp), shadows, motion. */
70
+
71
+ /* ---- spacing (4pt grid): the number in the name is the multiplier ---- */
72
+ --sp-0: calc(var(--sp-base) * 0.5); /* 2 hairline gaps (title→meta) */
73
+ --sp-1: var(--sp-base); /* 4 */
74
+ --sp-2: calc(var(--sp-base) * 2); /* 8 */
75
+ --sp-3: calc(var(--sp-base) * 3); /* 12 */
76
+ --sp-4: calc(var(--sp-base) * 4); /* 16 */
77
+ --sp-5: calc(var(--sp-base) * 5); /* 20 */
78
+ --sp-6: calc(var(--sp-base) * 6); /* 24 */
79
+ --sp-8: calc(var(--sp-base) * 8); /* 32 */
80
+ --sp-10: calc(var(--sp-base) * 10); /* 40 */
81
+ --sp-12: calc(var(--sp-base) * 12); /* 48 */
70
82
 
71
83
  /* ---- touch & control sizes ---- */
72
- --tap: 3rem; /* 48 primary touch target */
73
- --tap-sm: 2.75rem; /* 44 minimum touch target (Apple HIG floor) */
74
- --tap-lg: 3.5rem; /* 56 hero controls: track steps, send, field with action */
75
- --btn-h: 3.25rem; /* 52 button height */
76
- --btn-xl-h: 5.5rem; /* 88 the one giant CTA */
77
- --chip-h: 2.375rem; /* 38 chip height */
78
- --bar-h: 4rem; /* 64 float bar, day cell, deck actions */
79
- --header-h: 3.75rem;/* 60 top bar — .topbar and .appbar alike */
84
+ --tap-floor: 2.75rem; /* 44 the HIG floor — no --tap-base takes a touch target under it */
85
+ --tap: max(var(--tap-floor), var(--tap-base)); /* 48 primary touch target */
86
+ --tap-sm: max(var(--tap-floor), calc(var(--tap-base) * 11 / 12)); /* 44 minimum touch target */
87
+ --tap-lg: max(var(--tap-floor), calc(var(--tap-base) * 7 / 6)); /* 56 hero controls: track steps, send, field with action */
88
+ --btn-h: calc(var(--tap-base) * 13 / 12); /* 52 button height */
89
+ --btn-xl-h: calc(var(--tap-base) * 11 / 6); /* 88 the one giant CTA */
90
+ --chip-h: calc(var(--tap-base) * 19 / 24); /* 38 chip height */
91
+ --bar-h: calc(var(--tap-base) * 4 / 3); /* 64 float bar, day cell, deck actions */
92
+ --header-h: calc(var(--tap-base) * 5 / 4); /* 60 top bar — .topbar and .appbar alike */
80
93
  --screen-pad: var(--sp-4); /* 16 the screen's inset — .sheet-stack cancels it */
81
- --map-ring: 10rem; /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
82
- --fab: 4.5rem; /* 72 square floating action */
83
- --badge: 1.625rem; /* 26 count badge */
84
- --dot: 0.625rem; /* 10 signal dot */
85
- --dot-xs: 0.375rem; /* 6 marker dot */
94
+ --map-ring: calc(var(--tap-base) * 10 / 3); /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
95
+ --fab: calc(var(--tap-base) * 3 / 2); /* 72 square floating action */
96
+ --badge: calc(var(--fs-base) * 1.625); /* 26 count badge */
97
+ --dot: calc(var(--tap-base) * 5 / 24); /* 10 signal dot */
98
+ --dot-xs: calc(var(--tap-base) / 8); /* 6 marker dot */
86
99
 
87
100
  /* ---- icons (icons.svg sprite, Lucide) ---- */
88
- --icon: 1.75rem; /* 28 default */
89
- --icon-sm: 1.75rem; /* one size everywhere; retune if small contexts appear */
90
- --icon-lg: 2rem; /* 32 empty states */
91
- --icon-xs: 1rem; /* 16 inline with captions/chips only (row meta, link arrows) */
101
+ --icon: calc(var(--tap-base) * 7 / 12); /* 28 default */
102
+ --icon-sm: calc(var(--tap-base) * 7 / 12); /* one size everywhere; retune if small contexts appear */
103
+ --icon-lg: calc(var(--tap-base) * 2 / 3); /* 32 empty states */
104
+ --icon-xs: var(--fs-base); /* 16 inline with captions/chips only (row meta, link arrows) */
92
105
 
93
106
  /* ---- people & big elements ---- */
94
- --avatar-sm: 2.5rem; /* 40 row lead */
95
- --avatar: 3.5rem; /* 56 card head, person row */
96
- --avatar-lg: 5rem; /* 80 chooser row */
97
- --avatar-xl: 6rem; /* 96 sheet head icon ring */
98
- --ring: 11rem; /* 176 .ring gauge diameter */
99
- --ring-w: 1.375rem; /* 22 .ring stroke */
100
- --tile-min: 7.5rem; /* 120 .tile-grid column floor */
101
- --tile-h: 8.25rem; /* 132 tile / band height */
107
+ --avatar-sm: calc(var(--tap-base) * 5 / 6); /* 40 row lead */
108
+ --avatar: calc(var(--tap-base) * 7 / 6); /* 56 card head, person row */
109
+ --avatar-lg: calc(var(--tap-base) * 5 / 3); /* 80 chooser row */
110
+ --avatar-xl: calc(var(--tap-base) * 2); /* 96 sheet head icon ring */
111
+ --ring: calc(var(--tap-base) * 11 / 3); /* 176 .ring gauge diameter */
112
+ --ring-w: calc(var(--tap-base) * 11 / 24); /* 22 .ring stroke */
113
+ --tile-min: calc(var(--tap-base) * 5 / 2); /* 120 .tile-grid column floor */
114
+ --tile-h: calc(var(--tap-base) * 11 / 4); /* 132 tile / band height */
102
115
 
103
116
  /* ---- radius ---- */
104
117
  --r-chip: 999px; /* pills, chips, buttons — always fully round */
105
- --r-card: 2rem; /* 32 the one card radius: every card, row, tile, menu, sheet, fab */
106
- --r-media: 1rem; /* 16 images inside cards */
107
- --r-control: 0.875rem; /* 14 fields, thumbs, menu rows */
108
- --r-badge: 0.625rem; /* 10 tile badge, bars */
118
+ --r-card: calc(var(--r-base) * 2); /* 32 the one card radius: every card, row, tile, menu, sheet, fab */
119
+ --r-media: var(--r-base); /* 16 images inside cards */
120
+ --r-control: calc(var(--r-base) * 0.875); /* 14 fields, thumbs, menu rows */
121
+ --r-badge: calc(var(--r-base) * 0.625); /* 10 tile badge, bars */
109
122
 
110
123
  /* ---- borders / rules / elevation (px on purpose: crisp at any scale) ---- */
111
124
  --border-w: 1.5px;
@@ -126,22 +139,22 @@
126
139
  --font-display: "FC Pride", var(--font-ui); /* the character face for headings and big numbers (fonts.css) */
127
140
  --font-mono: "SF Mono", "JetBrains Mono", ui-monospace, monospace;
128
141
 
129
- --fs-xs: 0.6875rem; /* 11 overline */
130
- --fs-caption: 0.75rem; /* 12 */
131
- --fs-label: 0.8125rem; /* 13 */
132
- --fs-sm: 0.875rem; /* 14 chips, segments */
133
- --fs-body: 0.9375rem; /* 15 */
134
- --fs-md: 1rem; /* 16 buttons */
135
- --fs-title: 1.0625rem; /* 17 */
136
- --fs-lg: 1.25rem; /* 20 day numbers, tile values */
137
- --fs-h2: 1.375rem; /* 22 */
138
- --fs-2xl: 1.5rem; /* 24 composer, small ring */
139
- --fs-h1: 1.75rem; /* 28 */
140
- --fs-3xl: 2rem; /* 32 xl button */
141
- --fs-display: clamp(2.125rem, 9vw, 2.625rem); /* 34–42 */
142
- --fs-stat: clamp(1.875rem, 8vw, 2.375rem); /* 30–38 big numbers */
143
- --fs-hero: 3.5rem; /* 56 the one number on a sheet */
144
- --fs-giant: clamp(3rem, 15vw, 4rem); /* 48–64 data bands */
142
+ --fs-xs: calc(var(--fs-base) * 0.6875); /* 11 overline */
143
+ --fs-caption: calc(var(--fs-base) * 0.75); /* 12 */
144
+ --fs-label: calc(var(--fs-base) * 0.8125); /* 13 */
145
+ --fs-sm: calc(var(--fs-base) * 0.875); /* 14 chips, segments */
146
+ --fs-body: calc(var(--fs-base) * 0.9375); /* 15 */
147
+ --fs-md: var(--fs-base); /* 16 buttons */
148
+ --fs-title: calc(var(--fs-base) * 1.0625); /* 17 */
149
+ --fs-lg: calc(var(--fs-base) * 1.25); /* 20 day numbers, tile values */
150
+ --fs-h2: calc(var(--fs-base) * 1.375); /* 22 */
151
+ --fs-2xl: calc(var(--fs-base) * 1.5); /* 24 composer, small ring */
152
+ --fs-h1: calc(var(--fs-base) * 1.75); /* 28 */
153
+ --fs-3xl: calc(var(--fs-base) * 2); /* 32 xl button */
154
+ --fs-display: clamp(calc(var(--fs-base) * 2.125), 9vw, calc(var(--fs-base) * 2.625)); /* 34–42 */
155
+ --fs-stat: clamp(calc(var(--fs-base) * 1.875), 8vw, calc(var(--fs-base) * 2.375)); /* 30–38 big numbers */
156
+ --fs-hero: calc(var(--fs-base) * 3.5); /* 56 the one number on a sheet */
157
+ --fs-giant: clamp(calc(var(--fs-base) * 3), 15vw, calc(var(--fs-base) * 4)); /* 48–64 data bands */
145
158
 
146
159
  /* level weight size / line-height family */
147
160
  --type-display: 800 var(--fs-display) / 1.15 var(--font-display); /* leading: Thai marks stack to 1.1em up and 0.3em down in FC Pride — 1.02 clipped them (2026-09-10) */
@@ -161,35 +174,53 @@
161
174
  /* ============================================================
162
175
  COLOR ROLES — wireframe defaults (grayscale).
163
176
  palette.css overrides these under [data-palette="…"].
177
+
178
+ THE FIVE COLOUR BASES (Lh, 2026-09-20) — a project's own colours, the
179
+ same idea as the size bases: set a base, the roles follow.
180
+ --color-surface the app's ground
181
+ --color-ink the words
182
+ --color-paper the plain card, neutral buttons and fields
183
+ --color-brand the primary action, the strong card, the signal dot
184
+ --color-on-brand the words on the brand
185
+ They have NO default: unset, every role below is its exact wireframe
186
+ value. That is what the fallbacks are for — a private mix (--_*) that
187
+ reads an unset base is invalid, so var(--_x, #grey) lands on the grey.
188
+ Set, the roles derive: tone 2 = the brand washed into the surface, the
189
+ muted inks = the ink washed into their ground. A role set directly
190
+ (a palette, or a project's theme.css) always wins over its derivation.
164
191
  ============================================================ */
192
+ --_muted-on-surface: color-mix(in srgb, var(--color-ink) 66%, var(--color-surface, #f2f2f0));
193
+ --_muted-on-paper: color-mix(in srgb, var(--color-ink) 66%, var(--color-paper, #ffffff));
194
+ --_tone-2: color-mix(in srgb, var(--color-brand) 14%, var(--color-surface, #f2f2f0));
195
+ --_muted-on-tone-2: color-mix(in srgb, var(--color-ink, #141414) 72%, var(--_tone-2));
165
196
 
166
197
  /* app background ("earth-toned surface or shiny color, up to the app") */
167
- --surface: #f2f2f0;
168
- --on-surface: #141414;
169
- --on-surface-muted: #6b6b6b;
198
+ --surface: var(--color-surface, #f2f2f0);
199
+ --on-surface: var(--color-ink, #141414);
200
+ --on-surface-muted: var(--_muted-on-surface, #6b6b6b);
170
201
 
171
202
  /* card boundaries — 3 slots so stacked/adjacent cards can differ */
172
- --card-1-bg: #ffffff; --card-1-ink: #141414;
173
- --card-2-bg: #e6e6e3; --card-2-ink: #141414;
174
- --card-3-bg: #141414; --card-3-ink: #ffffff;
203
+ --card-1-bg: var(--color-paper, #ffffff); --card-1-ink: var(--color-ink, #141414);
204
+ --card-2-bg: var(--_tone-2, #e6e6e3); --card-2-ink: var(--color-ink, #141414);
205
+ --card-3-bg: var(--color-brand, #141414); --card-3-ink: var(--color-on-brand, #ffffff);
175
206
 
176
207
  --card-border: #141414; /* wireframe: visible outline; palettes may set transparent */
177
- --card-muted: #6b6b6b; /* secondary text inside cards (the default, tone-1 card) */
178
- --card-2-muted: #5f5f5f; /* the same text on the tone-2 card — 5.1:1 on its grey (#6b6b6b is 4.3:1, under AA);
208
+ --card-muted: var(--_muted-on-paper, #6b6b6b); /* secondary text inside cards (the default, tone-1 card) */
209
+ --card-2-muted: var(--_muted-on-tone-2, #5f5f5f); /* the same text on the tone-2 card — 5.1:1 on its grey (#6b6b6b is 4.3:1, under AA);
179
210
  a palette whose tone-2 ground is strong
180
211
  sets this to its tone-2 ink — a muted shade cannot reach AA there (clay) */
181
212
 
182
213
  /* actions */
183
- --accent: #141414; /* primary CTA fill */
184
- --on-accent: #ffffff;
185
- --action-bg: #ffffff; /* neutral buttons/chips/inputs */
186
- --action-ink: #141414;
214
+ --accent: var(--color-brand, #141414); /* primary CTA fill */
215
+ --on-accent: var(--color-on-brand, #ffffff);
216
+ --action-bg: var(--color-paper, #ffffff); /* neutral buttons/chips/inputs */
217
+ --action-ink: var(--color-ink, #141414);
187
218
  --action-border: #141414;
188
219
 
189
220
  /* signals (chip dots, deltas) */
190
221
  --pos: #8a8a8a;
191
222
  --neg: #3d3d3d;
192
- --dot-a: #141414;
223
+ --dot-a: var(--color-brand, #141414);
193
224
  --dot-b: #8a8a8a;
194
225
 
195
226
  /* derived tints — read the element's own ink, so they work on any slot */
@@ -330,6 +361,11 @@
330
361
 
331
362
  /* Colored palettes trade wireframe outlines for soft elevation. */
332
363
  [data-palette] :is(.card, .band-stack) { box-shadow: var(--shadow-card); }
364
+ /* COLOURED MODE is the attribute, whatever its value: a project with colours of its own (the five
365
+ --color-* bases, or roles, in its theme.css) writes <html data-palette="own"> — no block here matches
366
+ "own", so only the three generic rules apply: the card's shadow, the soft outline, the field's soft
367
+ edge (forms.css); and the two wireframe borders step back, as in every stock palette. */
368
+ [data-palette] { --card-border: transparent; --action-border: transparent; }
333
369
  /* …but controls keep a faint edge so they still read as controls */
334
370
  [data-palette] { --outline: color-mix(in srgb, currentColor 25%, transparent); }
335
371
 
@@ -2547,6 +2583,56 @@ button { font: inherit; cursor: pointer; }
2547
2583
  .kv-cell__key { font: var(--type-caption); color: var(--card-muted); }
2548
2584
  .card--3 .kv-cell__key { color: inherit; opacity: 0.7; }
2549
2585
 
2586
+ /* ---- bubble: a chat message — theirs on the left, mine on the right (Lh 2026-09-18, replaces the chip's tail) ----
2587
+ No tail: a rounded block whose corner nearest the sender is square-ish
2588
+ (bottom-left for theirs, bottom-right for mine), the way messaging apps
2589
+ draw it. Under it a caption line (time · a tick), beside it small round
2590
+ actions (a heart, more). A .thread stacks them.
2591
+ <div class="thread">
2592
+ <div class="bubble"><p class="bubble__body">…</p><span class="bubble__actions">…icon-btns…</span><span class="bubble__meta">Dec 4 · 8:15</span></div>
2593
+ <div class="bubble bubble--right">…</div>
2594
+ </div> */
2595
+ .thread { display: grid; gap: var(--sp-4); }
2596
+ /* two columns: the message (max-content, but it gives way — minmax(0) — so a long one shrinks
2597
+ and wraps while the actions stay beside it) and the actions; the meta on a second row under the message */
2598
+ .bubble {
2599
+ display: grid;
2600
+ grid-template-columns: minmax(0, max-content) auto;
2601
+ justify-content: start;
2602
+ align-items: center;
2603
+ gap: var(--sp-1) var(--sp-3);
2604
+ max-width: 88%;
2605
+ margin-right: auto; /* theirs: hugs the left */
2606
+ }
2607
+ .bubble--right { grid-template-columns: auto minmax(0, max-content); justify-content: end; margin-right: 0; margin-left: auto; } /* mine: hugs the right, the actions on its left */
2608
+ .bubble__body, .bubble__meta { grid-column: 1; }
2609
+ .bubble__actions { grid-column: 2; grid-row: 1; }
2610
+ .bubble--right > :is(.bubble__body, .bubble__meta) { grid-column: 2; }
2611
+ .bubble--right > .bubble__actions { grid-column: 1; }
2612
+ .bubble__body {
2613
+ margin: 0;
2614
+ min-width: 0;
2615
+ padding: var(--sp-3) var(--sp-4);
2616
+ border-radius: var(--r-card);
2617
+ border-bottom-left-radius: var(--r-control);
2618
+ background: var(--card-2-bg);
2619
+ color: var(--card-2-ink);
2620
+ font: var(--type-body);
2621
+ overflow-wrap: anywhere;
2622
+ }
2623
+ .bubble--right > .bubble__body {
2624
+ border-bottom-left-radius: var(--r-card);
2625
+ border-bottom-right-radius: var(--r-control);
2626
+ background: var(--accent);
2627
+ color: var(--on-accent);
2628
+ text-align: right;
2629
+ }
2630
+ .bubble__actions { display: flex; gap: var(--sp-1); flex: none; opacity: 0.7; }
2631
+ .bubble__meta { display: flex; align-items: center; gap: var(--sp-1); font: var(--type-caption); color: var(--on-surface-muted); }
2632
+ .bubble--right > .bubble__meta { justify-self: end; }
2633
+ .bubble__meta > .icon { color: var(--accent); }
2634
+ .card--3 .bubble__meta { color: inherit; opacity: 0.7; }
2635
+
2550
2636
  /* ---- timeline: a hairline with date chips, entries to the right ----
2551
2637
  <ol class="timeline"><li class="timeline__item"><span class="timeline__mark">Wed 14</span><p class="t-body">…</p></li>…</ol> */
2552
2638
  .timeline {
@@ -3119,22 +3205,6 @@ button { font: inherit; cursor: pointer; }
3119
3205
  }
3120
3206
  .callout__title { font: var(--type-title); }
3121
3207
  .callout__sub { font: var(--type-caption); opacity: 0.7; }
3122
- /* speech chip: a chip with the same tail (mascot / greeting) */
3123
- .chip--bubble { position: relative; }
3124
- .chip--bubble::after {
3125
- content: "";
3126
- position: absolute;
3127
- left: var(--sp-4);
3128
- bottom: calc(-1 * var(--sp-2) + var(--border-w));
3129
- width: var(--sp-3);
3130
- height: var(--sp-3);
3131
- rotate: 45deg;
3132
- background: inherit;
3133
- border: inherit;
3134
- border-top: 0;
3135
- border-left: 0;
3136
- }
3137
-
3138
3208
  /* ---- deck: swipe cards; the last child is on top ----
3139
3209
  <div class="deck"><article class="card">…</article><article class="card">…</article><article class="card card--fold">front</article></div>
3140
3210
  <div class="deck__actions"><button class="icon-btn">×</button><button class="icon-btn icon-btn--invert">✓</button></div> */
package/dist/index.d.ts CHANGED
@@ -59,6 +59,7 @@ export * from './lists/Row.js';
59
59
  export * from './lists/Kv.js';
60
60
  export * from './lists/Timeline.js';
61
61
  export * from './lists/Legend.js';
62
+ export * from './lists/Bubble.js';
62
63
  export * from './media/Quote.js';
63
64
  export * from './media/Postcard.js';
64
65
  export * from './media/MapArea.js';
package/dist/index.js CHANGED
@@ -62,6 +62,7 @@ export * from './lists/Row.js';
62
62
  export * from './lists/Kv.js';
63
63
  export * from './lists/Timeline.js';
64
64
  export * from './lists/Legend.js';
65
+ export * from './lists/Bubble.js';
65
66
  export * from './media/Quote.js';
66
67
  export * from './media/Postcard.js';
67
68
  export * from './media/MapArea.js';
@@ -0,0 +1,18 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
+ export interface ThreadProps extends ComponentPropsWithoutRef<'div'> {
3
+ }
4
+ /** Thread — a stack of Bubbles: a conversation. */
5
+ export declare function Thread({ className, ...rest }: ThreadProps): import("react").JSX.Element;
6
+ export interface BubbleProps extends ComponentPropsWithoutRef<'div'> {
7
+ /** whose: theirs on the left (default), mine on the right */
8
+ side?: 'left' | 'right';
9
+ /** the caption under the message — a time, a tick (an Icon) */
10
+ meta?: ReactNode;
11
+ /** small round buttons beside the message (a heart, more) */
12
+ actions?: ReactNode;
13
+ }
14
+ /**
15
+ * Bubble — a chat message: a rounded block, its corner nearest the sender squared (bottom-left for theirs, bottom-right
16
+ * for mine), no tail. `meta` sits under it on the sender's side, `actions` beside it. Stack them in a Thread.
17
+ */
18
+ export declare function Bubble({ side, meta, actions, className, children, ...rest }: BubbleProps): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../cx.js';
3
+ /** Thread — a stack of Bubbles: a conversation. */
4
+ export function Thread({ className, ...rest }) {
5
+ return _jsx("div", { className: cx('thread', className), ...rest });
6
+ }
7
+ /**
8
+ * Bubble — a chat message: a rounded block, its corner nearest the sender squared (bottom-left for theirs, bottom-right
9
+ * for mine), no tail. `meta` sits under it on the sender's side, `actions` beside it. Stack them in a Thread.
10
+ */
11
+ export function Bubble({ side = 'left', meta, actions, className, children, ...rest }) {
12
+ return (_jsxs("div", { className: cx('bubble', side === 'right' && 'bubble--right', className), ...rest, children: [_jsx("p", { className: "bubble__body", children: children }), actions != null && _jsx("span", { className: "bubble__actions", children: actions }), meta != null && _jsx("span", { className: "bubble__meta", children: meta })] }));
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
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.",