@orkestrel/scaffold 0.0.64 → 0.0.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/README.md +11 -1
  2. package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +184 -177
  3. package/dist/host/agents/skills/enterprise-bootstrap/references/bootstrap-reference.md +299 -76
  4. package/dist/host/agents/skills/enterprise-bootstrap/references/color-modes.md +241 -0
  5. package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +83 -36
  6. package/dist/host/agents/skills/enterprise-bootstrap/references/frontend-design.md +297 -98
  7. package/dist/host/agents/skills/enterprise-bootstrap/references/inputs.md +25 -14
  8. package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +216 -128
  9. package/dist/host/agents/skills/enterprise-bootstrap/references/responsive-layout.md +187 -0
  10. package/dist/host/agents/skills/enterprise-bootstrap/references/utilities.md +105 -16
  11. package/dist/host/claude/rules/workspace.md +2 -2
  12. package/dist/host/claude/settings.json +1 -1
  13. package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +10 -9
  14. package/dist/host/guides/scaffold.md +63 -22
  15. package/dist/host/manifest.json +25 -13
  16. package/dist/host/scripts/codex.sh +0 -0
  17. package/dist/host/scripts/cursor.sh +0 -0
  18. package/dist/host/scripts/deps.sh +0 -0
  19. package/dist/host/scripts/ollama.sh +322 -13
  20. package/dist/src/core/index.cjs +33 -12
  21. package/dist/src/core/index.cjs.map +1 -1
  22. package/dist/src/core/index.d.cts +31 -9
  23. package/dist/src/core/index.d.ts +31 -9
  24. package/dist/src/core/index.js +32 -13
  25. package/dist/src/core/index.js.map +1 -1
  26. package/package.json +4 -4
@@ -10,7 +10,7 @@
10
10
  - [Quick start](#quick-start)
11
11
  - [Breakpoints & layout](#breakpoints--layout)
12
12
  - [Color modes (light / dark / custom)](#color-modes-light--dark--custom)
13
- - [Theming & design tokens](#theming--design-tokens)
13
+ - [Theming & design tokens](#theming--design-tokens) — [Define the working scales](#define-the-working-scales) · [Elevation and depth](#elevation-and-depth) · [Layout and type extensions](#layout-and-type-extensions)
14
14
  - [Forms in production](#forms-in-production)
15
15
  - [JavaScript lifecycle](#javascript-lifecycle)
16
16
  - [Accessibility](#accessibility)
@@ -23,7 +23,7 @@
23
23
 
24
24
  ## Quick Start
25
25
 
26
- CDN (5.3.8 is the current — and final — 5.3.x patch before 5.4):
26
+ Pinned CDN example (5.3.8). Prefer the installed compatible version; this is not an upgrade instruction:
27
27
 
28
28
  ```html
29
29
  <!doctype html>
@@ -49,6 +49,20 @@ CDN (5.3.8 is the current — and final — 5.3.x patch before 5.4):
49
49
 
50
50
  ## Breakpoints & Layout
51
51
 
52
+ Start with the feature's content and narrow layout, then choose its container and breakpoints.
53
+ Take the region contract, content parity, and test matrix from [responsive-layout.md](responsive-layout.md).
54
+ Use fluid columns for content that needs to scale together; keep rails, forms, and reading measures
55
+ bounded where it does not. A full-width shell does not require full-width text or fields.
56
+ Take the missing role-based utilities from [Layout and type extensions](#layout-and-type-extensions).
57
+
58
+ Give a form, dialog, or login card a content-led maximum and let it shrink only when the viewport
59
+ is narrower: `w-100 mx-auto measure-form`, not `col-md-8 offset-md-2 col-lg-6 offset-lg-3`, whose
60
+ width changes at every breakpoint and is narrower on `lg` than at some `md` widths. Give a
61
+ sidebar a fixed rail (`shell-rail-lg-fixed flex-shrink-0`) beside a flexible `min-inline-0` main
62
+ region, not `col-3`, which grows on wide screens and collapses below its minimum on narrow ones.
63
+ Put supporting explanation beside a narrow form in a second column rather than widening its
64
+ fields. Percentage widths belong only where columns must scale together.
65
+
52
66
  | Breakpoint | Class Infix | Dimensions |
53
67
  | ----------- | ----------- | ---------- |
54
68
  | Extra small | (none) | <576px |
@@ -85,77 +99,160 @@ The 5.3 color-mode system replaces the old per-component `*-dark` classes.
85
99
 
86
100
  ### Mechanics
87
101
 
88
- - `data-bs-theme="light|dark"` on `<html>` sets the mode globally; on any element it scopes the mode to that subtree (nested scopes win over ancestors). Default is light.
89
- - Mode switching works by re-pointing root CSS variables — components never change their own rules. Core variables swapped per mode: `--bs-body-bg`, `--bs-body-color`, `--bs-emphasis-color`, `--bs-secondary-color`, `--bs-secondary-bg`, `--bs-tertiary-color`, `--bs-tertiary-bg`, `--bs-border-color`, `--bs-heading-color`, `--bs-link-color`.
90
- - Each theme color also gets a mode-adaptive triplet — `--bs-{color}-text-emphasis`, `--bs-{color}-bg-subtle`, `--bs-{color}-border-subtle` — surfaced as `.text-{color}-emphasis`, `.bg-{color}-subtle`, `.border-{color}-subtle`. These are the workhorses for status UI that must read in both modes.
91
- - **The triplet is a recipe, not a guarantee.** `text-{color}-emphasis` on `bg-{color}-subtle` is _designed_ to pass, and it usually does in stock Bootstrap — but the values are tokens, and a compatible skin redefines them. Resolve the actual computed values from the compiled cascade the page loads (the shipped CSS, dependency stylesheets included) and measure each pairing once per theme before you rely on it. A class with no rule of its own may still inherit one; never accept a value from documentation.
92
- - Deprecated by this system: `.navbar-dark`, `.dropdown-menu-dark`, `.btn-close-white`, `.carousel-dark` → put `data-bs-theme="dark"` on the component or an ancestor instead.
102
+ Read [color-modes.md](color-modes.md) before choosing color classes or repairing a theme failure.
103
+ It owns inheritance, adaptive-versus-fixed families, surface boundaries, and component exceptions.
104
+ Use the installed build's attribute or media-query strategy; do not assume every `--bs-*` variable
105
+ changes with the mode.
93
106
 
94
107
  ### Author rules
95
108
 
96
- - Paint custom CSS from `var(--bs-…)` — never hard-coded hex — so both modes track automatically.
97
- - Prefer `bg-body`, `bg-body-secondary`, `bg-body-tertiary` and `text-body`, `text-body-secondary` over `bg-white`/`bg-light`/`text-dark`, which freeze a mode.
98
- - A dark region inside a light page (or vice versa) is one attribute: `<footer data-bs-theme="dark">`.
109
+ Preserve ordinary text inheritance and native component states. Prefer adaptive body/subtle
110
+ surfaces. Establish an explicit foreground only at an owned solid or mode boundary, or for a
111
+ measured role that requires it. Do not turn every subtle panel into a custom color pair.
99
112
 
100
113
  ### Theme toggle
101
114
 
102
- Bootstrap ships **no** mode picker — you build the toggle. The essentials: read the stored preference, fall back to `prefers-color-scheme`, set `data-bs-theme` on `document.documentElement`, and do it in a script early in `<head>` so the first paint does not flash the wrong mode.
103
-
104
- ```js
105
- const stored = localStorage.getItem('theme')
106
- const preferred =
107
- stored ?? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
108
- document.documentElement.setAttribute('data-bs-theme', preferred)
109
- // On toggle: setAttribute + localStorage.setItem("theme", value)
110
- ```
115
+ Reuse the host controller. When implementing one, follow [Scope the mode](color-modes.md#scope-the-mode)
116
+ for validated preference, automatic-mode resolution, storage failure, first paint, and overlay
117
+ mounts. Bootstrap ships no picker; an attribute example is not a complete controller.
111
118
 
112
119
  ### Custom modes
113
120
 
114
- A custom mode is a named scope overriding the same variables:
121
+ Add a custom mode only when the brief requires it. Map its used body, surface, link, border,
122
+ validation, and component-state variables, including RGB companions. Resolve embedded component
123
+ images and `color-scheme` where relevant. Do not claim a complete mode from a partial token block.
124
+
125
+ In Sass, use `$enable-dark-mode`, `$color-mode-type: data` for local attribute scopes, or
126
+ `media-query` for system-driven mode without per-component scoping. Use `color-mode()` rather than
127
+ competing selectors; keep overrides in the host theme source.
128
+
129
+ ## Theming & Design Tokens
130
+
131
+ ### The tiered token model
132
+
133
+ Keep literal values in declared primitives, map primitives to semantic roles, and let component
134
+ variables consume those roles. Reuse Bootstrap's `--bs-*` semantic and component layers rather
135
+ than adding a parallel palette. Name semantics by purpose, not a particular shade.
136
+
137
+ Distinguish token structure from runtime behavior. Bootstrap also generates fixed values through
138
+ Sass; a component variable is not necessarily mode-adaptive. Map the actual consumer, including its
139
+ states, and resolve aliases at the scope where they must change. Take the constraints from
140
+ [Extend the theme](color-modes.md#extend-the-theme).
141
+
142
+ ### Define the working scales
143
+
144
+ Reuse the installed theme and its scales first. Declare new values only for a role the feature
145
+ needs; refine one shared definition instead of accumulating per-component exceptions. Every
146
+ system below has a Bootstrap source, a utility, and a known gap; extend the source, never the
147
+ markup.
148
+
149
+ | System | Sass source | Utility | Stock steps (default root) | Gap |
150
+ | -------------- | ----------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------- |
151
+ | Font size | `$font-sizes`, `$h1…h6-font-size`, `$display-font-sizes` | `fs-1…6`, `.h1…h6`, `display-1…6` | 16 · 20 · 24 · 28 · 32 · 40 px; display 40–80 px | No `rem` step below 16 px; `.small` is `.875em` |
152
+ | Font weight | `$font-weight-*`, `$headings-font-weight` | `fw-light…bold` | 300 · 400 · 500 · 600 · 700; headings 500 | Headings barely heavier than body |
153
+ | Line height | `$line-height-*`, `$headings-line-height` | `lh-1`, `lh-sm`, `lh-base`, `lh-lg` | 1 · 1.25 · 1.5 · 2; headings 1.2 | — |
154
+ | Color | `$gray-100…900`, `$blue-100…900` …, `$theme-colors`, triads | `text-*`, `bg-*`, `border-*` families | 9 shades per hue; subtle/emphasis per role | Ramps are mechanical mixes ([color-modes.md](color-modes.md) → Extend the theme) |
155
+ | Spacing | `$spacers` | `m-*`, `p-*`, `gap-*`, `g-*` | 0 · 4 · 8 · 16 · 24 · 48 px | No 12 or 32 px; nothing above 48 px |
156
+ | Width | `$container-max-widths`, the grid | `w-*`, `mw-100`, `col-*` | 25 / 50 / 75 / 100 % | No content-led maximums |
157
+ | Shadow | `$box-shadow`, `-sm`, `-lg`, `-inset` | `shadow-sm`, `shadow`, `shadow-lg` | 3 steps | Single-layer; modal shares the dropdown step |
158
+ | Radius | `$border-radius*`, `$enable-rounded` | `rounded-0…5`, `-pill`, `-circle` | 0 · 4 · 6 · 8 · 16 · 32 px | — |
159
+ | Border width | `$border-widths` | `border-1…5` | 1–5 px | Sets every side at once |
160
+ | Opacity | — | `opacity-*`, `text-opacity-*`, `bg-opacity-*` | 0 · 10 · 25 · 50 · 75 · 100 | Not a text tier |
161
+ | Letter-spacing | — | none | — | Generate ([Layout and type extensions](#layout-and-type-extensions)) |
162
+
163
+ - **Color:** neutral, brand, and required status/categorical ramps. Pick base, light surface, and
164
+ dark text shades in real components, then fill the gaps. Use HSL when it helps tune related
165
+ shades; keep the project's existing format. Review fixed shade pairs in each theme rather than
166
+ generating a new `lighten`, `darken`, or `color-mix` result at each use site. Stock ramps and
167
+ triads are tint/shade mixes with a fixed hue; override the nine shade variables and six triad
168
+ variables per brand hue, and the nine greys as one temperature-matched set
169
+ ([color-modes.md](color-modes.md) → Extend the theme).
170
+ - **Type:** display/body/utility roles, finite `rem` sizes, working weights, and line-height per
171
+ role. Roles may share a font. RFS scales sizes above 1.25 rem down below a 1200 px viewport
172
+ (`h1`–`h4`, `display-*`, `fs-1`–`fs-4`); body, `fs-5`, `fs-6`, `.lead`, and controls hold — do not
173
+ fight it with `em` heading sizes. Take 14 px and 12 px roles from generated `fs-sm`/`fs-xs`, not
174
+ nested `.small`. Never globally scale body text down to make a display treatment fit.
175
+ - **Space and size:** internal, group, panel, and section gaps; control sizes; reading/form widths;
176
+ rail width. Start with Bootstrap's shipped scale. Add a missing step through the utilities API
177
+ only where the adjacent steps cannot express the intended relationship. Button sizes already
178
+ scale padding faster than font (4/8 px at 14 px, 6/12 at 16, 8/16 at 20); use the three shipped
179
+ sizes rather than deriving one with `em` padding.
180
+ - **Radius and elevation:** a small consistent family, assigned to real component/layer roles.
181
+ Set `$border-radius` once and let components inherit it; do not hand-mix `rounded-*` per
182
+ element. Reuse component variables and shadow utilities ([Elevation and depth](#elevation-and-depth)).
183
+ No shadow is a valid surface role.
184
+
185
+ Record these roles in the existing token source or a compact design contract, not a second design
186
+ system. Keep literal colors and raw scale values in named primitive definitions; component rules
187
+ consume semantic or component tokens. [inspection.md](inspection.md) → Token discipline checks
188
+ that boundary; [frontend-design.md](frontend-design.md) owns the visual choices.
189
+
190
+ ### Elevation and depth
191
+
192
+ Three shipped steps — `--bs-box-shadow-sm` (`0 .125rem .25rem` at .075), `--bs-box-shadow`
193
+ (`0 .5rem 1rem` at .15), `--bs-box-shadow-lg` (`0 1rem 3rem` at .175) — plus
194
+ `--bs-box-shadow-inset`. Assign by z-position: `sm` for raised cards and controls, base for
195
+ floating menus and a dragged item, `lg` for dialogs. Stock dropdowns, popovers, toasts, and
196
+ modals all sit on `--bs-box-shadow` (modal: `-sm` below 576 px), which puts a blocking dialog at
197
+ dropdown elevation. Lift it at rung 3, in the project stylesheet after Bootstrap's so the rule
198
+ wins the `sm`-up media rule:
115
199
 
116
200
  ```css
117
- [data-bs-theme='midnight'] {
118
- --bs-body-bg: #0b1020;
119
- --bs-body-color: #dfe4f2;
120
- --bs-tertiary-bg: #131a30;
121
- --bs-border-color: #26304f;
122
- }
123
- [data-bs-theme='midnight'] .dropdown-menu {
124
- --bs-dropdown-bg: var(--bs-tertiary-bg);
201
+ .modal {
202
+ --bs-modal-box-shadow: var(--bs-box-shadow-lg);
125
203
  }
126
204
  ```
127
205
 
128
- In Sass: `$enable-dark-mode` (default true), `$color-mode-type: data` (attribute selectors) or `media-query` (`prefers-color-scheme` — loses per-component scoping), and the `@include color-mode(dark) { … }` mixin. Dark defaults live in `_variables-dark.scss`.
206
+ Two-part shadows — a broad cast plus a tight contact shadow that fades with elevation — are a
207
+ token change: redefine `$box-shadow-sm`, `$box-shadow`, and `$box-shadow-lg` as two-layer values
208
+ and every consumer follows.
129
209
 
130
- ## Theming & Design Tokens
210
+ `$enable-shadows: true` (off by default) paints light-from-above on controls: buttons take
211
+ `inset 0 1px 0 rgba(#fff, .15), 0 1px 1px rgba(#000, .075)` (lit top edge, tight cast shadow),
212
+ inputs `inset 0 1px 2px rgba(#000, .075)` (recessed), and an active button `inset 0 3px 5px`
213
+ (pressed). Enable it when the direction wants tactile controls, and verify both modes; the alphas
214
+ are fixed white and black. Without the flag the `box-shadow` mixin emits nothing, so
215
+ `--bs-btn-box-shadow` and `--bs-box-shadow-inset` have no consumer and a rung-3 override does
216
+ nothing; the recipe is then a proposed rule.
131
217
 
132
- ### The tiered token model
218
+ Flat depth: a `bg-body` panel on `bg-body-tertiary` reads raised and `bg-body-secondary` inside
219
+ `bg-body` reads inset, both mode-adaptive with no shadow. A hard offset shadow is a `$box-shadow`
220
+ override.
133
221
 
134
- Enterprise theming survives rebrands and dark mode only when tokens are tiered:
222
+ Overlap: `position-relative translate-middle-y`, or `mt-n*` after `$enable-negative-margins`.
223
+ Ring overlapping images in the surface color through the border variable so the ring follows the
224
+ mode, where `border-white` does not:
135
225
 
136
- 1. **Primitives** — raw values (`--brand-blue-600`, a spacing scale). Never referenced by component CSS directly.
137
- 2. **Semantic tokens** — intent (`--bs-primary`, `--bs-body-bg`, `--bs-border-color`, `--bs-danger`). Reference primitives.
138
- 3. **Component tokens** — one component's knobs (`--bs-btn-bg`, `--bs-card-spacer-y`). Reference semantics.
226
+ ```css
227
+ .ring-body {
228
+ --bs-border-color: var(--bs-body-bg);
229
+ }
230
+ ```
139
231
 
140
- **In Bootstrap, the `--bs-*` variables ARE your semantic and component layers.** Define your primitives, map them onto `--bs-*`, and let components read only `var(--bs-…)`. Dark mode then becomes a re-point of semantic tokens under `[data-bs-theme="dark"]` — if dark mode ever requires editing a component rule, the tier boundary leaked. Name semantics by role, never appearance (`--surface-sunken`, not `--gray-100`): a value change must never force a rename. Raw hex scattered in component CSS is a primitive referenced directly — the root cause of un-themable UI.
232
+ Then `rounded-circle border border-3 ring-body`.
141
233
 
142
234
  ### The CSS-variables-only path (no Sass build)
143
235
 
144
- Every component exposes local `--bs-{component}-*` variables (`--bs-btn-color`, `--bs-card-bg`, `--bs-nav-link-padding-x`, `--bs-table-bg`, …). The documented no-build theming route — right for consuming the CDN build:
236
+ Use native components and adaptive utilities before adding overrides. For a recurring component
237
+ surface role, use its local variable rather than repainting the whole component. This optional
238
+ project-defined class changes the card background without assigning a foreground:
145
239
 
146
240
  ```css
147
- :root {
148
- --bs-primary: #6f42c1; /* note: utility classes derived at build */
149
- --bs-primary-rgb: 111, 66, 193; /* time need the -rgb partner updated too */
150
- }
151
- .btn-brand {
152
- --bs-btn-bg: var(--bs-primary);
153
- --bs-btn-color: #fff;
154
- --bs-btn-hover-bg: color-mix(in srgb, var(--bs-primary), black 10%);
241
+ .card-quiet {
242
+ --bs-card-bg: var(--bs-tertiary-bg);
155
243
  }
156
244
  ```
157
245
 
158
- Overriding component variables in a scope beats high-specificity override rules every time: it composes with color modes, keeps specificity flat, and documents intent.
246
+ Use `class="card card-quiet"` only after declaring that extension in the host theme stylesheet.
247
+ Do not add it when `card bg-body-tertiary` already expresses the requirement. Measure the card's
248
+ inherited foreground and its header/footer layers in the loaded skin.
249
+
250
+ When a custom button variant is required, define rest, hover, focus, active/checked, and disabled
251
+ component variables as one contract. Include borders and the focus-ring RGB value; test busy
252
+ content without changing geometry. Do not generate a custom tinted button merely to distinguish a
253
+ secondary action, and do not assume reversing a subtle/emphasis pair produces valid states.
254
+ Changing root `--bs-primary` alone does not rebuild Sass-generated button states or utility RGB
255
+ consumers; follow [Extend the theme](color-modes.md#extend-the-theme).
159
256
 
160
257
  ### The Sass path (compiled builds)
161
258
 
@@ -175,7 +272,7 @@ Import order matters — override maps **before** the files that consume them:
175
272
  @import 'bootstrap/scss/utilities/api'; // generates utilities — keep LAST
176
273
  ```
177
274
 
178
- Feature flags worth knowing: `$enable-dark-mode`, `$enable-rounded`, `$enable-shadows`, `$enable-gradients`, `$enable-rfs` (fluid type), `$enable-validation-icons`, `$enable-negative-margins`, `$enable-important-utilities`, `$enable-reduced-motion`. Custom theme colors added to `$theme-colors` also need entries in the subtle/emphasis maps (`$theme-colors-text`, `$theme-colors-bg-subtle`, `$theme-colors-border-subtle` — and their `-dark` twins) to get full color-mode support.
275
+ Feature flags worth knowing: `$enable-dark-mode`, `$enable-rounded`, `$enable-shadows` (light-from-above button/input/active shadows, [Elevation and depth](#elevation-and-depth)), `$enable-gradients` (a white fade on every `bg-*`; not a two-hue gradient), `$enable-rfs` (sizes above 1.25 rem shrink below 1200 px), `$enable-validation-icons`, `$enable-negative-margins` (`mt-n*` for overlap), `$enable-important-utilities`, `$enable-reduced-motion`. For added theme colors, extend the light/dark emphasis and subtle maps and inspect the generated utility-value maps; see [Extend the theme](color-modes.md#extend-the-theme). Do not infer a generated class from a token alone.
179
276
 
180
277
  ### Utilities API
181
278
 
@@ -191,7 +288,7 @@ $utilities: map-merge(
191
288
  class: cursor,
192
289
  values: auto pointer grab,
193
290
  ),
194
- // modify an existing one, e.g. make width responsive:
291
+ // Modify an existing utility: make width responsive.
195
292
  'width': map-merge(
196
293
  map-get($utilities, 'width'),
197
294
  (
@@ -205,23 +302,123 @@ $utilities: map-merge(
205
302
 
206
303
  Remove with `map-remove($utilities, "width")` or set the key to `null`. This is the sanctioned answer when the shipped scale is missing a step (for example, a `vh-50` the design truly needs).
207
304
 
305
+ ### Layout and type extensions
306
+
307
+ These are **project-generated classes**, not stock Bootstrap utilities. Use the existing project
308
+ roles when present. Otherwise add only the needed entries; the values below are illustrative role
309
+ definitions, not universal sizes.
310
+
311
+ Scale steps go in the map-override slot (after `variables-dark`, before `maps`). Keys `0`–`5`
312
+ keep their shipped meaning; an intermediate 12 px or 32 px step is a new key, never a decimal.
313
+ `$font-sizes` feeds only the `fs-*` utility in 5.3.8, and RFS leaves values at or below 1.25 rem
314
+ alone, so extending it is safe:
315
+
316
+ ```scss
317
+ $spacers: map-merge(
318
+ $spacers,
319
+ (
320
+ 6: $spacer * 4,
321
+ 7: $spacer * 6,
322
+ )
323
+ ); // 64px section gap, 96px page rhythm
324
+ $font-sizes: map-merge(
325
+ $font-sizes,
326
+ (
327
+ sm: 0.875rem,
328
+ xs: 0.75rem,
329
+ )
330
+ ); // fs-sm 14px captions, fs-xs 12px eyebrows only
331
+ ```
332
+
333
+ Role utilities go after importing `utilities` and before `utilities/api`:
334
+
335
+ ```scss
336
+ $utilities: map-merge(
337
+ $utilities,
338
+ (
339
+ 'content-measure': (
340
+ property: max-inline-size,
341
+ class: measure,
342
+ values: (
343
+ prose: 65ch,
344
+ form: 36rem,
345
+ ),
346
+ ),
347
+ 'shell-rail': (
348
+ property: inline-size,
349
+ class: shell-rail,
350
+ responsive: true,
351
+ values: (
352
+ fixed: 16rem,
353
+ ),
354
+ ),
355
+ 'inline-minimum': (
356
+ property: min-inline-size,
357
+ class: min-inline,
358
+ values: (
359
+ 0: 0,
360
+ ),
361
+ ),
362
+ 'table-viewport': (
363
+ property: max-block-size,
364
+ class: max-block,
365
+ responsive: true,
366
+ values: (
367
+ table: 70vh,
368
+ ),
369
+ ),
370
+ 'tabular-figures': (
371
+ property: font-variant-numeric,
372
+ class: figures,
373
+ values: (
374
+ tabular: tabular-nums,
375
+ ),
376
+ ),
377
+ 'letter-spacing': (
378
+ property: letter-spacing,
379
+ class: ls,
380
+ values: (
381
+ tight: -0.02em,
382
+ wide: 0.05em,
383
+ ),
384
+ ),
385
+ )
386
+ );
387
+ // Generate once, after all utility-map additions:
388
+ @import 'bootstrap/scss/utilities/api';
389
+ ```
390
+
391
+ Use `w-100 measure-form` for a bounded form, `measure-prose` for a reading column,
392
+ `shell-rail-lg-fixed flex-shrink-0` for an inline desktop rail, `min-inline-0` for its flexible
393
+ sibling, `max-block-lg-table` only for a warranted wide-screen bounded table scroller, `figures-tabular` for comparable
394
+ quantities, `ls-tight` on `display-*` and `fs-1`, and `ls-wide` with `text-uppercase` labels (`em`
395
+ is correct for tracking: it follows the element's own size). Verify those selectors in the
396
+ compiled output before using the examples below. The font must support tabular figures. A `ch`
397
+ measure is a starting width, not a character-count proof.
398
+
399
+ Without a Sass build, take an existing equivalent; otherwise propose the smallest stylesheet rule
400
+ under [SKILL.md](../SKILL.md) → When custom CSS is justified. Never ship an unresolved utility name.
401
+
208
402
  ## Forms in Production
209
403
 
210
404
  ### Layout & labels
211
405
 
212
- - **Top-aligned labels by default** — the evidence (eye-tracking form research) shows fastest completion and the cleanest single-column scan, and they survive narrow screens without reflow. Reserve left-aligned labels for dense read-back forms where vertical compression matters more than speed.
406
+ - **Top-aligned labels by default** — keep a consistent single-column scan. Reserve side labels for a deliberate dense layout that still reads correctly at narrow widths.
213
407
  - Visible label or `.form-floating` — never placeholder-only (disappears on input, fails accessibility).
214
- - **Do not say the same thing twice.** When the host already names the request — a card heading, a dialog title, a section header stating the question — the form associates with that name through `aria-labelledby` instead of repeating the prompt in its own label. Repetition reads as separate questions to a screen-reader user and as clutter to everyone else.
215
- - One column beats multi-column for completion; use the form grid (`row g-3` + `col-md-*`) only for genuinely paired fields (city/state/zip).
408
+ - **Name the form without duplicating its heading.** Associate the form with an existing visible title through `aria-labelledby` where useful. Keep each control's own visible label; a form name does not label its fields.
409
+ - Use one field column by default; add columns only for genuinely related fields. At wide widths,
410
+ put supporting explanation beside the form rather than stretching its fields.
411
+ - Keep each label, control, help text, and error in one group. Use a smaller internal gap than the
412
+ gap to the next field group, and recheck the relationship when errors or long labels wrap.
216
413
 
217
414
  ```html
218
415
  <form class="row g-3">
219
- <div class="col-md-6">
416
+ <div class="col-12">
220
417
  <label for="inputEmail4" class="form-label">Email</label>
221
418
  <input type="email" class="form-control" id="inputEmail4" aria-describedby="emailHelp" />
222
419
  <div id="emailHelp" class="form-text">Work address preferred.</div>
223
420
  </div>
224
- <div class="col-md-6">
421
+ <div class="col-12">
225
422
  <label for="inputPassword4" class="form-label">Password</label>
226
423
  <input type="password" class="form-control" id="inputPassword4" />
227
424
  </div>
@@ -234,7 +431,7 @@ Remove with `map-remove($utilities, "width")` or set the key to `null`. This is
234
431
  ### Validation timing (the rules that matter)
235
432
 
236
433
  - Validate a field **on blur** — after the user leaves it — never on every keystroke, and never before the user has reached the field. Exception: live feedback that _helps_ while typing (password strength, username availability, character counts).
237
- - Once a field is in an error state, re-validate as the user types so they see the fix land.
434
+ - After a field enters an error state, re-validate as the user types so they see the fix land.
238
435
  - Always re-check everything on submit. Keep the submit button **enabled** — a disabled submit hides _what is_ wrong; a validating submit shows it.
239
436
  - On failed submit of a long form, render an **error summary** at the top (focus it; link each item to its field) _and_ inline messages at each field — never summary-only, never inline-only.
240
437
  - Error style = color + icon + text, stating what is wrong and how to fix it. Wire message to field with `aria-describedby`, mark the field `aria-invalid="true"`. Never report errors through a hover tooltip.
@@ -345,7 +542,7 @@ Hold the baseline in [SKILL.md](../SKILL.md) → Accessibility baseline. Its Boo
345
542
  **Measuring the bars:**
346
543
 
347
544
  - Hold the bars from [SKILL.md](../SKILL.md) → Surfaces, color, contrast: **≥ 4.5:1** for everything information-bearing, **≥ 3:1** for textless marks and state chrome. WCAG 2.2 permits 3:1 for large text; this package does not — size grants no lower tier.
348
- - Measure in **both themes**, from the compiled cascade, never from the token names. A pairing that passes in light routinely fails in dark, and a skin's values are its own.
545
+ - Measure each declared theme from the compiled cascade, never from token names. A light-theme result does not establish a dark-theme result, and a skin's values are its own.
349
546
  - Focus rings and hover fills are UI graphics: they are in scope for the 3:1 bar.
350
547
  - Disabled controls are exempt from the bars by the spec. That exemption covers legibility, not meaning — see [Destructive actions](#destructive-actions) for the one disabled state that still has to change color.
351
548
 
@@ -353,9 +550,12 @@ Hold the baseline in [SKILL.md](../SKILL.md) → Accessibility baseline. Its Boo
353
550
 
354
551
  - collects every painted layer from the element upward to the first opaque one, then composites them top over bottom (Porter-Duff `over`) onto that opaque base;
355
552
  - composites a translucent foreground over that result before taking the ratio, rather than reading the declared color;
356
- - measures both themes in one run, since the theme swap re-points the tokens under every layer;
553
+ - measures each declared theme in the same run, since a theme swap re-points the tokens under every layer;
357
554
  - carries a negative control drawn from outside the population it covers — a pairing known to fail — and voids the run if that negative control passes.
358
555
 
556
+ Include ancestor opacity in the painted stack. For images, gradients, masks, or blend modes the
557
+ reader does not support, use a suitable rendered-background measurement or leave the pairing open;
558
+ never flatten a variable background to its average color. Name reached states beside every result.
359
559
  Wire the reader into the suite once it has settled a question.
360
560
 
361
561
  ### WCAG 2.2 deltas that bite dense app UI
@@ -383,7 +583,7 @@ Wire the reader into the suite once it has settled a question.
383
583
  Browsers handle focus on full page loads; in an SPA **you** do:
384
584
 
385
585
  - On route change, move focus to the new view's `h1` (or the `<main>` with `tabindex="-1"`) so SR users hear where they landed.
386
- - On failed submit, focus the error summary. On destructive confirm, focus the dialog's safe action.
586
+ - On failed submit, focus the error summary. On a compact destructive confirm, focus the safe action; in a scrolling or structured dialog, focus a static heading at the start when an action would scroll its context away.
387
587
  - After deleting a row, move focus to a sensible neighbor (next row / the table region), never let it fall to `<body>`.
388
588
  - Anything focused programmatically under sticky chrome needs the `scroll-margin-top` offset (2.4.11 above).
389
589
 
@@ -395,7 +595,10 @@ Bootstrap wraps its transitions and animations (`.fade`, `.collapsing`, carousel
395
595
 
396
596
  ### App shell
397
597
 
398
- **Structure:** persistent left sidebar for dense apps with many top-level destinations (it scales, nests, and stays stable while content changes); top-bar-only nav for shallow apps (≤ ~5 destinations). A collapsible sidebar reclaims width for data.
598
+ **Structure:** reuse the product's shell. For a new product, let implemented features and their
599
+ navigation needs decide between a sidebar and a shallow top bar; do not design the shell first.
600
+ Give an inline rail a content-led width and the task the remaining space. A collapsible rail can
601
+ reclaim width for comparison data.
399
602
 
400
603
  The Bootstrap implementation — a responsive offcanvas that renders inline above `lg` and becomes a drawer below it, with no custom JS:
401
604
 
@@ -421,7 +624,7 @@ The Bootstrap implementation — a responsive offcanvas that renders inline abov
421
624
 
422
625
  <div class="d-flex">
423
626
  <div
424
- class="offcanvas-lg offcanvas-start border-end"
627
+ class="offcanvas-lg offcanvas-start border-end shell-rail-lg-fixed flex-shrink-0"
425
628
  tabindex="-1"
426
629
  id="appSidebar"
427
630
  aria-labelledby="appSidebarLabel"
@@ -436,7 +639,7 @@ The Bootstrap implementation — a responsive offcanvas that renders inline abov
436
639
  aria-label="Close"
437
640
  ></button>
438
641
  </div>
439
- <div class="offcanvas-body d-lg-block p-lg-3" style="width: 260px;">
642
+ <div class="offcanvas-body d-lg-block p-lg-3">
440
643
  <nav aria-label="Primary">
441
644
  <ul class="nav nav-pills flex-column gap-1">
442
645
  <li class="nav-item">
@@ -448,8 +651,8 @@ The Bootstrap implementation — a responsive offcanvas that renders inline abov
448
651
  </nav>
449
652
  </div>
450
653
  </div>
451
- <main id="main" class="flex-grow-1 p-3 p-lg-4" style="min-width: 0;">
452
- <!-- min-width: 0 lets tables shrink instead of blowing out the flex row -->
654
+ <main id="main" class="flex-grow-1 p-3 p-lg-4 min-inline-0">
655
+ <!-- Generated min-inline-0 lets the task shrink inside the flex row. -->
453
656
  </main>
454
657
  </div>
455
658
  </body>
@@ -468,12 +671,22 @@ The Bootstrap implementation — a responsive offcanvas that renders inline abov
468
671
 
469
672
  **Craft rules:**
470
673
 
471
- - **Align by type:** numbers, currency, dates right-aligned (`text-end`, header too); text left. Use tabular figures so digits stack into comparable columns: `font-variant-numeric: tabular-nums` on numeric cells (one small custom rule that earns its place).
674
+ - **Align by comparison:** quantities and currency right-aligned (`text-end`, header too), with
675
+ consistent units and precision. Use the generated `figures-tabular` utility or the project's
676
+ equivalent. Keep text start-aligned; choose date alignment by its format and comparison task.
677
+ - **Group related content:** combine identity and supporting detail only when they do not need
678
+ independent column comparison or sorting. Keep key comparison columns explicit. Quiet repeated
679
+ labels and row actions before increasing density.
472
680
  - **Density:** `table-sm` for compact; offer density as a user toggle (comfortable/compact) driven by one token or wrapper class, not per-cell tweaks. Do not shrink font below readability to fake density.
473
- - **Sticky header** once the table meaningfully scrolls (roughly a viewport / ~15+ rows). Not built into Bootstrap — the pattern:
681
+ - **Sticky header** when the table meaningfully scrolls (roughly a viewport / ~15+ rows). Not built into Bootstrap — the pattern:
474
682
 
475
683
  ```html
476
- <div class="table-responsive" style="max-height: 70vh;">
684
+ <div
685
+ class="table-responsive max-block-lg-table"
686
+ role="region"
687
+ aria-label="Comparison table"
688
+ tabindex="0"
689
+ >
477
690
  <table class="table table-sm align-middle">
478
691
  <thead class="sticky-top">
479
692
  <tr>
@@ -485,13 +698,13 @@ The Bootstrap implementation — a responsive offcanvas that renders inline abov
485
698
  </div>
486
699
  ```
487
700
 
488
- Give header cells an **opaque background** (`bg-body-secondary` or a `.table-*` tone class) — table backgrounds are transparent by default, so rows show through a sticky header otherwise. Sticky chrome is the prime Focus-Not-Obscured offender: add `scroll-margin-top` on row focusables equal to the header height. Sticky first column only when row identity is lost on horizontal scroll — it costs paint and complexity.
701
+ Keep sticky header cells on an **opaque, mode-aware surface** such as `bg-body-secondary`. Stock `.table` cells use the body background; verify that a skin or override has not made them translucent. Do not substitute a `.table-*` color variant and assume it adapts. Inspect cell overlays through [Tables and overlays](color-modes.md#tables-and-overlays). Sticky chrome is the prime Focus-Not-Obscured offender: add `scroll-margin-top` on row focusables equal to the header height. Sticky first column only when row identity is lost on horizontal scroll — it costs paint and complexity.
489
702
 
490
703
  - **Sorting:** the whole header is a button (not a bare caret), with a visible direction indicator, and `aria-sort="ascending|descending"` on the active `<th>` only:
491
704
 
492
705
  ```html
493
706
  <th scope="col" aria-sort="ascending">
494
- <button type="button" class="btn btn-link p-0 fw-semibold text-body text-decoration-none">
707
+ <button type="button" class="btn btn-link p-0 fw-semibold">
495
708
  Amount <span aria-hidden="true">↑</span>
496
709
  </button>
497
710
  </th>
@@ -500,7 +713,7 @@ Give header cells an **opaque background** (`bg-body-secondary` or a `.table-*`
500
713
  - **Row actions:** 1–3 high-frequency actions inline; the rest behind a per-row kebab (dropdown). Hover-only reveal fails touch and keyboard — keep at least the overflow trigger always visible and ≥24px.
501
714
  - **Selection & bulk actions:** header checkbox with indeterminate state for partial selection; per-row checkboxes with `aria-label` naming the row ("Select INV-1042"). When selection > 0, swap the toolbar's content in place for a contextual bar — "3 selected", the batch actions, and a clear-selection escape — never push the layout down (layout-shifting chrome is an anti-pattern). Announce the count through a polite live region.
502
715
  - **Pagination vs scrolling:** paginate when users need position, totals, deep links, and "go to page N" — most enterprise CRUD. Virtualize (windowed rendering) for long uniform lists where scrolling is natural. True infinite scroll is for exploratory feeds only — never where users need a footer or a findable end.
503
- - **Responsive, ranked:** (1) _priority columns_ — hide low-value columns per breakpoint (`d-none d-lg-table-cell`), always keeping the identifying + decision columns; (2) _horizontal scroll_ (`table-responsive`) when every column matters — remember it clips dropdowns; (3) _card-ify_ into label:value stacks below `md` for low row counts. Never card-ify a wide comparison table — comparison is the point.
716
+ - **Responsive, by task:** use a compact record list for record work, a locally scrollable semantic table for essential comparison, or priority columns with an operable detail path. Preserve identity, decision fields, and actions. Choose expansion from available container width, not `md` by habit. Keep one state model across variants; take the contract from [Keep the task intact](responsive-layout.md#keep-the-task-intact).
504
717
  - **Table states:** loading → **skeleton rows** matching the real column count/widths (a centered spinner collapses the layout); empty → distinguish _no data yet_ (invite the first action) from _no results for these filters_ (offer "Clear filters"); error → inline retry inside the table region, header and toolbar preserved.
505
718
 
506
719
  ### Filter & search bars
@@ -508,7 +721,7 @@ Give header cells an **opaque background** (`bg-body-secondary` or a `.table-*`
508
721
  - One toolbar above the table: search input first (`role="search"` on the form), then the 2–4 highest-value filters as `form-select`/segmented controls, overflow filters behind a "Filters" button (offcanvas on mobile, dropdown/collapse on desktop).
509
722
  - **Active filters must be visible and dismissible** — chips/badges with an ✕ and a "Clear all" — users must see _why_ the list is short. A filtered-empty state repeats the escape hatch.
510
723
  - Debounce live search; show result counts ("128 results") so feedback is immediate; filter state belongs in the URL when views are shareable.
511
- - Toolbars that overflow: `flex-nowrap overflow-auto` beats wrapping the toolbar onto a second row mid-task — do not crush icon targets below 24px.
724
+ - At the base, give search a full row; stack or wrap actions and filters without shrinking labels or targets. Expand with `col-12 col-md`, `col-md-auto`, or `d-grid d-sm-flex` when they fit. Reserve a horizontal scroller for a documented spatial interaction, not an ordinary toolbar. Keep active filters and the clear path outside any disclosed extras.
512
725
 
513
726
  ### Wizards & multi-step forms
514
727
 
@@ -525,9 +738,15 @@ Give header cells an **opaque background** (`bg-body-secondary` or a `.table-*`
525
738
  Design **every one** for every data surface: ideal (populated), empty, loading, partial, error. A component is not done until all of them exist.
526
739
 
527
740
  - **Skeleton vs spinner:** skeleton (`placeholder` + `placeholder-glow`) when you know the content's shape and it fills a region — tables, cards, detail panes — because it holds layout and shortens perceived wait. Spinner for short, indeterminate, or in-control waits (inside a button, a small inline fetch).
528
- - **Thresholds (guidance):** under ~1s show nothing — a flashed loader is worse than none; ~1–10s show a spinner or skeleton; beyond ~10s show determinate progress (percent or step) so it does not feel hung.
741
+ - **Wait feedback:** acknowledge the action promptly, avoid flashing a loader for trivial waits, and keep the known layout stable. For longer work, show actual steps or measured progress when available; otherwise state that work continues and offer cancellation where supported. Never invent a percentage.
529
742
  - **Optimistic vs pessimistic:** apply UI immediately and reconcile (rolling back loudly on failure) for reversible high-frequency actions — toggles, stars, reorders. Await confirmation for money, audited records, and anything a rollback would confuse.
530
- - **Empty states** invite the next action (button + one line of why). Never-had-data and filtered-empty are different states with different escapes — never ship one generic "nothing here".
743
+ - **First-use empty:** name what belongs here and the useful create/import action. Drop tabs or
744
+ filters only when they genuinely have no data to operate on. An illustration may support that
745
+ action; it must not replace it.
746
+ - **Filtered-empty:** retain the active filters and result context, explain that nothing matched,
747
+ and offer a clear-filter path. Never hide the controls needed to undo the empty result.
748
+ - **Partial:** keep available data readable, identify the missing or stale part, and scope recovery
749
+ to it. Missing is not zero. Do not collapse the whole surface into an error when some data exists.
531
750
  - **Every error state states what failed and how to fix it**, carries a keyboard-reachable retry in place, and preserves surrounding context — a body fetch failure must not blow away the toolbar and filters.
532
751
 
533
752
  ### Feedback discipline
@@ -546,9 +765,13 @@ Blocking errors are never toasts. Keep the acting verb consistent across the flo
546
765
  Match friction to reversibility × blast radius:
547
766
 
548
767
  1. **Undo** (soft-delete + toast with Undo) for reversible, low-stakes, frequent actions — least friction, best experience. Prefer making actions undoable over interrupting them.
549
- 2. **Confirm dialog** for irreversible-but-scoped operations. Restate the specific consequence ("This permanently deletes 3 invoices"), verb-labeled buttons ("Delete invoices" / "Cancel" — never Yes/No), destructive action visually separated from safe; `alertdialog` semantics; focus lands on the safe action.
768
+ 2. **Confirm dialog** for irreversible-but-scoped operations. Restate the specific consequence ("This permanently deletes 3 invoices"), verb-labeled buttons ("Delete invoices" / "Cancel" — never Yes/No), destructive action visually separated from safe; `alertdialog` semantics; focus lands on the safe action for a compact confirmation, or a static top heading when focusing an action would scroll the consequences out of view.
550
769
  3. **Type-to-confirm** (type the entity name) only for high-blast-radius irreversible operations — delete an org, drop a dataset.
551
770
 
771
+ Keep action rank separate from consequence. A row-level destructive action can use a measured
772
+ quiet treatment; emphasize the final destructive commit where the ladder makes it the decision.
773
+ Do not make every row's delete button compete with the page's primary action.
774
+
552
775
  Do not type-gate a single-row delete; do not one-tap a tenant wipe. Confirm only where this ladder calls for it — a confirmation on every action gets clicked through.
553
776
 
554
777
  **Neutralize a disabled destructive control.** `btn-danger` at full saturation reads as armed whatever the `disabled` attribute says, and the contrast exemption for disabled controls does not excuse it. While the action is unavailable, drop to the neutral or outline `btn-*` class (or let the disabled state mute the fill) so the color stops promising an action, and say _why_ it is unavailable in text the assistive layer reaches: `aria-describedby` pointing at the reason, with `title` only as the pointer-user convenience on top. Never use `title` alone — it never reaches a keyboard or screen-reader user, and it disappears on touch.
@@ -567,11 +790,11 @@ Do not type-gate a single-row delete; do not one-tap a tenant wipe. Confirm only
567
790
 
568
791
  ## Performance
569
792
 
570
- - **Ship one CSS system and no more.** Bootstrap plus a second framework (or a parallel bespoke layer) doubles payload and guarantees specificity fights.
793
+ - **Keep one CSS system.** Extend the installed Bootstrap theme; do not add a competing framework or a parallel palette to restyle the surface.
571
794
  - **Compressed, the full build is cheap; incomplete builds are not.** Trimming through a Sass-subset build (import only the parts used — see [Theming](#theming--design-tokens)) is the sanctioned diet. Aggressive purge tools are the risky one: Bootstrap adds classes **at runtime** (`show`, `showing`, `fade`, `collapsing`, `modal-open`, `modal-backdrop`, `offcanvas-backdrop`, tooltip/popover generated markup) — purging without safelisting them ships UIs whose modals silently stop rendering. If you purge, safelist every JS-toggled class and test every overlay.
572
795
  - **Icons:** Bootstrap Icons is a separate package — prefer inline SVG or an SVG sprite (crisp, styleable through `currentColor`, no font flash) over the icon font; load only the icons used.
573
796
  - **JS:** the bundle is small, but only load it where behavior exists; per-component ESM imports (`bootstrap/js/dist/modal`) trim further in bundlers.
574
- - **Fonts:** each display face is a payload decision; subset and `font-display: swap` characterful faces, and let the data face fall back to the system stack when the brief allows.
797
+ - **Fonts:** reuse the existing families and load only needed weights/scripts. Add a display face only for a distinct role; use `font-display: swap` and test fallback wrapping. Keep the data face legible before and after fonts load.
575
798
 
576
799
  ## When Not to Hand-Roll
577
800
 
@@ -587,8 +810,8 @@ Bootstrap has **no** combobox/autocomplete, date picker, multi-select tags input
587
810
  ### Centered content
588
811
 
589
812
  ```html
590
- <div class="d-flex justify-content-center align-items-center vh-100">
591
- <div>Centered content</div>
813
+ <div class="d-flex flex-column min-vh-100 p-3">
814
+ <div class="my-auto">Centered content</div>
592
815
  </div>
593
816
  ```
594
817