dowel-ui 0.20.1 → 0.20.3

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
@@ -11,7 +11,7 @@ A dowel is the hidden peg that joins two boards so the seam does not show. That
11
11
  **[Documentation](https://lacodda.github.io/dowel/)** — what everything is and why it is that way.
12
12
  **[The stand](https://lacodda.github.io/dowel/stand/)** — every component, live, in either theme and in the accent of any product of the line.
13
13
 
14
- **Status:** v0.20.1 - the theme, the scales, an accent per product, forty-five components - overlays, menus, the command palette and the three ways of saying something happened - and the gates each one passes: axe, the keyboard, a dependency budget and a picture in both themes. Every component has a page of its own on the stand, which remembers the theme and the accent you left it in. Components install from a versioned registry, the docs are served in the form an agent reads, and `dowel check` tells a project what stands between it and the vocabulary. Two products of the line live on it. See the [roadmap](#roadmap).
14
+ **Status:** v0.20.3 - the theme, the scales, an accent per product, forty-five components - overlays, menus, the command palette and the three ways of saying something happened - and the gates each one passes: axe, the keyboard, a dependency budget and a picture in both themes. Every component has a page of its own on the stand, which remembers the theme and the accent you left it in. Components install from a versioned registry, the docs are served in the form an agent reads, and `dowel check` tells a project what stands between it and the vocabulary. Two products of the line live on it. See the [roadmap](#roadmap).
15
15
 
16
16
  ## The theme
17
17
 
@@ -32,7 +32,7 @@ npm install dowel-ui
32
32
  }
33
33
  ```
34
34
 
35
- That single line moves the accent and both of its partners, the accent's soft fill, the focus ring, and the tint in the greys. Set nothing at all and you get dowel's own amber.
35
+ That single line moves the accent and both of its partners, the accent's soft fill, the focus ring, the scrollbars, and the tint in the greys. Set nothing at all and you get dowel's own amber.
36
36
 
37
37
  Dark is the default; light arrives with the reader's system preference, or pinned with a class:
38
38
 
@@ -0,0 +1,10 @@
1
+ /*
2
+ * furca - terracotta (paired with olive) #D9704A, the colour of its mark (fr).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #D9704A;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * hilvan - crimson #D64550, the colour of its mark (hv).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #D64550;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * rhapsod - field green #6CB14E, the colour of its mark (rh).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #6CB14E;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * rigger - iris (paired with lagoon) #8A62F0, the colour of its mark (rr).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #8A62F0;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * scheda - terracotta (paired with sea) #D9704A, the colour of its mark (sc).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #D9704A;
10
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"line.d.ts","sourceRoot":"","sources":["../src/line.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,6CAA6C;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,eAAO,MAAM,YAAY,EAAE,SAAS,WAAW,EAe9C,CAAA;AAED,kDAAkD;AAClD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAEjE"}
1
+ {"version":3,"file":"line.d.ts","sourceRoot":"","sources":["../src/line.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,6CAA6C;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,eAAO,MAAM,YAAY,EAAE,SAAS,WAAW,EA+B9C,CAAA;AAED,kDAAkD;AAClD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAEjE"}
package/dist/line.js CHANGED
@@ -27,6 +27,22 @@ export const lineProducts = [
27
27
  { name: 'dowel', code: 'dw', accent: '#E8862D', colorName: 'signal amber' },
28
28
  { name: 'lyrn', code: 'ln', accent: '#6D7BF2', colorName: 'indigo' },
29
29
  { name: 'austeris', code: 'au', accent: '#C25BD9', colorName: 'orchid' },
30
+ { name: 'rhapsod', code: 'rh', accent: '#6CB14E', colorName: 'field green' },
31
+ { name: 'hilvan', code: 'hv', accent: '#D64550', colorName: 'crimson' },
32
+ /*
33
+ * The last three marks are two-colour: the registry's palette of single hues
34
+ * ran out at hilvan, and a product now owns either one hue or a pair drawn as
35
+ * one diagonal gradient.
36
+ *
37
+ * The theme takes the first colour of the pair and nothing else. A gradient
38
+ * is a property of the mark — the tile, the code, the banner rule — and an
39
+ * accent is a flat colour a button is filled with, a focus ring is drawn in
40
+ * and a link is set in. Carrying the pair into the vocabulary would make
41
+ * every one of those a gradient, which is not what the pair is for.
42
+ */
43
+ { name: 'scheda', code: 'sc', accent: '#D9704A', colorName: 'terracotta (paired with sea)' },
44
+ { name: 'furca', code: 'fr', accent: '#D9704A', colorName: 'terracotta (paired with olive)' },
45
+ { name: 'rigger', code: 'rr', accent: '#8A62F0', colorName: 'iris (paired with lagoon)' },
30
46
  ];
31
47
  /** The product with this name, or `undefined`. */
32
48
  export function lineProduct(name) {
package/dist/line.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"line.js","sourceRoot":"","sources":["../src/line.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAaH,MAAM,CAAC,MAAM,YAAY,GAA2B;IAClD,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IAClE,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IACzE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IAClE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACrE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IACnE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACrE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE;IACtE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE;IACtE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE;IACpE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACrE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE;IAC3E,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACpE,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;CACzE,CAAA;AAED,kDAAkD;AAClD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;AAC9D,CAAC"}
1
+ {"version":3,"file":"line.js","sourceRoot":"","sources":["../src/line.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAaH,MAAM,CAAC,MAAM,YAAY,GAA2B;IAClD,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IAClE,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IACzE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IAClE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACrE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IACnE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACrE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE;IACtE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE;IACtE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE;IACpE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACrE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE;IAC3E,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACpE,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACxE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE;IAC5E,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE;IACvE;;;;;;;;;;OAUG;IACH,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,8BAA8B,EAAE;IAC5F,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,gCAAgC,EAAE;IAC7F,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,2BAA2B,EAAE;CAC1F,CAAA;AAED,kDAAkD;AAClD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;AAC9D,CAAC"}
@@ -14,7 +14,7 @@
14
14
  "path": "dowel/theme.css",
15
15
  "target": "~/dowel/theme.css",
16
16
  "type": "registry:file",
17
- "content": "/*\n * dowel theme - the token vocabulary every product of the lacodda line shares.\n *\n * The vocabulary comes from the products themselves: kilna and kasl-server\n * already ship the same token names (bg / raise / soft / line / text / dim /\n * accent / good / warn / bad / info) and differ only in values. That is the\n * contract this file freezes. Names are the mockup's own words rather than\n * stock component-library names, so a screen can be checked against a mockup\n * in the mockup's words.\n *\n * Two things are parametric, and everything else is derived from them:\n *\n * --accent-base the product's hue from the brand-line registry\n * --neutral-base the hue the greys are tinted with (the accent, by default)\n *\n * Tinting the neutrals is not decoration - it is what the two live products do\n * by hand: kilna's greys lean magenta, kasl-server's lean gold. Here that lean\n * is one declaration instead of thirty hand-picked hex values.\n *\n * Soft variants are mixed from their own base with `color-mix`, so a product\n * that overrides `--accent-base` gets a matching `--accent-soft` for free and\n * cannot pick one that disagrees with it.\n *\n * Theme selection: no class on the root element follows the operating system,\n * an explicit `light` or `dark` class pins the theme. Components never use\n * `dark:` utilities - every colour goes through a token, and the theme swaps\n * the token underneath.\n */\n\n:root {\n /* The two parameters. `--accent-base` is overridden per product by an accent\n * file; `--neutral-base` follows it unless a product says otherwise. */\n --accent-base: #e8862d;\n --neutral-base: var(--accent-base);\n\n /* Ink and ground of the dark theme, before the neutral tint is mixed in.\n * Kept as their own tokens so the tint amount is the only thing that\n * changes when a product wants greyer or warmer chrome. */\n --ground: #131316;\n --ink: #ece9ef;\n\n /* How much of `--neutral-base` bleeds into the greys. The live products sit\n * at roughly this much: enough that the chrome belongs to the product,\n * little enough that it still reads as grey. */\n --neutral-tint: 6%;\n --neutral-tint-strong: 9%;\n\n color-scheme: dark;\n\n --bg: color-mix(in oklab, var(--neutral-base) var(--neutral-tint), var(--ground));\n --raise: color-mix(in oklab, var(--neutral-base) var(--neutral-tint), #1c1c21);\n\n /* Surfaces that lift by translucency rather than by their own colour: they\n * must work over `--bg` and over `--raise` alike. */\n --soft: rgb(255 255 255 / 0.045);\n --softer: rgb(255 255 255 / 0.025);\n --line: rgb(255 255 255 / 0.08);\n --line-2: rgb(255 255 255 / 0.15);\n\n --text: color-mix(in oklab, var(--neutral-base) var(--neutral-tint), var(--ink));\n --dim: color-mix(in oklab, var(--neutral-base) var(--neutral-tint-strong), #a7a2ad);\n --faint: color-mix(in oklab, var(--neutral-base) var(--neutral-tint-strong), #6e6a76);\n\n --accent: var(--accent-base);\n /* The hover/active partner: lighter on dark, where the ground is what the\n * accent has to separate from. */\n --accent-2: color-mix(in oklab, white 22%, var(--accent-base));\n --accent-soft: color-mix(in oklab, var(--accent-base) 16%, transparent);\n\n /* Status hues are the line's own and do not follow the product accent: a\n * green that shifted per product would stop meaning \"good\". Meaning never\n * rests on colour alone - a badge carries an icon and a word - so these\n * exist for emphasis, not as the message. */\n --good: #45d18f;\n --warn: #e8b13f;\n --bad: #ef6a6a;\n --info: #4cc4e0;\n --good-soft: color-mix(in oklab, var(--good) 14%, transparent);\n --warn-soft: color-mix(in oklab, var(--warn) 14%, transparent);\n --bad-soft: color-mix(in oklab, var(--bad) 14%, transparent);\n --info-soft: color-mix(in oklab, var(--info) 15%, transparent);\n\n /* Elevation, three steps. The products had one shadow and used it for\n * everything that leaves the flow - a toast, a dropdown and a modal all\n * floated by the same amount, so a modal never felt further away than the\n * menu it covered. `raise` keeps its original value, so nothing shifts under\n * the products already using it; the other two are the steps either side. */\n --shadow-lift: 0 2px 8px rgb(0 0 0 / 0.3);\n --shadow-raise: 0 10px 34px rgb(0 0 0 / 0.45);\n --shadow-float: 0 24px 60px rgb(0 0 0 / 0.55);\n}\n\n/*\n * Light theme, twice: once for the operating system's preference, once for the\n * explicit `light` class. The declarations are identical - only the selector\n * differs - so that a product can pin a theme against the system setting.\n */\n@media (prefers-color-scheme: light) {\n :root:not(.dark) {\n color-scheme: light;\n\n --ground: #f6f5f7;\n --ink: #232027;\n\n --bg: color-mix(in oklab, var(--neutral-base) var(--neutral-tint), var(--ground));\n --raise: #ffffff;\n\n /* Tinted, and translucent - which took two goes to get right.\n *\n * `color-mix` mixes the alpha along with the colour, so mixing 60% of an\n * opaque accent into a 5%-opaque grey gives a surface 62% opaque: twelve\n * times denser than the hairline it was meant to be. It went unnoticed for\n * ten versions because `bg-soft` was only ever used for a hover, where a\n * flash of colour reads as feedback rather than as a mistake. The first\n * component to sit on it permanently - Alert - made it obvious.\n *\n * `oklch(from … / alpha)` keeps the alpha out of the mix: the hue comes\n * from the tinted colour, the transparency is stated. */\n --soft: oklch(from color-mix(in oklab, var(--neutral-base) 45%, #181420) l c h / 0.05);\n --softer: oklch(from color-mix(in oklab, var(--neutral-base) 45%, #181420) l c h / 0.03);\n --line: oklch(from color-mix(in oklab, var(--neutral-base) 30%, #181420) l c h / 0.11);\n --line-2: oklch(from color-mix(in oklab, var(--neutral-base) 30%, #181420) l c h / 0.2);\n\n --text: color-mix(in oklab, var(--neutral-base) var(--neutral-tint), var(--ink));\n --dim: color-mix(in oklab, var(--neutral-base) var(--neutral-tint-strong), #63606b);\n --faint: color-mix(in oklab, var(--neutral-base) var(--neutral-tint-strong), #8a8692);\n\n /* On a light ground the accent has to darken to stay legible as text and\n * as a fill. It darkens *to* a lightness rather than *by* an amount: how\n * far a hue has to travel depends on where it starts, and a fixed step\n * that suits magenta leaves lime and gold short. Pinning the lightness and\n * keeping the hue and chroma clears 5:1 for every accent in the line. */\n --accent: oklch(from var(--accent-base) 0.5 c h);\n --accent-2: oklch(from var(--accent-base) 0.4 c h);\n --accent-soft: color-mix(in oklab, var(--accent-base) 12%, transparent);\n\n --good: #0c8554;\n --warn: #9a6b0c;\n --bad: #c93b3b;\n --info: #0d7f9c;\n --good-soft: color-mix(in oklab, var(--good) 12%, transparent);\n --warn-soft: color-mix(in oklab, var(--warn) 14%, transparent);\n --bad-soft: color-mix(in oklab, var(--bad) 12%, transparent);\n --info-soft: color-mix(in oklab, var(--info) 11%, transparent);\n\n --shadow-lift: 0 2px 8px rgb(30 24 38 / 0.08);\n --shadow-raise: 0 10px 30px rgb(30 24 38 / 0.14);\n --shadow-float: 0 24px 60px rgb(30 24 38 / 0.18);\n }\n}\n\n:root.light {\n color-scheme: light;\n\n --ground: #f6f5f7;\n --ink: #232027;\n\n --bg: color-mix(in oklab, var(--neutral-base) var(--neutral-tint), var(--ground));\n --raise: #ffffff;\n\n /* The same surfaces as above; see the note there. */\n --soft: oklch(from color-mix(in oklab, var(--neutral-base) 45%, #181420) l c h / 0.05);\n --softer: oklch(from color-mix(in oklab, var(--neutral-base) 45%, #181420) l c h / 0.03);\n --line: oklch(from color-mix(in oklab, var(--neutral-base) 30%, #181420) l c h / 0.11);\n --line-2: oklch(from color-mix(in oklab, var(--neutral-base) 30%, #181420) l c h / 0.2);\n\n --text: color-mix(in oklab, var(--neutral-base) var(--neutral-tint), var(--ink));\n --dim: color-mix(in oklab, var(--neutral-base) var(--neutral-tint-strong), #63606b);\n --faint: color-mix(in oklab, var(--neutral-base) var(--neutral-tint-strong), #8a8692);\n\n /* Darkened to a lightness, not by an amount - see the note above. */\n --accent: oklch(from var(--accent-base) 0.5 c h);\n --accent-2: oklch(from var(--accent-base) 0.4 c h);\n --accent-soft: color-mix(in oklab, var(--accent-base) 12%, transparent);\n\n --good: #0c8554;\n --warn: #9a6b0c;\n --bad: #c93b3b;\n --info: #0d7f9c;\n --good-soft: color-mix(in oklab, var(--good) 12%, transparent);\n --warn-soft: color-mix(in oklab, var(--warn) 14%, transparent);\n --bad-soft: color-mix(in oklab, var(--bad) 12%, transparent);\n --info-soft: color-mix(in oklab, var(--info) 11%, transparent);\n\n --shadow-lift: 0 2px 8px rgb(30 24 38 / 0.08);\n --shadow-raise: 0 10px 30px rgb(30 24 38 / 0.14);\n --shadow-float: 0 24px 60px rgb(30 24 38 / 0.18);\n}\n\n/*\n * `--on-accent` - what sits on top of an accent fill.\n *\n * A light accent (gold, lime, amber) needs dark glyphs; a dark one needs\n * white. The live products picked this by hand and wrote the answer into the\n * theme; here the theme works it out, by the same rule the brand-line S tile\n * uses.\n *\n * `contrast-color()` is the direct way to say it and is used where supported.\n * The fallback covers browsers that lack it. Relative colour syntax exposes\n * the accent's own lightness as `l`; `clamp()` turns that into a hard switch,\n * because the multiplication drives the middle term far past either bound\n * everywhere except within a hair of the threshold. Below it the accent is\n * dark and the result is 1 (white); above it, 0 (black). Chroma is dropped to\n * zero, so what comes out is neutral rather than a tinted grey.\n *\n * The threshold is 0.58, and it is deliberately far below the midpoint an eye\n * would guess. Contrast is not symmetric about it: a mid-lightness colour is\n * still much closer to white than to black in luminance, so black wins well\n * before the colour looks light. Checked against all fourteen accents of the\n * line - every one of them reads better with dark glyphs, the closest being\n * cobalt at 4.86:1 against 4.32:1 for white. A higher threshold is what puts\n * white text on magenta at 3.6:1, which is the defect this rule exists to\n * prevent.\n */\n:root {\n --on-accent: oklch(from var(--accent) clamp(0, (0.58 - l) * 1000, 1) 0 0);\n\n /*\n * The same question for the status fills, and it has to be asked separately:\n * `--on-accent` is derived from the accent, so using it on a `--warn` fill\n * is only ever right by coincidence. The line's first consumer did exactly\n * that - a count on a yellow badge, drawn in white at 1.95:1 - and it read\n * as correct for as long as the product happened to pin white.\n *\n * The status hues do not follow the product accent, so these four are the\n * same for every product; they are still derived rather than written down,\n * because the status colours themselves change between the themes.\n */\n --on-good: oklch(from var(--good) clamp(0, (0.58 - l) * 1000, 1) 0 0);\n --on-warn: oklch(from var(--warn) clamp(0, (0.58 - l) * 1000, 1) 0 0);\n --on-bad: oklch(from var(--bad) clamp(0, (0.58 - l) * 1000, 1) 0 0);\n --on-info: oklch(from var(--info) clamp(0, (0.58 - l) * 1000, 1) 0 0);\n}\n\n@supports (color: contrast-color(red)) {\n :root {\n --on-accent: contrast-color(var(--accent));\n --on-good: contrast-color(var(--good));\n --on-warn: contrast-color(var(--warn));\n --on-bad: contrast-color(var(--bad));\n --on-info: contrast-color(var(--info));\n }\n}\n\n/*\n * The Tailwind 4 surface. `--color-*: initial` drops the stock palette on\n * purpose: a raw `bg-zinc-800` in a product should not compile, because the\n * only colours that exist here are the line's own.\n */\n@theme inline {\n --color-*: initial;\n --color-bg: var(--bg);\n --color-raise: var(--raise);\n --color-soft: var(--soft);\n --color-softer: var(--softer);\n --color-line: var(--line);\n --color-line-2: var(--line-2);\n --color-text: var(--text);\n --color-dim: var(--dim);\n --color-faint: var(--faint);\n --color-accent: var(--accent);\n --color-accent-2: var(--accent-2);\n --color-accent-soft: var(--accent-soft);\n --color-on-accent: var(--on-accent);\n --color-on-good: var(--on-good);\n --color-on-warn: var(--on-warn);\n --color-on-bad: var(--on-bad);\n --color-on-info: var(--on-info);\n --color-good: var(--good);\n --color-good-soft: var(--good-soft);\n --color-warn: var(--warn);\n --color-warn-soft: var(--warn-soft);\n --color-bad: var(--bad);\n --color-bad-soft: var(--bad-soft);\n --color-info: var(--info);\n --color-info-soft: var(--info-soft);\n\n /* Kept because they are not palette choices: a hairline is `transparent`,\n * an SVG follows `currentColor`, and pure black and white are what an\n * overlay scrim and a print sheet are made of. */\n --color-transparent: transparent;\n --color-current: currentColor;\n --color-white: #fff;\n --color-black: #000;\n\n /*\n * Type. System stacks on purpose: a downloaded face costs a network round\n * trip before the first word appears, and the line's products are desktop\n * tools where the operating system's own face is the one the user already\n * reads everything else in.\n */\n --font-sans: 'Segoe UI Variable Text', 'Segoe UI', system-ui, -apple-system, sans-serif;\n --font-mono: ui-monospace, 'Cascadia Code', 'SF Mono', Consolas, monospace;\n\n /*\n * Radius. Taken from what the products actually draw, not from a ratio:\n * `rounded-[9px]` appears twenty times and `rounded-[10px]` twelve, because\n * a control and the primary button were tuned by eye and then copied. The\n * scale keeps the cluster they landed in and gives it names.\n *\n * `md` is the control radius - inputs, buttons, list rows. That the primary\n * button was one pixel rounder than every other variant is not preserved:\n * the products differ from themselves there, and buttons of the same size\n * sitting side by side should not have mismatched corners.\n */\n --radius-xs: 4px;\n --radius-sm: 6px;\n --radius-md: 9px;\n --radius-lg: 12px;\n --radius-xl: 16px;\n --radius-2xl: 20px;\n\n /*\n * A radius nested inside another has to be smaller by the gap between them,\n * or the inner corner looks wrong against the outer one. The products did\n * this by hand once - 18px outside, 17px inside - and nowhere else.\n */\n --radius-inner: calc(var(--radius-lg) - 1px);\n\n /*\n * Type scale. The products live between 10px and 14px: `text-sm` and\n * `text-xs` together account for nine tenths of every size in both, and the\n * rest scattered across 9, 9.5, 10, 10.5, 11, 11.5, 12.5 and 13 - nine steps\n * inside four pixels, which no eye distinguishes and no reason justifies.\n * This is the same range with the noise removed.\n */\n --text-2xs: 10px;\n --text-2xs--line-height: 14px;\n --text-xs: 11px;\n --text-xs--line-height: 15px;\n --text-sm: 12px;\n --text-sm--line-height: 16px;\n --text-base: 14px;\n --text-base--line-height: 20px;\n --text-lg: 16px;\n --text-lg--line-height: 22px;\n --text-xl: 18px;\n --text-xl--line-height: 24px;\n --text-2xl: 21px;\n --text-2xl--line-height: 28px;\n\n /* Weights. `semibold` is what both products use for anything emphasised;\n * `bold` appears in neither, and the one `font-[650]` in a page title is the\n * kind of value a scale exists to absorb. */\n --font-weight-normal: 400;\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n\n /* Tracking. The uppercase caption is the only place the products track at\n * all - and they do it at 0.08em in six files and 0.09em in three, a\n * difference nobody can see. One name settles it. */\n --tracking-caption: 0.085em;\n --tracking-tight: -0.01em;\n\n /* Easing. `out` for anything the user asked for - it arrives fast and\n * settles, which reads as responsive. `in-out` for something moving on its\n * own. `in` is deliberately absent: it starts slowly, which on a control\n * reads as lag. */\n --ease-out: cubic-bezier(0.2, 0, 0, 1);\n --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\n\n /*\n * Elevation. Three steps, because the products had one and used it for a\n * toast, a dropdown and a modal alike - so a modal never sat further from\n * the page than the menu it covered.\n */\n --shadow-lift: var(--shadow-lift);\n --shadow-raise: var(--shadow-raise);\n --shadow-float: var(--shadow-float);\n}\n\n/*\n * Stacking order.\n *\n * Not in `@theme`: Tailwind has no z-index namespace, so `z-50` is a literal\n * fifty and a named step would not compile. These are custom properties a\n * component reads directly - `z-index: var(--z-modal)`.\n *\n * The order is the products' own, with the gaps closed. They ran 10 for an\n * in-flow popup, 20 sticky, 30 menu, 40 for a floating button, 50 for modals\n * and drawers, then jumped to 70 and 80 for the command palette - which had to\n * clear the modal layer and had no name to do it with.\n */\n:root {\n /*\n * Motion. One duration existed before this - 160ms on a route change - and\n * everything else rode Tailwind's default. These are the steps around it:\n * `quick` for a colour or an opacity that should feel immediate, `base` for\n * something that moves, `slow` for something arriving from off-screen.\n *\n * Not in `@theme`: Tailwind's `duration-*` utility takes a literal number,\n * not a named step, so these are read directly - `transition-duration:\n * var(--duration-base)`. The easing curves opposite them ARE a namespace,\n * so `ease-out` is a class.\n *\n * Every duration here is for people who want motion: `prefers-reduced-\n * motion` cuts them to nothing further down.\n */\n --duration-quick: 120ms;\n --duration-base: 160ms;\n --duration-slow: 240ms;\n\n --z-popup: 10;\n --z-sticky: 20;\n --z-menu: 30;\n --z-floating: 40;\n --z-overlay: 50;\n --z-modal: 60;\n --z-palette: 70;\n --z-toast: 80;\n}\n\n/*\n * Base layer: what every product would otherwise write again. Scoped to\n * elements and to `:focus-visible`, never to a class, so nothing here can\n * collide with a component.\n */\nbody {\n margin: 0;\n background-color: var(--bg);\n color: var(--text);\n font-family: var(--font-sans);\n -webkit-font-smoothing: antialiased;\n}\n\n/*\n * In `@layer base`, so a component can turn it off.\n *\n * Unlayered, this rule has the same specificity as `focus-visible:outline-none`\n * from Tailwind - both are one pseudo-class - and wins on source order alone,\n * because the theme is imported before the utilities. Every component that\n * draws its own focus ring got this one on top of it: the command palette's\n * field had an accent outline it had explicitly opted out of, and a combobox\n * with chips drew two rings, one around the box and one around the input\n * inside it.\n *\n * A layered rule loses to any unlayered one regardless of specificity, which\n * is the whole point of cascade layers - the base layer states a default and\n * a component overrides it by saying so.\n */\n@layer base {\n :focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n transition-duration: 0.01ms !important;\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n scroll-behavior: auto !important;\n }\n}\n\n/*\n * Scrollbars. A browser's default bar is a piece of someone else's chrome\n * sitting in the middle of the product - full width, with step arrows. These\n * are the line's own: thin, in the palette, drawn only where something\n * actually scrolls.\n */\n* {\n scrollbar-width: thin;\n scrollbar-color: var(--line-2) transparent;\n}\n\n*::-webkit-scrollbar {\n width: 10px;\n height: 10px;\n}\n\n*::-webkit-scrollbar-track {\n background: transparent;\n}\n\n*::-webkit-scrollbar-thumb {\n border: 3px solid transparent;\n border-radius: 999px;\n background: var(--line-2);\n background-clip: content-box;\n}\n\n*::-webkit-scrollbar-thumb:hover {\n background: var(--dim);\n background-clip: content-box;\n}\n\n*::-webkit-scrollbar-corner {\n background: transparent;\n}\n\n*::-webkit-scrollbar-button {\n display: none;\n}\n"
17
+ "content": "/*\n * dowel theme - the token vocabulary every product of the lacodda line shares.\n *\n * The vocabulary comes from the products themselves: kilna and kasl-server\n * already ship the same token names (bg / raise / soft / line / text / dim /\n * accent / good / warn / bad / info) and differ only in values. That is the\n * contract this file freezes. Names are the mockup's own words rather than\n * stock component-library names, so a screen can be checked against a mockup\n * in the mockup's words.\n *\n * Two things are parametric, and everything else is derived from them:\n *\n * --accent-base the product's hue from the brand-line registry\n * --neutral-base the hue the greys are tinted with (the accent, by default)\n *\n * Tinting the neutrals is not decoration - it is what the two live products do\n * by hand: kilna's greys lean magenta, kasl-server's lean gold. Here that lean\n * is one declaration instead of thirty hand-picked hex values.\n *\n * Soft variants are mixed from their own base with `color-mix`, so a product\n * that overrides `--accent-base` gets a matching `--accent-soft` for free and\n * cannot pick one that disagrees with it.\n *\n * Theme selection: no class on the root element follows the operating system,\n * an explicit `light` or `dark` class pins the theme. Components never use\n * `dark:` utilities - every colour goes through a token, and the theme swaps\n * the token underneath.\n */\n\n:root {\n /* The two parameters. `--accent-base` is overridden per product by an accent\n * file; `--neutral-base` follows it unless a product says otherwise. */\n --accent-base: #e8862d;\n --neutral-base: var(--accent-base);\n\n /* Ink and ground of the dark theme, before the neutral tint is mixed in.\n * Kept as their own tokens so the tint amount is the only thing that\n * changes when a product wants greyer or warmer chrome. */\n --ground: #131316;\n --ink: #ece9ef;\n\n /* How much of `--neutral-base` bleeds into the greys. The live products sit\n * at roughly this much: enough that the chrome belongs to the product,\n * little enough that it still reads as grey. */\n --neutral-tint: 6%;\n --neutral-tint-strong: 9%;\n\n color-scheme: dark;\n\n --bg: color-mix(in oklab, var(--neutral-base) var(--neutral-tint), var(--ground));\n --raise: color-mix(in oklab, var(--neutral-base) var(--neutral-tint), #1c1c21);\n\n /* Surfaces that lift by translucency rather than by their own colour: they\n * must work over `--bg` and over `--raise` alike. */\n --soft: rgb(255 255 255 / 0.045);\n --softer: rgb(255 255 255 / 0.025);\n --line: rgb(255 255 255 / 0.08);\n --line-2: rgb(255 255 255 / 0.15);\n\n --text: color-mix(in oklab, var(--neutral-base) var(--neutral-tint), var(--ink));\n --dim: color-mix(in oklab, var(--neutral-base) var(--neutral-tint-strong), #a7a2ad);\n --faint: color-mix(in oklab, var(--neutral-base) var(--neutral-tint-strong), #6e6a76);\n\n --accent: var(--accent-base);\n /* The hover/active partner: lighter on dark, where the ground is what the\n * accent has to separate from. */\n --accent-2: color-mix(in oklab, white 22%, var(--accent-base));\n --accent-soft: color-mix(in oklab, var(--accent-base) 16%, transparent);\n\n /* Status hues are the line's own and do not follow the product accent: a\n * green that shifted per product would stop meaning \"good\". Meaning never\n * rests on colour alone - a badge carries an icon and a word - so these\n * exist for emphasis, not as the message. */\n --good: #45d18f;\n --warn: #e8b13f;\n --bad: #ef6a6a;\n --info: #4cc4e0;\n --good-soft: color-mix(in oklab, var(--good) 14%, transparent);\n --warn-soft: color-mix(in oklab, var(--warn) 14%, transparent);\n --bad-soft: color-mix(in oklab, var(--bad) 14%, transparent);\n --info-soft: color-mix(in oklab, var(--info) 15%, transparent);\n\n /* Elevation, three steps. The products had one shadow and used it for\n * everything that leaves the flow - a toast, a dropdown and a modal all\n * floated by the same amount, so a modal never felt further away than the\n * menu it covered. `raise` keeps its original value, so nothing shifts under\n * the products already using it; the other two are the steps either side. */\n --shadow-lift: 0 2px 8px rgb(0 0 0 / 0.3);\n --shadow-raise: 0 10px 34px rgb(0 0 0 / 0.45);\n --shadow-float: 0 24px 60px rgb(0 0 0 / 0.55);\n}\n\n/*\n * Light theme, twice: once for the operating system's preference, once for the\n * explicit `light` class. The declarations are identical - only the selector\n * differs - so that a product can pin a theme against the system setting.\n */\n@media (prefers-color-scheme: light) {\n :root:not(.dark) {\n color-scheme: light;\n\n --ground: #f6f5f7;\n --ink: #232027;\n\n --bg: color-mix(in oklab, var(--neutral-base) var(--neutral-tint), var(--ground));\n --raise: #ffffff;\n\n /* Tinted, and translucent - which took two goes to get right.\n *\n * `color-mix` mixes the alpha along with the colour, so mixing 60% of an\n * opaque accent into a 5%-opaque grey gives a surface 62% opaque: twelve\n * times denser than the hairline it was meant to be. It went unnoticed for\n * ten versions because `bg-soft` was only ever used for a hover, where a\n * flash of colour reads as feedback rather than as a mistake. The first\n * component to sit on it permanently - Alert - made it obvious.\n *\n * `oklch(from … / alpha)` keeps the alpha out of the mix: the hue comes\n * from the tinted colour, the transparency is stated. */\n --soft: oklch(from color-mix(in oklab, var(--neutral-base) 45%, #181420) l c h / 0.05);\n --softer: oklch(from color-mix(in oklab, var(--neutral-base) 45%, #181420) l c h / 0.03);\n --line: oklch(from color-mix(in oklab, var(--neutral-base) 30%, #181420) l c h / 0.11);\n --line-2: oklch(from color-mix(in oklab, var(--neutral-base) 30%, #181420) l c h / 0.2);\n\n --text: color-mix(in oklab, var(--neutral-base) var(--neutral-tint), var(--ink));\n --dim: color-mix(in oklab, var(--neutral-base) var(--neutral-tint-strong), #63606b);\n --faint: color-mix(in oklab, var(--neutral-base) var(--neutral-tint-strong), #8a8692);\n\n /* On a light ground the accent has to darken to stay legible as text and\n * as a fill. It darkens *to* a lightness rather than *by* an amount: how\n * far a hue has to travel depends on where it starts, and a fixed step\n * that suits magenta leaves lime and gold short. Pinning the lightness and\n * keeping the hue and chroma clears 5:1 for every accent in the line. */\n --accent: oklch(from var(--accent-base) 0.5 c h);\n --accent-2: oklch(from var(--accent-base) 0.4 c h);\n --accent-soft: color-mix(in oklab, var(--accent-base) 12%, transparent);\n\n --good: #0c8554;\n --warn: #9a6b0c;\n --bad: #c93b3b;\n --info: #0d7f9c;\n --good-soft: color-mix(in oklab, var(--good) 12%, transparent);\n --warn-soft: color-mix(in oklab, var(--warn) 14%, transparent);\n --bad-soft: color-mix(in oklab, var(--bad) 12%, transparent);\n --info-soft: color-mix(in oklab, var(--info) 11%, transparent);\n\n --shadow-lift: 0 2px 8px rgb(30 24 38 / 0.08);\n --shadow-raise: 0 10px 30px rgb(30 24 38 / 0.14);\n --shadow-float: 0 24px 60px rgb(30 24 38 / 0.18);\n }\n}\n\n:root.light {\n color-scheme: light;\n\n --ground: #f6f5f7;\n --ink: #232027;\n\n --bg: color-mix(in oklab, var(--neutral-base) var(--neutral-tint), var(--ground));\n --raise: #ffffff;\n\n /* The same surfaces as above; see the note there. */\n --soft: oklch(from color-mix(in oklab, var(--neutral-base) 45%, #181420) l c h / 0.05);\n --softer: oklch(from color-mix(in oklab, var(--neutral-base) 45%, #181420) l c h / 0.03);\n --line: oklch(from color-mix(in oklab, var(--neutral-base) 30%, #181420) l c h / 0.11);\n --line-2: oklch(from color-mix(in oklab, var(--neutral-base) 30%, #181420) l c h / 0.2);\n\n --text: color-mix(in oklab, var(--neutral-base) var(--neutral-tint), var(--ink));\n --dim: color-mix(in oklab, var(--neutral-base) var(--neutral-tint-strong), #63606b);\n --faint: color-mix(in oklab, var(--neutral-base) var(--neutral-tint-strong), #8a8692);\n\n /* Darkened to a lightness, not by an amount - see the note above. */\n --accent: oklch(from var(--accent-base) 0.5 c h);\n --accent-2: oklch(from var(--accent-base) 0.4 c h);\n --accent-soft: color-mix(in oklab, var(--accent-base) 12%, transparent);\n\n --good: #0c8554;\n --warn: #9a6b0c;\n --bad: #c93b3b;\n --info: #0d7f9c;\n --good-soft: color-mix(in oklab, var(--good) 12%, transparent);\n --warn-soft: color-mix(in oklab, var(--warn) 14%, transparent);\n --bad-soft: color-mix(in oklab, var(--bad) 12%, transparent);\n --info-soft: color-mix(in oklab, var(--info) 11%, transparent);\n\n --shadow-lift: 0 2px 8px rgb(30 24 38 / 0.08);\n --shadow-raise: 0 10px 30px rgb(30 24 38 / 0.14);\n --shadow-float: 0 24px 60px rgb(30 24 38 / 0.18);\n}\n\n/*\n * `--on-accent` - what sits on top of an accent fill.\n *\n * A light accent (gold, lime, amber) needs dark glyphs; a dark one needs\n * white. The live products picked this by hand and wrote the answer into the\n * theme; here the theme works it out, by the same rule the brand-line S tile\n * uses.\n *\n * `contrast-color()` is the direct way to say it and is used where supported.\n * The fallback covers browsers that lack it. Relative colour syntax exposes\n * the accent's own lightness as `l`; `clamp()` turns that into a hard switch,\n * because the multiplication drives the middle term far past either bound\n * everywhere except within a hair of the threshold. Below it the accent is\n * dark and the result is 1 (white); above it, 0 (black). Chroma is dropped to\n * zero, so what comes out is neutral rather than a tinted grey.\n *\n * The threshold is 0.58, and it is deliberately far below the midpoint an eye\n * would guess. Contrast is not symmetric about it: a mid-lightness colour is\n * still much closer to white than to black in luminance, so black wins well\n * before the colour looks light. Checked against all fourteen accents of the\n * line - every one of them reads better with dark glyphs, the closest being\n * cobalt at 4.86:1 against 4.32:1 for white. A higher threshold is what puts\n * white text on magenta at 3.6:1, which is the defect this rule exists to\n * prevent.\n */\n:root {\n --on-accent: oklch(from var(--accent) clamp(0, (0.58 - l) * 1000, 1) 0 0);\n\n /*\n * The same question for the status fills, and it has to be asked separately:\n * `--on-accent` is derived from the accent, so using it on a `--warn` fill\n * is only ever right by coincidence. The line's first consumer did exactly\n * that - a count on a yellow badge, drawn in white at 1.95:1 - and it read\n * as correct for as long as the product happened to pin white.\n *\n * The status hues do not follow the product accent, so these four are the\n * same for every product; they are still derived rather than written down,\n * because the status colours themselves change between the themes.\n */\n --on-good: oklch(from var(--good) clamp(0, (0.58 - l) * 1000, 1) 0 0);\n --on-warn: oklch(from var(--warn) clamp(0, (0.58 - l) * 1000, 1) 0 0);\n --on-bad: oklch(from var(--bad) clamp(0, (0.58 - l) * 1000, 1) 0 0);\n --on-info: oklch(from var(--info) clamp(0, (0.58 - l) * 1000, 1) 0 0);\n}\n\n@supports (color: contrast-color(red)) {\n :root {\n --on-accent: contrast-color(var(--accent));\n --on-good: contrast-color(var(--good));\n --on-warn: contrast-color(var(--warn));\n --on-bad: contrast-color(var(--bad));\n --on-info: contrast-color(var(--info));\n }\n}\n\n/*\n * The Tailwind 4 surface. `--color-*: initial` drops the stock palette on\n * purpose: a raw `bg-zinc-800` in a product should not compile, because the\n * only colours that exist here are the line's own.\n */\n@theme inline {\n --color-*: initial;\n --color-bg: var(--bg);\n --color-raise: var(--raise);\n --color-soft: var(--soft);\n --color-softer: var(--softer);\n --color-line: var(--line);\n --color-line-2: var(--line-2);\n --color-text: var(--text);\n --color-dim: var(--dim);\n --color-faint: var(--faint);\n --color-accent: var(--accent);\n --color-accent-2: var(--accent-2);\n --color-accent-soft: var(--accent-soft);\n --color-on-accent: var(--on-accent);\n --color-on-good: var(--on-good);\n --color-on-warn: var(--on-warn);\n --color-on-bad: var(--on-bad);\n --color-on-info: var(--on-info);\n --color-good: var(--good);\n --color-good-soft: var(--good-soft);\n --color-warn: var(--warn);\n --color-warn-soft: var(--warn-soft);\n --color-bad: var(--bad);\n --color-bad-soft: var(--bad-soft);\n --color-info: var(--info);\n --color-info-soft: var(--info-soft);\n\n /* Kept because they are not palette choices: a hairline is `transparent`,\n * an SVG follows `currentColor`, and pure black and white are what an\n * overlay scrim and a print sheet are made of. */\n --color-transparent: transparent;\n --color-current: currentColor;\n --color-white: #fff;\n --color-black: #000;\n\n /*\n * Type. System stacks on purpose: a downloaded face costs a network round\n * trip before the first word appears, and the line's products are desktop\n * tools where the operating system's own face is the one the user already\n * reads everything else in.\n */\n --font-sans: 'Segoe UI Variable Text', 'Segoe UI', system-ui, -apple-system, sans-serif;\n --font-mono: ui-monospace, 'Cascadia Code', 'SF Mono', Consolas, monospace;\n\n /*\n * Radius. Taken from what the products actually draw, not from a ratio:\n * `rounded-[9px]` appears twenty times and `rounded-[10px]` twelve, because\n * a control and the primary button were tuned by eye and then copied. The\n * scale keeps the cluster they landed in and gives it names.\n *\n * `md` is the control radius - inputs, buttons, list rows. That the primary\n * button was one pixel rounder than every other variant is not preserved:\n * the products differ from themselves there, and buttons of the same size\n * sitting side by side should not have mismatched corners.\n */\n --radius-xs: 4px;\n --radius-sm: 6px;\n --radius-md: 9px;\n --radius-lg: 12px;\n --radius-xl: 16px;\n --radius-2xl: 20px;\n\n /*\n * A radius nested inside another has to be smaller by the gap between them,\n * or the inner corner looks wrong against the outer one. The products did\n * this by hand once - 18px outside, 17px inside - and nowhere else.\n */\n --radius-inner: calc(var(--radius-lg) - 1px);\n\n /*\n * Type scale. The products live between 10px and 14px: `text-sm` and\n * `text-xs` together account for nine tenths of every size in both, and the\n * rest scattered across 9, 9.5, 10, 10.5, 11, 11.5, 12.5 and 13 - nine steps\n * inside four pixels, which no eye distinguishes and no reason justifies.\n * This is the same range with the noise removed.\n */\n --text-2xs: 10px;\n --text-2xs--line-height: 14px;\n --text-xs: 11px;\n --text-xs--line-height: 15px;\n --text-sm: 12px;\n --text-sm--line-height: 16px;\n --text-base: 14px;\n --text-base--line-height: 20px;\n --text-lg: 16px;\n --text-lg--line-height: 22px;\n --text-xl: 18px;\n --text-xl--line-height: 24px;\n --text-2xl: 21px;\n --text-2xl--line-height: 28px;\n\n /* Weights. `semibold` is what both products use for anything emphasised;\n * `bold` appears in neither, and the one `font-[650]` in a page title is the\n * kind of value a scale exists to absorb. */\n --font-weight-normal: 400;\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n\n /* Tracking. The uppercase caption is the only place the products track at\n * all - and they do it at 0.08em in six files and 0.09em in three, a\n * difference nobody can see. One name settles it. */\n --tracking-caption: 0.085em;\n --tracking-tight: -0.01em;\n\n /* Easing. `out` for anything the user asked for - it arrives fast and\n * settles, which reads as responsive. `in-out` for something moving on its\n * own. `in` is deliberately absent: it starts slowly, which on a control\n * reads as lag. */\n --ease-out: cubic-bezier(0.2, 0, 0, 1);\n --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\n\n /*\n * Elevation. Three steps, because the products had one and used it for a\n * toast, a dropdown and a modal alike - so a modal never sat further from\n * the page than the menu it covered.\n */\n --shadow-lift: var(--shadow-lift);\n --shadow-raise: var(--shadow-raise);\n --shadow-float: var(--shadow-float);\n}\n\n/*\n * Stacking order.\n *\n * Not in `@theme`: Tailwind has no z-index namespace, so `z-50` is a literal\n * fifty and a named step would not compile. These are custom properties a\n * component reads directly - `z-index: var(--z-modal)`.\n *\n * The order is the products' own, with the gaps closed. They ran 10 for an\n * in-flow popup, 20 sticky, 30 menu, 40 for a floating button, 50 for modals\n * and drawers, then jumped to 70 and 80 for the command palette - which had to\n * clear the modal layer and had no name to do it with.\n */\n:root {\n /*\n * Motion. One duration existed before this - 160ms on a route change - and\n * everything else rode Tailwind's default. These are the steps around it:\n * `quick` for a colour or an opacity that should feel immediate, `base` for\n * something that moves, `slow` for something arriving from off-screen.\n *\n * Not in `@theme`: Tailwind's `duration-*` utility takes a literal number,\n * not a named step, so these are read directly - `transition-duration:\n * var(--duration-base)`. The easing curves opposite them ARE a namespace,\n * so `ease-out` is a class.\n *\n * Every duration here is for people who want motion: `prefers-reduced-\n * motion` cuts them to nothing further down.\n */\n --duration-quick: 120ms;\n --duration-base: 160ms;\n --duration-slow: 240ms;\n\n --z-popup: 10;\n --z-sticky: 20;\n --z-menu: 30;\n --z-floating: 40;\n --z-overlay: 50;\n --z-modal: 60;\n --z-palette: 70;\n --z-toast: 80;\n}\n\n/*\n * Base layer: what every product would otherwise write again. Scoped to\n * elements and to `:focus-visible`, never to a class, so nothing here can\n * collide with a component.\n */\nbody {\n margin: 0;\n background-color: var(--bg);\n color: var(--text);\n font-family: var(--font-sans);\n -webkit-font-smoothing: antialiased;\n}\n\n/*\n * In `@layer base`, so a component can turn it off.\n *\n * Unlayered, this rule has the same specificity as `focus-visible:outline-none`\n * from Tailwind - both are one pseudo-class - and wins on source order alone,\n * because the theme is imported before the utilities. Every component that\n * draws its own focus ring got this one on top of it: the command palette's\n * field had an accent outline it had explicitly opted out of, and a combobox\n * with chips drew two rings, one around the box and one around the input\n * inside it.\n *\n * A layered rule loses to any unlayered one regardless of specificity, which\n * is the whole point of cascade layers - the base layer states a default and\n * a component overrides it by saying so.\n */\n@layer base {\n :focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n transition-duration: 0.01ms !important;\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n scroll-behavior: auto !important;\n }\n}\n\n/*\n * Scrollbars. A browser's default bar is a piece of someone else's chrome\n * sitting in the middle of the product - full width, with step arrows. These\n * are the line's own: thin, in the palette, drawn only where something\n * actually scrolls.\n */\n* {\n scrollbar-width: thin;\n scrollbar-color: var(--line-2) transparent;\n}\n\n*::-webkit-scrollbar {\n width: 10px;\n height: 10px;\n}\n\n*::-webkit-scrollbar-track {\n background: transparent;\n}\n\n*::-webkit-scrollbar-thumb {\n border: 3px solid transparent;\n border-radius: 999px;\n background: var(--line-2);\n background-clip: content-box;\n /* The thumb is drawn proportional to how much of the document fits on\n * screen, so a long one collapses it to a few pixels: still visible, no\n * longer catchable by a pointer. This is the floor below which it stops\n * being a control. */\n min-height: 2.5rem;\n}\n\n*::-webkit-scrollbar-thumb:hover {\n background: var(--dim);\n background-clip: content-box;\n}\n\n*::-webkit-scrollbar-corner {\n background: transparent;\n}\n\n*::-webkit-scrollbar-button {\n display: none;\n}\n"
18
18
  }
19
19
  ],
20
20
  "docs": "Import the theme, then your product accent:\n\n @import './dowel/theme.css';\n @import './dowel/accents/kilna.css';\n\nOutside the line, set the colour directly instead:\n\n :root { --accent-base: #2f7d6b; }"
@@ -215,6 +215,76 @@
215
215
  }
216
216
  ]
217
217
  },
218
+ {
219
+ "name": "accent-rhapsod",
220
+ "type": "registry:file",
221
+ "title": "rhapsod accent",
222
+ "description": "field green #6CB14E - the colour of the rhapsod mark (rh). Import after the theme.",
223
+ "files": [
224
+ {
225
+ "path": "dowel/accents/rhapsod.css",
226
+ "target": "~/dowel/accents/rhapsod.css",
227
+ "type": "registry:file",
228
+ "content": "/*\n * rhapsod - field green #6CB14E, the colour of its mark (rh).\n *\n * Import after the theme. Everything else - the hover shade, the soft fill,\n * the focus ring, the colour of text on an accent fill, and the trace of the\n * hue the greys carry - is derived from this one value.\n */\n:root {\n --accent-base: #6CB14E;\n}\n"
229
+ }
230
+ ]
231
+ },
232
+ {
233
+ "name": "accent-hilvan",
234
+ "type": "registry:file",
235
+ "title": "hilvan accent",
236
+ "description": "crimson #D64550 - the colour of the hilvan mark (hv). Import after the theme.",
237
+ "files": [
238
+ {
239
+ "path": "dowel/accents/hilvan.css",
240
+ "target": "~/dowel/accents/hilvan.css",
241
+ "type": "registry:file",
242
+ "content": "/*\n * hilvan - crimson #D64550, the colour of its mark (hv).\n *\n * Import after the theme. Everything else - the hover shade, the soft fill,\n * the focus ring, the colour of text on an accent fill, and the trace of the\n * hue the greys carry - is derived from this one value.\n */\n:root {\n --accent-base: #D64550;\n}\n"
243
+ }
244
+ ]
245
+ },
246
+ {
247
+ "name": "accent-scheda",
248
+ "type": "registry:file",
249
+ "title": "scheda accent",
250
+ "description": "terracotta (paired with sea) #D9704A - the colour of the scheda mark (sc). Import after the theme.",
251
+ "files": [
252
+ {
253
+ "path": "dowel/accents/scheda.css",
254
+ "target": "~/dowel/accents/scheda.css",
255
+ "type": "registry:file",
256
+ "content": "/*\n * scheda - terracotta (paired with sea) #D9704A, the colour of its mark (sc).\n *\n * Import after the theme. Everything else - the hover shade, the soft fill,\n * the focus ring, the colour of text on an accent fill, and the trace of the\n * hue the greys carry - is derived from this one value.\n */\n:root {\n --accent-base: #D9704A;\n}\n"
257
+ }
258
+ ]
259
+ },
260
+ {
261
+ "name": "accent-furca",
262
+ "type": "registry:file",
263
+ "title": "furca accent",
264
+ "description": "terracotta (paired with olive) #D9704A - the colour of the furca mark (fr). Import after the theme.",
265
+ "files": [
266
+ {
267
+ "path": "dowel/accents/furca.css",
268
+ "target": "~/dowel/accents/furca.css",
269
+ "type": "registry:file",
270
+ "content": "/*\n * furca - terracotta (paired with olive) #D9704A, the colour of its mark (fr).\n *\n * Import after the theme. Everything else - the hover shade, the soft fill,\n * the focus ring, the colour of text on an accent fill, and the trace of the\n * hue the greys carry - is derived from this one value.\n */\n:root {\n --accent-base: #D9704A;\n}\n"
271
+ }
272
+ ]
273
+ },
274
+ {
275
+ "name": "accent-rigger",
276
+ "type": "registry:file",
277
+ "title": "rigger accent",
278
+ "description": "iris (paired with lagoon) #8A62F0 - the colour of the rigger mark (rr). Import after the theme.",
279
+ "files": [
280
+ {
281
+ "path": "dowel/accents/rigger.css",
282
+ "target": "~/dowel/accents/rigger.css",
283
+ "type": "registry:file",
284
+ "content": "/*\n * rigger - iris (paired with lagoon) #8A62F0, the colour of its mark (rr).\n *\n * Import after the theme. Everything else - the hover shade, the soft fill,\n * the focus ring, the colour of text on an accent fill, and the trace of the\n * hue the greys carry - is derived from this one value.\n */\n:root {\n --accent-base: #8A62F0;\n}\n"
285
+ }
286
+ ]
287
+ },
218
288
  {
219
289
  "name": "action-bar",
220
290
  "type": "registry:ui",
@@ -223,7 +293,7 @@
223
293
  "dependencies": [
224
294
  "@base-ui/react",
225
295
  "class-variance-authority",
226
- "dowel-ui@^0.20.1"
296
+ "dowel-ui@^0.20.3"
227
297
  ],
228
298
  "registryDependencies": [],
229
299
  "files": [
@@ -242,7 +312,7 @@
242
312
  "description": "A message that stays on the screen, in the flow of the page, about the thing next to it: this field could not be saved, this profile has no axes yet, this export is out of date.",
243
313
  "dependencies": [
244
314
  "class-variance-authority",
245
- "dowel-ui@^0.20.1"
315
+ "dowel-ui@^0.20.3"
246
316
  ],
247
317
  "registryDependencies": [],
248
318
  "files": [
@@ -261,7 +331,7 @@
261
331
  "description": "A small piece of state attached to something else: a count, a status, a label. It is not a button and never was - if it can be clicked it is a Chip.",
262
332
  "dependencies": [
263
333
  "class-variance-authority",
264
- "dowel-ui@^0.20.1"
334
+ "dowel-ui@^0.20.3"
265
335
  ],
266
336
  "registryDependencies": [],
267
337
  "files": [
@@ -280,7 +350,7 @@
280
350
  "description": "A strip across the top of the application, about the application: you are offline, this build is a preview, your licence expires on Friday, a new version is ready to install.",
281
351
  "dependencies": [
282
352
  "class-variance-authority",
283
- "dowel-ui@^0.20.1"
353
+ "dowel-ui@^0.20.3"
284
354
  ],
285
355
  "registryDependencies": [],
286
356
  "files": [
@@ -300,7 +370,7 @@
300
370
  "dependencies": [
301
371
  "@base-ui/react",
302
372
  "class-variance-authority",
303
- "dowel-ui@^0.20.1"
373
+ "dowel-ui@^0.20.3"
304
374
  ],
305
375
  "registryDependencies": [],
306
376
  "files": [
@@ -334,7 +404,7 @@
334
404
  "title": "Calendar",
335
405
  "description": "The sums live next door in `calendar-math`, which has no React in it; this is the grid that draws them and the keyboard that moves around it.",
336
406
  "dependencies": [
337
- "dowel-ui@^0.20.1"
407
+ "dowel-ui@^0.20.3"
338
408
  ],
339
409
  "registryDependencies": [
340
410
  "https://lacodda.github.io/dowel/r/calendar-math.json"
@@ -355,7 +425,7 @@
355
425
  "description": "The interesting part is the words. A checkbox on its own is a nine-pixel target that says nothing; wired to a label it is the whole row, and the row is what a finger and a pointer both aim at. So the label is part of the component rather than something a caller remembers to add - the commonest bug in a hand-rolled checkbox is a `<label>` that is next to the input instead of tied to it, which looks identical and does nothing.",
356
426
  "dependencies": [
357
427
  "@base-ui/react",
358
- "dowel-ui@^0.20.1"
428
+ "dowel-ui@^0.20.3"
359
429
  ],
360
430
  "registryDependencies": [],
361
431
  "files": [
@@ -374,7 +444,7 @@
374
444
  "description": "A badge you can act on: a filter that can be removed, a tag with a count, a selected value in a field. The difference from a Badge is entirely about whether something happens when you click it - and if something does, that part is a real `<button>` with a real label, not a decorative cross.",
375
445
  "dependencies": [
376
446
  "class-variance-authority",
377
- "dowel-ui@^0.20.1"
447
+ "dowel-ui@^0.20.3"
378
448
  ],
379
449
  "registryDependencies": [],
380
450
  "files": [
@@ -392,7 +462,7 @@
392
462
  "title": "Color-field",
393
463
  "description": "Picking a colour for something the product stores: a tag, a project, a calendar. Note what that is *not* - it is not choosing the appearance of the interface. The theme decides that, from one accent, and a field that let a reader repaint the chrome would undo the argument the whole system rests on.",
394
464
  "dependencies": [
395
- "dowel-ui@^0.20.1"
465
+ "dowel-ui@^0.20.3"
396
466
  ],
397
467
  "registryDependencies": [
398
468
  "https://lacodda.github.io/dowel/r/input.json"
@@ -414,7 +484,7 @@
414
484
  "dependencies": [
415
485
  "@base-ui/react",
416
486
  "class-variance-authority",
417
- "dowel-ui@^0.20.1"
487
+ "dowel-ui@^0.20.3"
418
488
  ],
419
489
  "registryDependencies": [
420
490
  "https://lacodda.github.io/dowel/r/input.json",
@@ -437,7 +507,7 @@
437
507
  "dependencies": [
438
508
  "@base-ui/react",
439
509
  "class-variance-authority",
440
- "dowel-ui@^0.20.1"
510
+ "dowel-ui@^0.20.3"
441
511
  ],
442
512
  "registryDependencies": [
443
513
  "https://lacodda.github.io/dowel/r/combobox.json",
@@ -460,7 +530,7 @@
460
530
  "dependencies": [
461
531
  "@base-ui/react",
462
532
  "class-variance-authority",
463
- "dowel-ui@^0.20.1"
533
+ "dowel-ui@^0.20.3"
464
534
  ],
465
535
  "registryDependencies": [],
466
536
  "files": [
@@ -479,7 +549,7 @@
479
549
  "description": "The same list of actions as Menu, opened the other way round: by right click, or by a long press on a touch screen, over an *area* rather than from a button. So the trigger is not a control - it is the region the menu belongs to, a row, a canvas, a file tile - and it renders a `<div>`.",
480
550
  "dependencies": [
481
551
  "@base-ui/react",
482
- "dowel-ui@^0.20.1"
552
+ "dowel-ui@^0.20.3"
483
553
  ],
484
554
  "registryDependencies": [
485
555
  "https://lacodda.github.io/dowel/r/menu.json"
@@ -499,7 +569,7 @@
499
569
  "title": "Copyable",
500
570
  "description": "Any text that someone will eventually want to copy - an id, a path, a hash, a token - copied with one click. The rule comes from nitid: if a value is worth showing, it is worth being able to take away, and selecting a monospaced id by hand is a small daily tax.",
501
571
  "dependencies": [
502
- "dowel-ui@^0.20.1"
572
+ "dowel-ui@^0.20.3"
503
573
  ],
504
574
  "registryDependencies": [],
505
575
  "files": [
@@ -517,7 +587,7 @@
517
587
  "title": "Date-picker",
518
588
  "description": "The trigger is a button rather than a text input, and that is the decision worth stating. A typable date field has to answer \"what does `03/04/26` mean\" in a locale it cannot be sure of, and it answers wrong for half the world; a button showing the date spelled out has no such question. Where typing genuinely matters - a birth date, forty years back - the calendar is the wrong control anyway and a product should reach for a plain field.",
519
589
  "dependencies": [
520
- "dowel-ui@^0.20.1"
590
+ "dowel-ui@^0.20.3"
521
591
  ],
522
592
  "registryDependencies": [
523
593
  "https://lacodda.github.io/dowel/r/calendar.json",
@@ -540,7 +610,7 @@
540
610
  "title": "Date-range-picker",
541
611
  "description": "The interesting part is the state between them. After the first click there is a start and no end, and that is not an incomplete range to be hidden or a range of one day - it is the normal middle of the interaction, and the calendar has to show it: the first day marked, the days under the pointer shading as the reader moves, the popup staying open. Products that skip it end up with a picker that seems to do nothing until the second click.",
542
612
  "dependencies": [
543
- "dowel-ui@^0.20.1"
613
+ "dowel-ui@^0.20.3"
544
614
  ],
545
615
  "registryDependencies": [
546
616
  "https://lacodda.github.io/dowel/r/calendar.json",
@@ -565,7 +635,7 @@
565
635
  "dependencies": [
566
636
  "@base-ui/react",
567
637
  "class-variance-authority",
568
- "dowel-ui@^0.20.1"
638
+ "dowel-ui@^0.20.3"
569
639
  ],
570
640
  "registryDependencies": [],
571
641
  "files": [
@@ -585,7 +655,7 @@
585
655
  "dependencies": [
586
656
  "@base-ui/react",
587
657
  "class-variance-authority",
588
- "dowel-ui@^0.20.1"
658
+ "dowel-ui@^0.20.3"
589
659
  ],
590
660
  "registryDependencies": [],
591
661
  "files": [
@@ -603,7 +673,7 @@
603
673
  "title": "Duration-field",
604
674
  "description": "The alternative is what products keep building: two number boxes labelled \"hours\" and \"minutes\", which means two tab stops, two validations, and a reader who has to divide 90 minutes in their head before typing. Here they write `1h 30m`, or `90m`, or `1.5h`, and it means the same thing.",
605
675
  "dependencies": [
606
- "dowel-ui@^0.20.1"
676
+ "dowel-ui@^0.20.3"
607
677
  ],
608
678
  "registryDependencies": [
609
679
  "https://lacodda.github.io/dowel/r/input.json"
@@ -624,7 +694,7 @@
624
694
  "description": "Every form is the same four parts repeated: a name for the control, the control, sometimes a hint, and sometimes an error. Written by hand each time, they drift - the label loses its `htmlFor`, the hint is a `<div>` no screen reader mentions, the error appears in red and is announced by nothing at all. This is that arrangement, once.",
625
695
  "dependencies": [
626
696
  "@base-ui/react",
627
- "dowel-ui@^0.20.1"
697
+ "dowel-ui@^0.20.3"
628
698
  ],
629
699
  "registryDependencies": [],
630
700
  "files": [
@@ -642,7 +712,7 @@
642
712
  "title": "File-drop",
643
713
  "description": "A place to put files: drag them onto it, or press it and pick them. It takes files and hands them over - it does not upload them. Where they go, with which credentials, retried how - that is the product's transport, and a primitive that owned it would be wrong for every product whose upload does not look like the one it guessed.",
644
714
  "dependencies": [
645
- "dowel-ui@^0.20.1"
715
+ "dowel-ui@^0.20.3"
646
716
  ],
647
717
  "registryDependencies": [],
648
718
  "files": [
@@ -660,7 +730,7 @@
660
730
  "title": "Input",
661
731
  "description": "A single-line field. It is a plain `<input>` with the line's clothes on, so everything a browser gives an input for free - autofill, spellcheck, the right keyboard on a phone, `type=\"email\"` validation - still works.",
662
732
  "dependencies": [
663
- "dowel-ui@^0.20.1"
733
+ "dowel-ui@^0.20.3"
664
734
  ],
665
735
  "registryDependencies": [],
666
736
  "files": [
@@ -678,7 +748,7 @@
678
748
  "title": "Kbd",
679
749
  "description": "A key, as printed in a menu or a hint: `Ctrl` `K`. It is a `<kbd>` element because that is what the element is for - a screen reader announces it as keyboard input rather than reading a stray capital letter.",
680
750
  "dependencies": [
681
- "dowel-ui@^0.20.1"
751
+ "dowel-ui@^0.20.3"
682
752
  ],
683
753
  "registryDependencies": [],
684
754
  "files": [
@@ -698,7 +768,7 @@
698
768
  "dependencies": [
699
769
  "@base-ui/react",
700
770
  "class-variance-authority",
701
- "dowel-ui@^0.20.1"
771
+ "dowel-ui@^0.20.3"
702
772
  ],
703
773
  "registryDependencies": [],
704
774
  "files": [
@@ -717,7 +787,7 @@
717
787
  "description": "A number typed into a text input is a string that happens to look like a number, and every product then writes the same four fixes: strip the letters, clamp to a range, round to a step, and decide what an empty box means. This is those four, once, plus the stepper - because a value with a small range is faster nudged than typed.",
718
788
  "dependencies": [
719
789
  "@base-ui/react",
720
- "dowel-ui@^0.20.1"
790
+ "dowel-ui@^0.20.3"
721
791
  ],
722
792
  "registryDependencies": [
723
793
  "https://lacodda.github.io/dowel/r/input.json"
@@ -738,7 +808,7 @@
738
808
  "description": "The raised surface everything else sits on. It is the one place a screen gets its structure from, so it stays deliberately plain: a ground, a hairline, a corner.",
739
809
  "dependencies": [
740
810
  "class-variance-authority",
741
- "dowel-ui@^0.20.1"
811
+ "dowel-ui@^0.20.3"
742
812
  ],
743
813
  "registryDependencies": [],
744
814
  "files": [
@@ -756,7 +826,7 @@
756
826
  "title": "Password-field",
757
827
  "description": "The reveal is the whole component, and it is not a convenience. A masked field is the only one in a form where a typo cannot be seen, so people either paste (fine) or type slowly and get it wrong anyway; the toggle is what turns an unverifiable field into a checkable one, and it is why long passphrases became usable at all.",
758
828
  "dependencies": [
759
- "dowel-ui@^0.20.1"
829
+ "dowel-ui@^0.20.3"
760
830
  ],
761
831
  "registryDependencies": [
762
832
  "https://lacodda.github.io/dowel/r/input.json"
@@ -778,7 +848,7 @@
778
848
  "dependencies": [
779
849
  "@base-ui/react",
780
850
  "class-variance-authority",
781
- "dowel-ui@^0.20.1"
851
+ "dowel-ui@^0.20.3"
782
852
  ],
783
853
  "registryDependencies": [],
784
854
  "files": [
@@ -798,7 +868,7 @@
798
868
  "dependencies": [
799
869
  "@base-ui/react",
800
870
  "class-variance-authority",
801
- "dowel-ui@^0.20.1"
871
+ "dowel-ui@^0.20.3"
802
872
  ],
803
873
  "registryDependencies": [],
804
874
  "files": [
@@ -818,7 +888,7 @@
818
888
  "dependencies": [
819
889
  "@base-ui/react",
820
890
  "class-variance-authority",
821
- "dowel-ui@^0.20.1"
891
+ "dowel-ui@^0.20.3"
822
892
  ],
823
893
  "registryDependencies": [],
824
894
  "files": [
@@ -836,7 +906,7 @@
836
906
  "title": "Rating-scale",
837
907
  "description": "Generalised from kilna, where it is how a work is scored on each of its axes. The shape is a row of marks rather than stars: stars carry a meaning of their own - a review, a public verdict - and this is as often \"how hard was this\" or \"how finished is it\" as it is \"how good\".",
838
908
  "dependencies": [
839
- "dowel-ui@^0.20.1"
909
+ "dowel-ui@^0.20.3"
840
910
  ],
841
911
  "registryDependencies": [],
842
912
  "files": [
@@ -854,7 +924,7 @@
854
924
  "title": "Save-state",
855
925
  "description": "The quiet line beside a field that saves itself: \"saving…\", then a tick that fades. It exists because a form without a Save button has to say what it did anyway - otherwise the reader is left guessing whether their edit survived, and the usual answer to that guess is to press Ctrl+S at a page that has no such thing.",
856
926
  "dependencies": [
857
- "dowel-ui@^0.20.1"
927
+ "dowel-ui@^0.20.3"
858
928
  ],
859
929
  "registryDependencies": [
860
930
  "https://lacodda.github.io/dowel/r/spinner.json"
@@ -874,7 +944,7 @@
874
944
  "title": "Search-field",
875
945
  "description": "An Input that knows it is a search box, which is three small things the products kept not doing:\n * - a magnifier, so the field is recognisable before it is read; - a way to clear it that is not \"select all and delete\" - and one that a keyboard can reach, which a decorative `<span>` cannot; - the shortcut that focuses it, shown in the field rather than learned.",
876
946
  "dependencies": [
877
- "dowel-ui@^0.20.1"
947
+ "dowel-ui@^0.20.3"
878
948
  ],
879
949
  "registryDependencies": [
880
950
  "https://lacodda.github.io/dowel/r/input.json",
@@ -898,7 +968,7 @@
898
968
  "dependencies": [
899
969
  "@base-ui/react",
900
970
  "class-variance-authority",
901
- "dowel-ui@^0.20.1"
971
+ "dowel-ui@^0.20.3"
902
972
  ],
903
973
  "registryDependencies": [
904
974
  "https://lacodda.github.io/dowel/r/input.json"
@@ -935,7 +1005,7 @@
935
1005
  "description": "The case for it over a NumberField is that the number does not matter much: a volume, an opacity, a weight in a search filter. Where the exact figure does matter, a slider is a worse field with more pixels - it cannot be typed into, it cannot be pasted into, and it has no state for \"empty\".",
936
1006
  "dependencies": [
937
1007
  "@base-ui/react",
938
- "dowel-ui@^0.20.1"
1008
+ "dowel-ui@^0.20.3"
939
1009
  ],
940
1010
  "registryDependencies": [],
941
1011
  "files": [
@@ -954,7 +1024,7 @@
954
1024
  "description": "Something is happening and the answer has not arrived. It carries no text of its own - what is loading is the product's word, not the system's - but it does have to say *something* to a screen reader, or a page that is busy is silently identical to a page that is empty.",
955
1025
  "dependencies": [
956
1026
  "class-variance-authority",
957
- "dowel-ui@^0.20.1"
1027
+ "dowel-ui@^0.20.3"
958
1028
  ],
959
1029
  "registryDependencies": [],
960
1030
  "files": [
@@ -973,7 +1043,7 @@
973
1043
  "description": "The difference from Checkbox is not how it looks, and getting it wrong is the commonest mistake in the pair. A checkbox is an answer collected now and submitted later, with the rest of the form; a switch is a setting that applies the moment it moves. Put a switch in a form with a Save button and the reader cannot tell whether anything happened - they flipped it, and nothing said so.",
974
1044
  "dependencies": [
975
1045
  "@base-ui/react",
976
- "dowel-ui@^0.20.1"
1046
+ "dowel-ui@^0.20.3"
977
1047
  ],
978
1048
  "registryDependencies": [],
979
1049
  "files": [
@@ -992,7 +1062,7 @@
992
1062
  "description": "Free text turned into a list: type a word, press Enter, it becomes a chip.",
993
1063
  "dependencies": [
994
1064
  "class-variance-authority",
995
- "dowel-ui@^0.20.1"
1065
+ "dowel-ui@^0.20.3"
996
1066
  ],
997
1067
  "registryDependencies": [
998
1068
  "https://lacodda.github.io/dowel/r/chip.json",
@@ -1013,7 +1083,7 @@
1013
1083
  "title": "Textarea",
1014
1084
  "description": "A multi-line field that can grow with what is typed into it, which is the only interesting part: a fixed box makes someone scroll inside a scroll, and a box that grows without limit pushes the button they are trying to reach off the screen. `autoResize` grows it; `maxRows` says when to stop and let it scroll after all.",
1015
1085
  "dependencies": [
1016
- "dowel-ui@^0.20.1"
1086
+ "dowel-ui@^0.20.3"
1017
1087
  ],
1018
1088
  "registryDependencies": [
1019
1089
  "https://lacodda.github.io/dowel/r/input.json"
@@ -1033,7 +1103,7 @@
1033
1103
  "title": "Time-field",
1034
1104
  "description": "No donor for this one: neither product of the line had a time field, so this is written from the same shape as DurationField, and for the same reason. Anything a person plausibly types is accepted - `9`, `9:30`, `930`, `9.30`, `9pm`, `21:30` - and what comes back is always `HH:MM`.",
1035
1105
  "dependencies": [
1036
- "dowel-ui@^0.20.1"
1106
+ "dowel-ui@^0.20.3"
1037
1107
  ],
1038
1108
  "registryDependencies": [
1039
1109
  "https://lacodda.github.io/dowel/r/input.json"
@@ -1055,7 +1125,7 @@
1055
1125
  "dependencies": [
1056
1126
  "@base-ui/react",
1057
1127
  "class-variance-authority",
1058
- "dowel-ui@^0.20.1"
1128
+ "dowel-ui@^0.20.3"
1059
1129
  ],
1060
1130
  "registryDependencies": [],
1061
1131
  "files": [
@@ -1075,7 +1145,7 @@
1075
1145
  "dependencies": [
1076
1146
  "@base-ui/react",
1077
1147
  "class-variance-authority",
1078
- "dowel-ui@^0.20.1"
1148
+ "dowel-ui@^0.20.3"
1079
1149
  ],
1080
1150
  "registryDependencies": [],
1081
1151
  "files": [
@@ -1093,7 +1163,7 @@
1093
1163
  "title": "Truncate",
1094
1164
  "description": "Text that does not fit, cut with an ellipsis - and, importantly, still readable in full: the element carries its own text as a `title`, so hovering shows what was cut. Every product wrote the one-line version of this and none of them remembered the title.",
1095
1165
  "dependencies": [
1096
- "dowel-ui@^0.20.1"
1166
+ "dowel-ui@^0.20.3"
1097
1167
  ],
1098
1168
  "registryDependencies": [],
1099
1169
  "files": [
package/dist/theme.css CHANGED
@@ -481,6 +481,11 @@ body {
481
481
  border-radius: 999px;
482
482
  background: var(--line-2);
483
483
  background-clip: content-box;
484
+ /* The thumb is drawn proportional to how much of the document fits on
485
+ * screen, so a long one collapses it to a few pixels: still visible, no
486
+ * longer catchable by a pointer. This is the floor below which it stops
487
+ * being a control. */
488
+ min-height: 2.5rem;
484
489
  }
485
490
 
486
491
  *::-webkit-scrollbar-thumb:hover {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dowel-ui",
3
- "version": "0.20.1",
3
+ "version": "0.20.3",
4
4
  "description": "The lacodda line design system: theme tokens and React primitives, distributed as a shadcn-compatible registry.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -56,16 +56,16 @@
56
56
  "prepack": "node ../../tools/copy-readme.mjs"
57
57
  },
58
58
  "peerDependencies": {
59
+ "eslint": "^9.0.0 || ^10.0.0",
59
60
  "react": "^19.0.0",
60
- "react-dom": "^19.0.0",
61
- "eslint": "^9.0.0 || ^10.0.0"
61
+ "react-dom": "^19.0.0"
62
62
  },
63
63
  "devDependencies": {
64
- "@types/node": "^26.4.0",
64
+ "@types/node": "^26.4.1",
65
65
  "@types/react": "^19.2.18",
66
- "@types/react-dom": "^19.2.5",
66
+ "@types/react-dom": "^19.2.7",
67
67
  "class-variance-authority": "^0.7.1",
68
- "eslint": "^10.9.1",
68
+ "eslint": "^10.10.0",
69
69
  "react": "^19.2.8",
70
70
  "react-dom": "^19.2.8"
71
71
  },