@apliteni/apliteni-ui 0.23.2 → 0.23.4

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
@@ -132,6 +132,12 @@ Each accent re-points only the accent family (`--accent`, `--purple*`, `--glow-p
132
132
  stay put — so **every accent works in both themes** and every component follows with no
133
133
  component-level change.
134
134
 
135
+ Both attributes are overrides, not requirements: with neither present the kit paints dark
136
+ Nebula, and `data-accent` alone paints that accent on the dark theme. An absent `data-theme`
137
+ is *not* "follow the system" — the kit ships no `prefers-color-scheme` rule, so a host that
138
+ wants the OS preference resolves it in JS and stamps the attribute. See
139
+ [`docs/library.md`](./docs/library.md#an-absent-attribute-means-dark).
140
+
135
141
  Shipped accents: **Nebula** (purple, default), **Phoenix** (ember), **Ocean** (azure),
136
142
  **Emerald** (jade). Runtime helpers: `applyTheme('light')` / `applyAccent('phoenix')`
137
143
  (both persist to `localStorage`); or the `accentPicker()` component wired by `wireTopbar()`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.23.2",
3
+ "version": "0.23.4",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
@@ -1,19 +1,12 @@
1
- // Overlay primitives — the page state the kit's modal surfaces share.
2
- //
3
- // Drawer and Confirm are the same problem twice: content over a scrim that owns
4
- // the keyboard until it is answered. Three of the questions they raise are
5
- // properties of the *page*, not of either component what is inert right now,
6
- // which overlay Escape talks to, and where Tab may go — and an overlay that
7
- // answers them from its own storage gets them wrong as soon as a second one is
8
- // open. So they are answered here, once, from one stack per document.
9
- //
10
- // Internal — not re-exported from src/index.js.
11
-
12
- // What each overlay paints on, as its stylesheet resolves it today: a drawer at
13
- // `--z-overlay` (src/styles/drawer.css) and a confirm one above it
14
- // (src/styles/confirm.css). Absolute values, not ranks, so a sheet that moves and
15
- // a table that did not is a failed test rather than a keyboard on the wrong layer
16
- // — stories/overlay-css.test.js holds these two numbers to those two rules.
1
+ // Overlay primitives — the page state the kit's modal surfaces share. Drawer and Confirm
2
+ // are the same problem twice: content over a scrim that owns the keyboard until it is
3
+ // answered. What is inert, which overlay Escape talks to and where Tab may go are
4
+ // properties of the *page*, so they are answered here from one stack per document rather
5
+ // than from either component's own storage. Internalnot re-exported from src/index.js.
6
+
7
+ // What each overlay paints on today: a drawer at `--z-overlay` (styles/drawer.css) and a
8
+ // confirm above it (styles/confirm.css). Absolute values, not ranks, so a sheet that moves
9
+ // and a table that did not is a failed test — stories/overlay-css.test.js holds both.
17
10
  export const OVERLAY_LAYER = { drawer: 100, confirm: 101 };
18
11
 
19
12
  const FOCUSABLE = [
@@ -33,10 +26,9 @@ function pageOf(doc) {
33
26
  return page;
34
27
  }
35
28
 
36
- // Focusable to the *browser*, not merely matching the selector. A control in a
37
- // closed overlay, an inert subtree or a hidden ancestor is skipped by the real
38
- // tab order, so a trap that counts it wraps at an element focus never reaches
39
- // and Tab walks straight out of the modal.
29
+ // Focusable to the *browser*, not merely matching the selector. A control in a closed
30
+ // overlay, an inert subtree or a hidden ancestor is skipped by the real tab order, so a
31
+ // trap that counts it wraps at an element focus never reaches.
40
32
  function reachable(el) {
41
33
  for (let n = el; n && n.nodeType === 1; n = n.parentElement) {
42
34
  if (n.inert || n.hasAttribute('inert') || n.hasAttribute('hidden')) return false;
@@ -53,10 +45,9 @@ export function focusablesIn(panel) {
53
45
  return Array.from(panel.querySelectorAll(FOCUSABLE)).filter(reachable);
54
46
  }
55
47
 
56
- // Hide everything *outside* `root` from AT + the tab order: walk root→body and
57
- // mark each ancestor's other children. The scrim and panel live inside the root
58
- // so they stay interactive; an overlay one layer down is outside it, so it does
59
- // not — a drawer under an aria-modal alertdialog must not be tabbable.
48
+ // Hide everything *outside* `root` from AT + the tab order: walk root→body and mark each
49
+ // ancestor's other children. Scrim and panel live inside the root so they stay
50
+ // interactive; a drawer under an aria-modal alertdialog is outside it and must not be.
60
51
  function mark(page, doc, root) {
61
52
  let node = root;
62
53
  while (node && node.parentElement && node !== doc.body) {
@@ -81,10 +72,9 @@ function unmark(page) {
81
72
  page.marked = [];
82
73
  }
83
74
 
84
- // Recompute the page from the stack — never replay a snapshot taken when an
85
- // overlay opened, because by the time it closes that snapshot describes a page
86
- // that has moved on. Roots torn down while open drop out here, so the record of
87
- // what to give back outlives the node that hid it.
75
+ // Recompute the page from the stack — never replay a snapshot taken when an overlay
76
+ // opened, because by the time it closes that snapshot describes a page that has moved on.
77
+ // Roots torn down while open drop out here.
88
78
  function sync(doc) {
89
79
  const page = pageOf(doc);
90
80
  for (let i = page.stack.length - 1; i >= 0; i--) {
@@ -128,12 +118,11 @@ function place(root, panel, dismiss, layer, where) {
128
118
  sync(doc);
129
119
  }
130
120
 
131
- // The layer this root actually paints on. In a browser the live z-index is the
132
- // truth, so a consumer who moves either overlay in their own stylesheet gets the
133
- // keyboard on the layer they can see. JSDOM has no cascade to resolve: it hands
134
- // back the declared text — `calc(var(--z-overlay) + 1)` with the kit's sheets,
135
- // `auto` with none — and neither is a number, so under test the passed constant
136
- // stands. `auto` must not read as 0; anything unparseable falls through.
121
+ // The layer this root actually paints on. In a browser the live z-index is the truth, so
122
+ // a consumer who moves an overlay in their own stylesheet gets the keyboard on the layer
123
+ // they can see. JSDOM hands back declared text rather than a number, so under test the
124
+ // passed constant stands — `auto` must not read as 0, and anything unparseable falls
125
+ // through.
137
126
  function paintedLayer(root, layer) {
138
127
  const painted = root.ownerDocument.defaultView?.getComputedStyle(root)?.zIndex;
139
128
  const live = painted ? Number(painted) : NaN;
@@ -152,16 +141,11 @@ export function pushOverlay(root, panel, dismiss, layer) {
152
141
  }
153
142
 
154
143
  /**
155
- * Take on a root that arrived already open — markup rendered with `open: true`,
156
- * which nobody called open…() for. Without this its aria-modal is a claim the
157
- * page contradicts.
158
- *
159
- * It goes in by PAINT ORDER, because wiring has no history to order by. Document
160
- * position only separates two overlays on the same layer, and there it is the
161
- * right answer rather than a fallback: at equal stack levels the later root
162
- * paints on top (CSS 2.2 Appendix E, steps 8 and 9).
163
- *
164
- * A root that renders closed is left alone — wiring is not an opening.
144
+ * Take on a root that arrived already open — markup rendered with `open: true`, which
145
+ * nobody called open…() for. Without this its aria-modal is a claim the page contradicts.
146
+ * It goes in by PAINT ORDER, because wiring has no history to order by; document position
147
+ * separates two overlays on the same layer, where the later root paints on top (CSS 2.2
148
+ * Appendix E, steps 8 and 9). A root that renders closed is left alone.
165
149
  */
166
150
  export function adoptOverlay(root, panel, dismiss, layer) {
167
151
  if (!root.classList.contains('is-open')) return;
@@ -10,9 +10,8 @@ import { sidebarNav, breadcrumbs } from './nav.js';
10
10
  import { prism } from '../assets/brand.js';
11
11
  import { ACCOUNT_NAV, toMenuTuple, initials } from './account-nav.js';
12
12
 
13
- // The one account navigation definition lives in account-nav.js, because
14
- // topbar.js needs it too and this file already imports topbar.js. Re-exported
15
- // here so the published name docs/library.md documents keeps working.
13
+ // The one account navigation definition lives in account-nav.js because topbar.js needs
14
+ // it too; re-exported here so the name docs/library.md publishes keeps working.
16
15
  export { ACCOUNT_NAV };
17
16
 
18
17
  const str = (v) => (v == null ? '' : String(v));
@@ -20,32 +19,24 @@ const isRecord = (v) => typeof v === 'object' && v !== null;
20
19
 
21
20
  // ---- the options bag, settled once --------------------------------------
22
21
  //
23
- // Every option below carries a shape, and a default parameter covers
24
- // `undefined` and nothing else so `nav: null` from an /auth/me, a `maxWidth`
25
- // out of tenant config, a `crumbs` string written by somebody reading the
26
- // migration note all arrived as they were. A shell that throws mid-render takes
27
- // the page with it, so each is settled here, before the first sink sees it, and
28
- // a parameter is protected by being declared rather than by what it crashes
29
- // into. Adding one to appShell() means adding it to SHAPES or deciding in the
30
- // open that it needs nothing.
31
-
32
- // accountShell() has always taken its nav as [id, icon, label, href?, target?].
33
- // Accept that shape and nav.js's object shape side by side, so a consumer's
34
- // existing tuples and the exported ACCOUNT_NAV both work. A nav that is not a
35
- // list at all falls back to the default; an entry inside one that is neither
36
- // shape is dropped, because sideItem() reads `.items` off whatever it is given.
37
- // An empty list is an answer, not a mistake — it stays empty.
22
+ // A default parameter covers `undefined` and nothing else, so `nav: null` from an
23
+ // /auth/me and a `crumbs` string from somebody reading the migration note both arrived
24
+ // as they were. A shell that throws mid-render takes the page with it, so each option is
25
+ // settled here, before the first sink sees it. Adding one to appShell() means adding it
26
+ // to SHAPES or deciding in the open that it needs nothing.
27
+
28
+ // accountShell()'s tuple nav [id, icon, label, href?, target?] and nav.js's object
29
+ // shape are accepted side by side. A nav that is not a list falls back to the default;
30
+ // an entry that is neither shape is dropped. An empty list is an answer and stays.
38
31
  const toItems = (nav) => (Array.isArray(nav) ? nav : ACCOUNT_NAV)
39
32
  .filter(isRecord)
40
33
  .map((n) => (Array.isArray(n)
41
34
  ? { id: n[0], icon: n[1], label: n[2], href: n[3], target: n[4] }
42
35
  : n));
43
36
 
44
- // The trail is the caller's, and `crumbs` is the one option whose shape changed
45
- // in this release: the old API was `crumb: 'Payouts'`, a string, and the new one
46
- // is `crumbs: [{ label }]`. One letter apart. A value that is not a list is not
47
- // read as a one-crumb trail — that would draw a plausible page and hide the
48
- // migration mistake — so it is no trail at all, which is the visible answer. A
37
+ // The trail is the caller's, and `crumbs` changed shape this release: `crumb: 'Payouts'`
38
+ // became `crumbs: [{ label }]`, one letter apart. A non-list is NOT read as a one-crumb
39
+ // trail that hides the migration mistake so it is no trail at all, which shows. A
49
40
  // crumb with no label would draw an empty cell, so it goes too.
50
41
  const toCrumbs = (crumbs) => (Array.isArray(crumbs) ? crumbs : [])
51
42
  .filter((c) => isRecord(c) && !Array.isArray(c) && str(c.label) !== '');
@@ -56,23 +47,16 @@ const toReader = (a) => (isRecord(a) ? { name: str(a.name), email: str(a.email)
56
47
 
57
48
  // ---- the topbar, which interpolates where the rail escapes ----------------
58
49
  //
59
- // brand() writes `word` straight into its markup and accountMenu() does the
60
- // same with the reader's name, address and menu entries. So the escaping is
61
- // here, on the one path into productTopbar(), and not in each caller:
62
- // accountShell() escaped for itself and the `topbar` option underneath it
63
- // escaped for nobody. The caller passes text either way.
64
-
65
- // The reader the menu draws. Both fields are always written, empty when the
66
- // caller gave noneaccountMenu()'s own defaults are a demo identity, and a
67
- // key dropped here is a key its default fills in, which is how a consumer's
68
- // page came to name its own reader in the rail and the kit's fixture beside it.
69
- //
70
- // `initials` travels with them because it is derived, and a derived value comes
71
- // from what the caller passed, not from the entities made of it: `<Ada>` and
72
- // `&lt;Ada&gt;` do not start with the same character. The entries land in the
73
- // same sink and toMenuTuple() is what escapes them; it reads item objects, so
74
- // tuples go through toItems() first. A nav nobody passed stays unpassed, and
75
- // accountMenu() falls back to its own derived default rather than to nothing.
50
+ // brand() writes `word` straight into its markup and accountMenu() does the same with
51
+ // the reader's name, address and menu entries, so the escaping is here on the one path
52
+ // into productTopbar() rather than in each caller. The caller passes text either way.
53
+
54
+ // The reader the menu draws. Both fields are always written, empty when the caller gave
55
+ // none: accountMenu()'s defaults are a demo identity, so a key dropped here is a key its
56
+ // fixture fills in. `initials` is derived from what the caller passed rather than from
57
+ // the entities made of it `<Ada>` and `&lt;Ada&gt;` do not start with the same
58
+ // character. toMenuTuple() escapes the entries and reads item objects, so tuples go
59
+ // through toItems() first; a nav nobody passed stays unpassed.
76
60
  const toMenuReader = (a) => {
77
61
  const { name, email } = toReader(a);
78
62
  const rest = isRecord(a) && !Array.isArray(a) ? a : {};
@@ -89,12 +73,11 @@ const toTopbar = (t) => {
89
73
  return out;
90
74
  };
91
75
 
92
- // `maxWidth` lands inside a style attribute, so a length is all this accepts —
93
- // a number and a unit, or `none`. Anything else yields '' and the caller writes
94
- // no style attribute, letting layout.css fall through to --measure. It must
95
- // REMOVE the property rather than pass a default or a bad value on: a custom
96
- // property accepts any token stream, so garbage is a valid declaration that
97
- // drops the column to `none`, the full track.
76
+ // `maxWidth` lands inside a style attribute, so a length is all this accepts — a number
77
+ // and a unit, or `none`. Anything else yields '' and the caller writes no style
78
+ // attribute, falling through to --measure. It must REMOVE the property rather than pass
79
+ // a bad value on: a custom property accepts any token stream, so garbage is a valid
80
+ // declaration that drops the column to `none`, the full track.
98
81
  // why: docs/specification.md#widths
99
82
  const LENGTH = /^(?:\d+|\d*\.\d+)(?:px|rem|em|ch|%|vw)$/;
100
83
  const mainMax = (v) => {
@@ -108,11 +91,9 @@ const SHAPES = {
108
91
  nav: toItems, crumbs: toCrumbs, account: toReader, maxWidth: mainMax, topbar: toTopbar,
109
92
  };
110
93
 
111
- // The text options settle too, by the same argument the rest of them do. A
112
- // default parameter covers `undefined` and not `null`, so `body: null` from a
113
- // record with no description drew the word "null" on the page, and `word: null`
114
- // left the brand link with no accessible name at all. Dropping the key is what
115
- // lets the declared default apply.
94
+ // The text options settle by the same argument. `body: null` from a record with no
95
+ // description drew the word "null" on the page and `word: null` left the brand link with
96
+ // no accessible name; dropping the key is what lets the declared default apply.
116
97
  const TEXT = ['word', 'brandHref', 'navLabel', 'title', 'sub', 'body', 'signOutHref', 'active'];
117
98
 
118
99
  function settle(options) {
@@ -122,23 +103,18 @@ function settle(options) {
122
103
  return out;
123
104
  }
124
105
 
125
- // Signing out is a navigation action, so it belongs in the rail nav's footer
126
- // slot. Opt-in: a shell that renders it unasked puts a dead link on a page with
127
- // no session behind it.
106
+ // Signing out is a navigation action, so it belongs in the rail nav's footer slot.
107
+ // Opt-in: rendering it unasked puts a dead link on a page with no session behind it.
128
108
  const signOut = (href) =>
129
109
  `<a class="ui-nav__item is-danger" href="${esc(href)}" aria-label="Sign out">` +
130
110
  `<span class="ui-nav__ic">${icon('logout')}</span>` +
131
111
  `<span class="ui-nav__label">Sign out</span></a>`;
132
112
 
133
- // Who is signed in. A sibling of the <nav>, not its footer: a reader's name and
134
- // address are not navigation, and inside the landmark a screen reader announces
135
- // the address as an entry. Empty when nobody is a shell must not invent an
136
- // identity for a reader it does not know.
137
- //
138
- // The initials carry the name, and the spelled-out half is aria-hidden. The
139
- // narrow rail folds `.ui-app__who` out of view, so a name that lived only there
140
- // left the initials on screen with nothing at all in the accessibility tree.
141
- // Naming the mark instead makes the two agree at every width, and says it once.
113
+ // Who is signed in. A sibling of the <nav>, not its footer: a name and address are not
114
+ // navigation, and inside the landmark a screen reader announces the address as an entry.
115
+ // Empty when nobody is. The initials carry the name and the spelled-out half is
116
+ // aria-hidden, because the narrow rail folds `.ui-app__who` out of view and a name that
117
+ // lived only there left nothing in the accessibility tree.
142
118
  function railUser({ name, email }) {
143
119
  if (!name && !email) return '';
144
120
  const who = [name, email].filter(Boolean).join(', ');
@@ -179,16 +155,14 @@ export function appShell(options = {}) {
179
155
  ariaLabel: navLabel,
180
156
  footer: signOutHref ? signOut(signOutHref) : '',
181
157
  });
182
- // The topbar already says the product word. Two lockups on one screen is one
183
- // product word too many, so the rail head steps aside when there is a topbar.
184
- // The word is the link's only text and the narrow rail folds it out of view,
185
- // so the name is written out — the mark itself is aria-hidden.
158
+ // The topbar already says the product word, so the rail head steps aside when there is
159
+ // one. The word is the link's only text and the narrow rail folds it out of view, so
160
+ // the name is written out the mark itself is aria-hidden.
186
161
  const brand = topbar ? '' : `<a class="ui-app__brand" href="${esc(brandHref)}" aria-label="${esc(word)}">`
187
162
  + `${prism(`appb-${++_shellUid}`, 24)}<span>${esc(word)}</span></a>`;
188
- // A <div>, not an <aside>: <aside> is the `complementary` landmark content
189
- // related to the page but separable from it and this holds the page's
190
- // primary navigation and the reader who is signed in. The <nav> inside it is
191
- // already the landmark that names the menu.
163
+ // A <div>, not an <aside>: <aside> is the `complementary` landmark, and this holds the
164
+ // page's primary navigation and the signed-in reader. The <nav> inside it is already
165
+ // the landmark that names the menu.
192
166
  const grid = `<div class="ui-app">
193
167
  <div class="ui-app__rail">
194
168
  ${brand}
@@ -225,11 +199,9 @@ export function accountShell({
225
199
  // topbar menu are handed one list rather than two readings of `nav`.
226
200
  const items = toItems(nav);
227
201
  const trail = [{ label: cap }, { label: crumb || title }];
228
- // The preset hands the topbar the caller's text, exactly as it hands the rail
229
- // the caller's text. toTopbar() is what escapes for the menu's raw sinks, and
230
- // it runs once inside appShell() — escaping here as well would reach the menu
231
- // as entities, and escaping nowhere is what the public `topbar` option used
232
- // to do.
202
+ // The preset hands the topbar the caller's text, as it does the rail. toTopbar()
203
+ // escapes for the menu's raw sinks and runs once inside appShell(); escaping here as
204
+ // well would reach the menu as entities.
233
205
  return appShell({
234
206
  word,
235
207
  nav: items,
@@ -16,7 +16,7 @@ html {
16
16
 
17
17
  body {
18
18
  background: var(--bg);
19
- color: var(--ink);
19
+ color: var(--text);
20
20
  font-family: var(--font-sans);
21
21
  font-weight: var(--weight-normal);
22
22
  font-size: var(--text-base);
@@ -34,14 +34,10 @@
34
34
  .ui-table tbody tr { transition: background var(--dur-fast) var(--ease); }
35
35
 
36
36
  /* Row hover: an inset, rounded fill so the highlight never sits flush against a
37
- * container's border (issue #71 the full-bleed rectangle collided with a card's
38
- * left edge). Separate border model with zero spacing keeps the row separators
39
- * continuous while letting the row ends round; table padding (which only applies
40
- * in the separate model) insets the rows so the fill clears the container edge.
41
- * Zebra tables keep the original full-bleed tint (finance ledger), so they're
42
- * excluded here — their hover rule lives further down.
43
- * The inset was 6px, which is a tie between --space-1 and --space-2; it rounds
44
- * up, because what it buys is clearance and clearance rounds away from the edge. */
37
+ * container's border (#71). The separate border model with zero spacing keeps the row
38
+ * separators continuous while letting the row ends round, and table padding — which
39
+ * only applies in that model insets the rows. Zebra keeps the original full-bleed
40
+ * tint (finance ledger) and is excluded here; its hover rule is below. */
45
41
  .ui-table--hover:not(.ui-table--zebra) {
46
42
  border-collapse: separate;
47
43
  border-spacing: 0;
@@ -54,9 +50,7 @@
54
50
  border-top-right-radius: var(--radius-sm); border-bottom-right-radius: var(--radius-sm); }
55
51
 
56
52
  /* Data-table modifiers, composable: .ui-table--dense --zebra --hover.
57
- * --dense --space-3 across, --space-2 down — one step in from the base
58
- * table, and both round DOWN, because the modifier exists to fit
59
- * more rows and rounding up spends the distinction it is for.
53
+ * --dense --space-3 across, --space-2 down — one step in from the base table
60
54
  * --zebra row striping; 2px header rule; the hover tint out-ranks the stripe
61
55
  * Cell helpers: .ui-table__num (right, tabular), .ui-table__code.
62
56
  * why: docs/specification.md#spacing-and-rhythm
@@ -71,15 +65,11 @@
71
65
  .ui-table--zebra th { border-bottom-width: 2px; }
72
66
  .ui-table--zebra td { border-bottom: 0; }
73
67
  .ui-table--zebra tbody tr:nth-child(even) td { background: var(--surface-2); }
74
- /* The stripe is full-bleed on purpose the hover note above inset the hover fill for
75
- * #71 and deliberately left the ledger stripe alone. But full-bleed only reads right if
76
- * the end cells are inset from the tint's own edge, and the base rhythm does not inset
77
- * them: `.ui-table td` sets `padding-left: 0`. Only `--dense` supplied the inset, so a
78
- * `--zebra` table composed WITHOUT `--dense` put the first cell's content at exactly
79
- * x=0 of the stripe. `DataTable` is that composition — it writes `ui-table
80
- * ui-table--hover ui-table--zebra` and no `--dense` — so its select-all and per-row
81
- * checkboxes sat flush against the tint with a measured 0px gap.
82
- * Header and body are inset together, or the columns stop lining up. */
68
+ /* The stripe stays full-bleed (the #71 inset above is hover only), but it only reads right
69
+ * if the end cells are inset from the tint's own edge, and `.ui-table td` sets
70
+ * `padding-left: 0`. Only `--dense` supplied that inset, so `--zebra` without `--dense`
71
+ * `DataTable`'s composition sat its checkboxes at x=0 of the stripe. Head and body are
72
+ * inset together, or the columns stop lining up. */
83
73
  .ui-table--zebra th:first-child,
84
74
  .ui-table--zebra td:first-child { padding-left: var(--space-3); }
85
75
  .ui-table--zebra th:last-child,
@@ -1,7 +1,8 @@
1
1
  /* Accent sub-themes — an orthogonal `data-accent` dimension on top of
2
- * `data-theme`. Set both on <html>:
2
+ * `data-theme`. On <html>, with or without a theme:
3
3
  *
4
4
  * <html data-theme="dark" data-accent="phoenix">
5
+ * <html data-accent="phoenix"> <!-- default theme, same accent -->
5
6
  *
6
7
  * Each sub-theme only re-points the accent family. Surfaces, text and signal
7
8
  * colours (green = live, pink = danger) stay put, so every accent works in
@@ -10,12 +11,24 @@
10
11
  * --ring is NOT here: it is declared once in tokens.css as var(--accent), so
11
12
  * re-pointing --accent re-points the focus ring too.
12
13
  *
13
- * Selectors carry two attributes + :root, so they always out-specify the
14
- * single-attribute theme blocks in tokens.css regardless of import order.
14
+ * Every dark cell is a two-line selector list a bare `:root[data-accent]`
15
+ * above the stamped form — which is the shape tokens.css gives the dark theme
16
+ * one file earlier, and for the same reason: the default theme needs no
17
+ * attribute, so neither may the accent. Written stamped-only, `data-accent`
18
+ * paints nothing at all in the state hosts use to mean "follow the system",
19
+ * where data-theme is simply absent. See issue #250.
20
+ *
21
+ * That bare line ties on specificity with :root[data-theme="light"] and wins on
22
+ * import order, so LIGHT is what the cascade has to be careful about: each light
23
+ * cell carries three attributes to out-specify it, and declares the same
24
+ * properties as its dark twin so none of the dark ramp leaks past it. Adding a
25
+ * property to one half of a pair and not the other is what breaks that, and
26
+ * stories/accent-without-theme.test.js is the gate that says so.
15
27
  *
16
28
  * why: docs/specification.md#the-focus-ring */
17
29
 
18
30
  /* ---- Phoenix — ember / rising fire (the strategy's namesake) ------------- */
31
+ :root[data-accent="phoenix"],
19
32
  :root[data-theme="dark"][data-accent="phoenix"] {
20
33
  --purple: #e0531f;
21
34
  --purple-light: #ff8a5c;
@@ -45,6 +58,7 @@
45
58
  }
46
59
 
47
60
  /* ---- Ocean — azure ------------------------------------------------------- */
61
+ :root[data-accent="ocean"],
48
62
  :root[data-theme="dark"][data-accent="ocean"] {
49
63
  --purple: #1f7fe0;
50
64
  --purple-light: #5ab0ff;
@@ -62,31 +76,22 @@
62
76
  --purple: #1156b8;
63
77
  --purple-light: #1568d6;
64
78
  --purple-mid: #1560c8;
65
- /* Deepened for WCAG AA on the raised grey panel and its wash: #1568d6 read
66
- 4.54:1 on --surface-3 and 3.98:1 on --glow-purple over it; #005bc8 clears
67
- both at 5.43:1 and 4.76:1. Both washed figures are at the 0.10 alpha below,
68
- which is what this cell ships — an earlier version of this comment quoted
69
- them at the 0.05 the wash briefly carried, which flattered both inks.
70
- Light Ocean is the one light accent #96 never deepened, because it happened
71
- to clear plain white. Same kind of move, smaller: hue and chroma unchanged
72
- in OKLCH (H 258, C 0.185) and four points of lightness down, where #96's
73
- two took about ten points and gave up 15.9% of the chroma on Phoenix and
74
- 17.6% on Emerald. See issues #96 and #157. */
79
+ /* Deepened for WCAG AA on the raised grey panel and its wash hue and chroma held
80
+ in OKLCH (H 258, C 0.185), four points of lightness down. The washed figures are
81
+ read at the 0.10 alpha below, which is what this cell ships. See issues #96, #157. */
75
82
  --accent: #005bc8;
76
83
  --accent-strong: var(--accent);
77
84
  --accent-contrast: #ffffff;
78
- /* Back to 0.10, the alpha this cell carried before #157. #157 thinned it to
79
- 0.05 while --accent was still #1568d6, because no alpha could carry that
80
- blue over the grey panel. Deepening the ink is what closed the pair, so the
81
- wash does not have to be the thing that gives. The rgb stays --purple-light
82
- rather than the accent — in light a glow tints --purple-light, which
83
- stories/signal-contrast.test.js holds. See issue #157. */
85
+ /* Back to the 0.10 this cell carried before #157: deepening the ink closed the pair,
86
+ so the wash does not have to give. The rgb stays --purple-light rather than the
87
+ accent in light a glow tints --purple-light, which signal-contrast.test.js holds. */
84
88
  --glow-purple: rgba(21, 104, 214, 0.10);
85
89
  --grad-from: #1568d6;
86
90
  --grad-to: #17a2c0;
87
91
  }
88
92
 
89
93
  /* ---- Emerald — jade ------------------------------------------------------ */
94
+ :root[data-accent="emerald"],
90
95
  :root[data-theme="dark"][data-accent="emerald"] {
91
96
  --purple: #0fa876;
92
97
  --purple-light: #3ad9a0;
@@ -1,14 +1,8 @@
1
1
  /* ============================================================================
2
- * apliteni-ui — design tokens
3
- * Single source of truth for the Apliteni deck theme.
4
- *
5
- * Colour semantics are identical to viz/ (deck index.html, account.mjs):
6
- * purple accent, near-black surfaces, green = live, cyan = link/flag.
7
- * Two themes: dark (default) and light. Toggle with `data-theme` on <html>.
8
- *
9
- * Everything downstream references the SEMANTIC tokens (--bg, --surface,
10
- * --accent, --text…), never the raw ramp. Swap a ramp value once, and every
11
- * component follows.
2
+ * apliteni-ui — design tokens. Single source of truth for the Apliteni deck theme,
3
+ * with viz/'s colour semantics: purple accent, near-black surfaces, green = live,
4
+ * cyan = link/flag. Two themes, dark (default) and light, toggled with `data-theme`
5
+ * on <html>. Everything downstream references the SEMANTIC tokens, never the ramp.
12
6
  * ========================================================================== */
13
7
 
14
8
  :root {
@@ -44,7 +38,6 @@
44
38
  /* -- Measure ------------------------------------------------------------
45
39
  * --container is the page, gutter to gutter; --measure is the reading column
46
40
  * INSIDE a track, beside a sidebar. Different axes, never interchangeable.
47
- * Below --measure are --panel-* and --prose-*, at the end of this file (#208).
48
41
  * why: docs/specification.md#widths */
49
42
  --container: 1120px;
50
43
  --measure: 860px;
@@ -79,13 +72,12 @@
79
72
  --tracking-caps: 0.14em;
80
73
 
81
74
  /* -- Motion -------------------------------------------------------------
82
- * Durations + easings are the Apliteni brand's motion vocabulary, synced
83
- * from design-system into brand.generated.css (--duration-*, --easing-*).
84
- * The kit's legacy --dur-* and --ease names alias onto them, so every existing
85
- * component transitively consumes the brand primitives one vocabulary,
86
- * zero drift. Fallbacks keep tokens.css self-sufficient when imported
87
- * without brand.generated.css. Delays are the kit's own (not a brand
88
- * concern). The reusable effect layer lives in styles/motion.css. */
75
+ * The kit's legacy --dur-* and --ease names alias onto the brand's
76
+ * --duration-* and --easing-* primitives, so every component consumes one
77
+ * vocabulary; the fallbacks keep tokens.css self-sufficient when imported
78
+ * without brand.generated.css. Delays are the kit's own, and the reusable
79
+ * effect layer lives in styles/motion.css.
80
+ * why: CONTRIBUTING.md#brand-tokens-synced-from-design-system */
89
81
  --ease: var(--easing-ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
90
82
  --ease-out: var(--easing-ease-out, cubic-bezier(0, 0, 0.2, 1));
91
83
  --ease-in: var(--easing-ease-in, cubic-bezier(0.4, 0, 1, 1));
@@ -119,22 +111,16 @@
119
111
  color-scheme: dark;
120
112
 
121
113
  --purple: #6a2dcc;
122
- /* The ramp moves up one step so --accent is --purple-light again, which is
123
- the shape light Nebula and light Ocean already ship and which dark Nebula
124
- held until #157 lifted the accent onto the old --purple-mid. Left alone,
125
- the two tokens would resolve to one colour and the ramp would carry a
126
- duplicate step. #bd8cff is the value docs/specification.md#colour-and-contrast reasons about and
127
- rejects as the accent; it becomes the ramp's top step instead. Light
128
- Nebula's ramp does not move. See issue #157. */
114
+ /* The ramp moves up one step so --accent is --purple-light again, the shape light
115
+ Nebula and light Ocean already ship; left alone the two tokens would resolve to
116
+ one colour. #bd8cff becomes the ramp's top step instead. See issue #157. */
129
117
  --purple-light: #b479ff;
130
118
  --purple-mid: #bd8cff;
131
119
  --cyan: #20dcf5;
132
120
  --green: #98ff8f;
133
- /* Lighter than it looks like it should be, and deliberately so: --pink is read
134
- mostly ON its own 16% glow, and that wash lifts the ground faster than the
135
- hue lifts the ink. #e35b8f measured 3.82:1 on --glow-pink over a card
136
- under AA — so danger moved AWAY from the canvas, not toward it. Light moves
137
- the opposite way for the same reason. See issue #131. */
121
+ /* Lighter than it looks like it should be: --pink is read mostly ON its own 16%
122
+ glow, and that wash lifts the ground faster than the hue lifts the ink, so danger
123
+ moved AWAY from the canvas. Light moves the opposite way. See issue #131. */
138
124
  --pink: #e97ca5;
139
125
  --amber: #ffcf6a;
140
126
 
@@ -144,9 +130,8 @@
144
130
  --surface-2: #1b1927;
145
131
  --surface-3: #2a2739;
146
132
  --seg-active-bg: #34314a; /* raised pill — clearly above the inset track in dark */
147
- /* The raised handle on a control track (switch knob). Stays white in both
148
- themes and under every accent: it has to read on the plain track AND on
149
- the accent fill the track takes when the control is on. */
133
+ /* The raised handle on a control track (switch knob). White in both themes and under
134
+ every accent: it reads on the plain track AND on the accent fill of an on control. */
150
135
  --control-knob: #ffffff;
151
136
  --border: #332f45;
152
137
  --border-strong: #453f5c;
@@ -156,53 +141,39 @@
156
141
  --dim: #c6c2d6;
157
142
  --muted: #948fa8;
158
143
 
159
- /* The paint of a control that is off. NOT an opacity: `opacity` is a group
160
- property, so it fades a label and the box under it toward the ground
161
- TOGETHER and the pair a reader is left with is whatever that composite
162
- lands on a disabled primary button measured 1.48:1 that way, white on a
163
- faded accent. These three composite predictably instead: the ink is read on
164
- the surface beside it, and both are opaque. They are aliases, not new
165
- colours — the ramp gains nothing to keep in sync. A disabled control drops
166
- its accent, which is most of what makes it read as inert; the ink going
167
- quiet is the rest. Held by stories/guidelines/accessibility-floor.test.js.
144
+ /* The paint of a control that is off. NOT an opacity: `opacity` is a group property,
145
+ so it fades label and box toward the ground TOGETHER a disabled primary button
146
+ measured 1.48:1 that way. These three are aliases that composite predictably, the
147
+ ink read on the surface beside it. Held by guidelines/accessibility-floor.test.js.
168
148
  why: docs/specification.md#colour-and-contrast */
169
149
  --disabled-ink: var(--muted);
170
150
  --disabled-surface: var(--surface-2);
171
151
  --disabled-border: var(--border);
172
152
 
173
- /* Lifted from #9b5dff, which cleared the flat surfaces and failed on its own
174
- wash over a card. This value was already in the ramp as --purple-mid, so no
175
- new colour entered it; the ramp then moved up a step above so --accent is
176
- --purple-light, which is how every other Nebula and Ocean cell is written.
177
- --glow-purple follows it down the file; --ring is written as var(--accent)
178
- and so follows it by reference. Held by
179
- stories/accent-contrast.test.js. See issue #157.
153
+ /* Lifted from #9b5dff, which cleared the flat surfaces and failed on its own wash over
154
+ a card; the value was already in the ramp. --ring is var(--accent) and follows by
155
+ reference. Held by stories/accent-contrast.test.js. #157.
180
156
  why: docs/specification.md#colour-and-contrast */
181
157
  --accent: #b479ff;
182
158
  --accent-strong: #7c3aed; /* button bg — white text clears WCAG AA at 5.70:1 */
183
159
  --accent-contrast: #ffffff;
184
- --ink: #e9e7f0;
185
160
 
186
- /* Ink that reads on a SIGNAL colour once that colour becomes a fill — the
187
- glyph in a success circle, the glyph on a toast's status circle. One
188
- near-black ink clears all five in dark; danger keeps a token of its own
189
- because LIGHT still needs one.
161
+ /* Ink that reads on a SIGNAL colour once that colour becomes a fill — the glyph in a
162
+ success circle, or on a toast's status circle. One near-black ink clears all five in
163
+ dark; danger keeps its own token because LIGHT still needs one.
190
164
  why: docs/specification.md#colour-and-contrast */
191
165
  --signal-contrast: #0c0c0c;
192
166
  --danger-contrast: var(--signal-contrast);
193
167
 
194
168
  --glow-green: rgba(152, 255, 143, 0.16);
195
- /* Dropped from 0.18: the wash, not the surface beneath it, is what put --accent
196
- under AA it lifts the ground toward the ink faster than the ink lifts off
197
- it. Re-tinted from the new --accent, which signal-contrast.test.js requires.
198
- See issue #157. */
169
+ /* Dropped from 0.18: the wash, not the surface beneath it, is what put --accent under
170
+ AA. Re-tinted from the new --accent, which signal-contrast.test.js requires. #157. */
199
171
  --glow-purple: rgba(180, 121, 255, 0.12);
200
172
  --glow-cyan: rgba(32, 220, 245, 0.14);
201
173
  --glow-pink: rgba(233, 124, 165, 0.16);
202
174
 
203
- /* Status-chip paint — the ink + fill of a badge / pill. Dark tints with the
204
- signal's own glow; light needs a solid, deepened pair (the glows wash out
205
- on white), which is why these are a token pair and not just --green etc. */
175
+ /* Status-chip paint — the ink + fill of a badge / pill. Dark tints with the signal's
176
+ own glow; light needs a solid, deepened pair, which is why these are a token pair. */
206
177
  --chip-success-ink: var(--green);
207
178
  --chip-success-fill: var(--glow-green);
208
179
  --chip-warn-ink: var(--amber);
@@ -212,14 +183,10 @@
212
183
  --chip-info-ink: var(--cyan);
213
184
  --chip-info-fill: var(--glow-cyan);
214
185
 
215
- /* Solid signal surfaces — a status that has stopped being ink and become the
216
- FILL (a solid toast). The fill and its ink are ONE pair, never two
217
- independent picks: the fill is the status at this theme's extreme, and the
218
- ink is the pole opposite it, so a single ink clears all five. Dark fills are
219
- the bright signals, so the ink is near-black — it measures 15.87 on --green,
220
- 7.35 on --pink, 13.40 on --amber, 11.75 on --cyan and 6.29 on --muted.
221
- Neutral needs no chip pair to take part: --muted is already its
222
- fill-strength form. See issues #131 and #149. */
186
+ /* Solid signal surfaces — a status that has stopped being ink and become the FILL (a
187
+ solid toast). Fill and ink are ONE pair, never two independent picks: the fill is the
188
+ status at this theme's extreme and the ink the pole opposite it, so a single ink
189
+ clears all five. --muted is already its own fill-strength form. See #131, #149. */
223
190
  --signal-solid-ink: var(--signal-contrast);
224
191
  --signal-solid-success: var(--green);
225
192
  --signal-solid-danger: var(--pink);
@@ -232,19 +199,18 @@
232
199
  --shadow-lg: 0 18px 50px rgba(0, 0, 0, 0.5);
233
200
  /* Tight elevation for the active segmented pill — stays contained in its track. */
234
201
  --shadow-seg: 0 1px 2px rgba(0, 0, 0, 0.45), 0 2px 4px rgba(0, 0, 0, 0.3);
235
- /* Resting elevation for a card. Dark cards are borderless — the surface does
236
- the grouping on its own — so they cast nothing. */
202
+ /* Resting elevation for a card. Dark cards are borderless — the surface groups on
203
+ its own — so they cast nothing. */
237
204
  --shadow-card: none;
238
- /* THE ONLY --ring in the kit — light and every data-accent sub-theme declare
239
- an --accent and inherit this line. Opaque because a translucent ring is a
240
- glow: the eight rgba() literals this replaced all missed the 3:1 of WCAG
241
- 1.4.11, at 1.35:1 worst. Held at 4.22:1 worst by
242
- stories/guidelines/accessibility-floor.test.js. See issue #218.
205
+ /* THE ONLY --ring in the kit — light and every data-accent sub-theme declare an
206
+ --accent and inherit this line. Opaque because a translucent ring is a glow, and the
207
+ eight rgba() literals it replaced all missed WCAG 1.4.11's 3:1 (#218). Held by
208
+ stories/guidelines/accessibility-floor.test.js.
243
209
  why: docs/specification.md#the-focus-ring */
244
210
  --ring: 0 0 0 3px var(--accent);
245
211
 
246
- /* The ink a cast shadow is made of, and the specular white that reads as
247
- gloss on a raised surface. Effects mix these instead of writing rgba(). */
212
+ /* The ink a cast shadow is made of, and the specular white that reads as gloss on a
213
+ raised surface. Effects mix these instead of writing rgba(). */
248
214
  --shadow-ink: #000000;
249
215
  --sheen: #ffffff;
250
216
 
@@ -252,10 +218,8 @@
252
218
  --scrim: rgba(0, 0, 0, 0.58);
253
219
 
254
220
  /* Accent gradient stops (hero headline, marketing) — overridden per sub-theme.
255
- --grad-from is pinned to the literal rather than tracking --purple-light,
256
- which #157 moved up a step: following it would lighten the hero gradient's
257
- first stop, a visible change nobody asked for. Every --grad-from in
258
- src/tokens/accents.css is already written as a literal for the same reason. */
221
+ --grad-from is pinned to a literal rather than tracking --purple-light, which #157
222
+ moved up a step; every --grad-from in accents.css is a literal for the same reason. */
259
223
  --grad-from: #9b5dff;
260
224
  --grad-to: var(--cyan);
261
225
  }
@@ -271,15 +235,13 @@
271
235
  --purple-mid: #7a3be0;
272
236
  --cyan: #0c8fa8;
273
237
  --green: #1c8a2c;
274
- /* Deeper than the dark pink for the same reason dark's is lighter: the wash
275
- under it is white, so the ink has to come down to meet it. #d63c72 measured
276
- 3.87:1 on --glow-pink over white and 4.42:1 on plain white — the required
277
- marker and the form error missed AA on nothing at all. See issue #131. */
238
+ /* Deeper than the dark pink for the same reason dark's is lighter: the wash under it
239
+ is white, so the ink has to come down to meet it. See issue #131. */
278
240
  --pink: #b63361;
279
241
  --amber: #b5730a;
280
242
 
281
- /* A WHITE app: the page and cards are both white; cards are delineated by a
282
- hairline border + soft shadow (see card.css), not a grey canvas. */
243
+ /* A WHITE app: page and cards are both white, and cards are delineated by a hairline
244
+ border + soft shadow (see card.css), not a grey canvas. */
283
245
  --bg: #ffffff;
284
246
  --bg-elevated: #ffffff;
285
247
  --surface: #ffffff;
@@ -295,9 +257,8 @@
295
257
  --dim: #3a404b;
296
258
  --muted: #5c6270;
297
259
 
298
- /* Same three aliases as dark, same reasoning. Light moves the disabled
299
- surface AWAY from white for the same reason dark moves it away from the
300
- card: a control that is off should sink, not rise. */
260
+ /* Same three aliases as dark. Light moves the disabled surface AWAY from white for the
261
+ same reason dark moves it away from the card: a control that is off should sink. */
301
262
  --disabled-ink: var(--muted);
302
263
  --disabled-surface: var(--surface-2);
303
264
  --disabled-border: var(--border);
@@ -305,50 +266,37 @@
305
266
  --accent: #6a2dcc;
306
267
  --accent-strong: #6a2dcc; /* already dark enough for white text */
307
268
  --accent-contrast: #ffffff;
308
- --ink: #1e232b;
309
269
 
310
270
  --signal-contrast: #0c0c0c;
311
271
  --danger-contrast: #ffffff;
312
272
 
313
- /* Re-tinted from --green. It was rgba(30, 150, 50, .1) a colour that exists
314
- nowhere else in the kit, carried unedited since the initial commit while the
315
- other three light glows are exact tints of their own token. The wash over
316
- white moves #e9f5eb -> #e8f3ea, at most two levels on a channel, which is
317
- 0.86 of a deltaE — under the 1.0 that is normally called imperceptible. No
318
- verdict moves with it: body copy on the success wash goes 9.29 -> 9.15
319
- against the 4.5 AA bar, and the --green icon on it 3.97 -> 3.91 against the
320
- 3.0 bar WCAG 1.4.11 sets for a graphic. The success chips are untouched —
321
- light's --chip-success-fill is the solid #dff3e4, not this. Gated for all
322
- four glows in stories/signal-contrast.test.js. See issue #131. */
273
+ /* Re-tinted from --green; it was rgba(30, 150, 50, .1), a colour that exists nowhere
274
+ else in the kit while the other three light glows are exact tints of their own token.
275
+ The wash over white moves at most two levels on a channel and no verdict moves with
276
+ it. Gated for all four glows in stories/signal-contrast.test.js. */
323
277
  --glow-green: rgba(28, 138, 44, 0.1);
324
278
  --glow-purple: rgba(106, 45, 204, 0.09);
325
279
  --glow-cyan: rgba(12, 143, 168, 0.1);
326
280
  --glow-pink: rgba(182, 51, 97, 0.1);
327
281
 
328
- /* Solid chip pairs for the white app. Each ink is deepened past its signal
329
- hue so 10px uppercase text clears WCAG AA on its own pale fill
330
- e.g. --pink (#b63361) on #fbe0ea is 4.66:1, #b7295f is 4.85:1. */
282
+ /* Solid chip pairs for the white app. Each ink is deepened past its signal hue so 10px
283
+ uppercase text clears WCAG AA on its own pale fill. */
331
284
  --chip-success-ink: #1f7a38;
332
285
  --chip-success-fill: #dff3e4;
333
286
  --chip-warn-ink: #8a5e00;
334
287
  --chip-warn-fill: #fbedd2;
335
288
  --chip-danger-ink: #b7295f;
336
289
  --chip-danger-fill: #fbe0ea;
337
- /* Info was the one signal family without a pair, so the info badge was still
338
- painting --cyan (#0c8fa8) on --glow-cyan over white, which is 3.39:1
339
- under AA. #0a7286 on #ddeff3 is 4.71:1, inside the 4.64–4.93 band the other
340
- three pairs already sit in. See issue #131. */
290
+ /* Info was the one signal family without a pair, so the info badge painted --cyan on
291
+ --glow-cyan over white and missed AA. This pair sits in the band the other three
292
+ already occupy. See issue #131. */
341
293
  --chip-info-ink: #0a7286;
342
294
  --chip-info-fill: #ddeff3;
343
295
 
344
- /* Light runs the same rule the other way. The signals are mid-tone here a
345
- solid --green fill measures 4.40 against near-black and 4.45 against white,
346
- so NO ink rescues it and the fill is what has to move. It moves to the
347
- deepened form the chip pair already carries, and the pole above it, white,
348
- clears every one: #ffffff measures 5.39 on --chip-success-ink, 6.01 on
349
- --chip-danger-ink, 5.70 on --chip-warn-ink, 5.58 on --chip-info-ink and 6.11
350
- on --muted. A future status supplies one --signal-solid-* value dark enough
351
- for white and inherits the ink. See issues #131 and #149. */
296
+ /* Light runs the same rule the other way. The signals are mid-tone here, so NO ink
297
+ rescues them and the fill is what has to move: it takes the deepened form the chip
298
+ pair already carries, and white clears every one. A future status supplies one
299
+ --signal-solid-* value dark enough for white and inherits the ink. See #131, #149. */
352
300
  --signal-solid-ink: #ffffff;
353
301
  --signal-solid-success: var(--chip-success-ink);
354
302
  --signal-solid-danger: var(--chip-danger-ink);
@@ -361,8 +309,8 @@
361
309
  --shadow-lg: 0 18px 50px rgba(30, 30, 50, 0.16);
362
310
  /* Tight elevation for the active segmented pill — stays contained in its track. */
363
311
  --shadow-seg: 0 1px 2px rgba(30, 30, 50, 0.12), 0 2px 4px rgba(30, 30, 50, 0.1);
364
- /* A white card on a white page needs its own, flatter-than-sm elevation:
365
- a hairline contact shadow plus a wide soft one (see card.css). */
312
+ /* A white card on a white page needs its own flatter-than-sm elevation: a hairline
313
+ contact shadow plus a wide soft one (see card.css). */
366
314
  --shadow-card: 0 1px 2px rgba(20, 25, 45, 0.04), 0 4px 14px rgba(20, 25, 45, 0.05);
367
315
  /* No --ring here. :root's is var(--accent), and light re-points --accent. */
368
316
 
@@ -376,42 +324,32 @@
376
324
  }
377
325
 
378
326
  /* ============================================================================
379
- * WIDTHS BELOW THE PAGE — the box, and the line
380
- *
381
- * --container is the page and --measure the reading column inside it (both
382
- * above). Underneath them the kit has two scales and one rule for choosing
383
- * between them: THE UNIT SAYS WHICH SCALE YOU ARE ON.
327
+ * WIDTHS BELOW THE PAGE — the box, and the line. Two scales, and one rule for
328
+ * choosing between them: THE UNIT SAYS WHICH SCALE YOU ARE ON.
384
329
  *
385
330
  * a box that holds a COMPONENT -> --panel-* (px)
386
331
  * a box that holds a LINE -> --prose-* (ch)
387
332
  *
388
- * Both are theme-invariant, which is why they sit outside the palette blocks.
333
+ * Both are theme-invariant, so they sit outside the palette blocks.
389
334
  * why: docs/specification.md#boxes-below-the-page
390
335
  * ========================================================================== */
391
336
  :root {
392
337
  /* -- Panel widths -------------------------------------------------------
393
- * Not a new scale: the drawer has shipped sm/md/lg at exactly these three
394
- * values since it was written, and confirm, the auth card and the toast each
395
- * wrote one of them out again on their own. --panel-md answers the question
396
- * #208 asked — the two 420s were three (--confirm-w, --drawer-w md and
397
- * .ui-auth__card), and they are one measure now. */
338
+ * Not a new scale: the drawer has shipped sm/md/lg at these three values, and
339
+ * confirm, the auth card and the toast each wrote one out again (#208). */
398
340
  --panel-sm: 320px;
399
341
  --panel-md: 420px;
400
342
  --panel-lg: 560px;
401
343
 
402
344
  /* -- Prose measures -----------------------------------------------------
403
- * ch resolves against the font-size of the element it is DECLARED on, so a
404
- * step goes on the paragraph itself never on a wrapper holding two type
405
- * sizes, where the same token would mean two different widths.
406
- *
407
- * The four steps are the reading band (44–72ch), named for what is being
408
- * read rather than sized s/m/l, so the choice is made by the writer:
345
+ * ch resolves against the font-size of the element it is DECLARED on, so a step
346
+ * goes on the paragraph itself, never on a wrapper holding two type sizes. The
347
+ * four steps are the reading band (44–72ch), named for what is being read:
409
348
  * caption one or two sentences under a glyph, centred
410
349
  * lede the line under a title
411
350
  * body a left-aligned column of copy
412
351
  * dense reference prose set below 13px, where the band runs wider
413
- * --prose-display is NOT a measure. It is where a display headline rags, and
414
- * it is far under the band on purpose. */
352
+ * --prose-display is NOT a measure it is where a display headline rags. */
415
353
  --prose-display: 14ch;
416
354
  --prose-caption: 44ch;
417
355
  --prose-lede: 54ch;