saasaloy 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +7 -7
  2. package/dist/index.js +637 -522
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/schemas/registry-item.schema.json +31 -1
  6. package/templates/base/AGENTS.md +180 -53
  7. package/templates/base/README.md +20 -8
  8. package/templates/base/_agents/skills/saasaloy-design/SKILL.md +26 -15
  9. package/templates/base/_agents/skills/saasaloy-env/SKILL.md +130 -0
  10. package/templates/base/_agents/skills/saasaloy-landing-copy/SKILL.md +63 -46
  11. package/templates/base/_agents/skills/saasaloy-setup/SKILL.md +1 -1
  12. package/templates/base/_gitignore +0 -3
  13. package/templates/base/_saasaloy-base.json +8 -1
  14. package/templates/base/{DESIGN.md → apps/web/DESIGN.md} +36 -12
  15. package/templates/base/apps/web/astro.config.mjs +36 -6
  16. package/templates/base/apps/web/package.json +2 -1
  17. package/templates/base/apps/web/src/env.ts +41 -0
  18. package/templates/base/apps/web/src/layouts/Layout.astro +39 -11
  19. package/templates/base/apps/web/src/pages/index.astro +33 -24
  20. package/templates/base/apps/web/src/pages/privacy.astro +10 -2
  21. package/templates/base/apps/web/src/pages/terms.astro +10 -2
  22. package/templates/base/oxlint.config.mjs +91 -4
  23. package/templates/base/package.json +7 -3
  24. package/templates/base/packages/env/_env.example +54 -0
  25. package/templates/base/packages/env/package.json +35 -0
  26. package/templates/base/packages/env/scripts/env-setup.ts +87 -0
  27. package/templates/base/packages/env/src/check.test.ts +41 -0
  28. package/templates/base/packages/env/src/check.ts +50 -0
  29. package/templates/base/packages/env/src/define.test.ts +206 -0
  30. package/templates/base/packages/env/src/define.ts +315 -0
  31. package/templates/base/packages/env/src/deploy.test.ts +438 -0
  32. package/templates/base/packages/env/src/deploy.ts +518 -0
  33. package/templates/base/packages/env/src/dotenv.test.ts +179 -0
  34. package/templates/base/packages/env/src/dotenv.ts +173 -0
  35. package/templates/base/packages/env/src/env-file.ts +51 -0
  36. package/templates/base/packages/env/src/error.ts +38 -0
  37. package/templates/base/packages/env/src/example.test.ts +86 -0
  38. package/templates/base/packages/env/src/example.ts +90 -0
  39. package/templates/base/packages/env/src/index.ts +15 -0
  40. package/templates/base/packages/env/src/schema.ts +63 -0
  41. package/templates/base/packages/env/src/services.test.ts +83 -0
  42. package/templates/base/packages/env/src/services.ts +110 -0
  43. package/templates/base/packages/env/src/setup.test.ts +254 -0
  44. package/templates/base/packages/env/src/setup.ts +177 -0
  45. package/templates/base/packages/env/src/source.ts +20 -0
  46. package/templates/base/packages/env/src/sources.test.ts +137 -0
  47. package/templates/base/packages/env/src/sources.ts +93 -0
  48. package/templates/base/packages/env/src/zod.ts +8 -0
  49. package/templates/base/packages/env/tsconfig.json +13 -0
  50. package/templates/base/packages/tsconfig/base.json +7 -0
  51. package/templates/base/packages/ui/components.json +1 -1
  52. package/templates/base/packages/ui/package.json +1 -0
  53. package/templates/base/packages/ui/src/blocks/audience.tsx +73 -0
  54. package/templates/base/packages/ui/src/blocks/cta.tsx +41 -30
  55. package/templates/base/packages/ui/src/blocks/error-state.tsx +2 -0
  56. package/templates/base/packages/ui/src/blocks/faq.tsx +64 -36
  57. package/templates/base/packages/ui/src/blocks/feature-grid.tsx +48 -146
  58. package/templates/base/packages/ui/src/blocks/footer.tsx +129 -70
  59. package/templates/base/packages/ui/src/blocks/hero.tsx +183 -26
  60. package/templates/base/packages/ui/src/blocks/how-it-works.tsx +75 -0
  61. package/templates/base/packages/ui/src/blocks/logo-cloud.tsx +118 -0
  62. package/templates/base/packages/ui/src/blocks/navbar.tsx +204 -88
  63. package/templates/base/packages/ui/src/blocks/pain-points.tsx +131 -0
  64. package/templates/base/packages/ui/src/blocks/pricing-table.tsx +112 -94
  65. package/templates/base/packages/ui/src/blocks/showcase.tsx +185 -0
  66. package/templates/base/packages/ui/src/blocks/testimonials.tsx +85 -0
  67. package/templates/base/packages/ui/src/blocks/theme-toggle.tsx +16 -12
  68. package/templates/base/packages/ui/src/components/badge.tsx +1 -1
  69. package/templates/base/packages/ui/src/components/button.tsx +2 -2
  70. package/templates/base/packages/ui/src/components/tabs.tsx +1 -1
  71. package/templates/base/packages/ui/src/content/landing.ts +590 -125
  72. package/templates/base/packages/ui/src/index.ts +6 -2
  73. package/templates/base/packages/ui/src/lib/external-link.ts +36 -0
  74. package/templates/base/packages/ui/src/lib/icons.ts +131 -0
  75. package/templates/base/packages/ui/src/lib/inline-code.ts +29 -0
  76. package/templates/base/packages/ui/src/lib/theme.ts +140 -56
  77. package/templates/base/packages/ui/src/styles/globals.css +47 -125
  78. package/templates/base/packages/ui/src/styles/web.css +359 -0
  79. package/templates/base/pnpm-workspace.yaml +0 -6
  80. package/templates/base/saasaloy.json +1 -0
  81. package/templates/base/stylelint.config.js +3 -3
  82. package/templates/base/turbo.json +13 -0
  83. package/templates/base/vitest.config.ts +21 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasaloy",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Composable SaaS accelerator kit.",
5
5
  "keywords": [
6
6
  "saas",
@@ -117,9 +117,39 @@
117
117
  "type": "string"
118
118
  }
119
119
  },
120
+ "envServices": {
121
+ "type": "object",
122
+ "description": "Which services read each of this module's `envVars` — a service being a workspace that reads a `.env` (`api`, `web`, `admin`), or `deploy`, the Cloudflare credentials `pnpm deploy:production` loads with no file. `default` covers every key the module declares; a key named alongside it overrides that list for itself. `saasaloy add` groups the key list in `packages/env/.env.example` under a `# @services` line per distinct set. Omit it and the `PUBLIC_` prefix rule decides instead, with a warning naming the module: a prefixed key goes to every client service, everything else to the api.",
123
+ "properties": {
124
+ "default": {
125
+ "type": "array",
126
+ "uniqueItems": true,
127
+ "minItems": 1,
128
+ "items": {
129
+ "type": "string",
130
+ "enum": ["api", "web", "admin", "deploy"]
131
+ }
132
+ }
133
+ },
134
+ "propertyNames": {
135
+ "pattern": "^(default|[A-Z][A-Z0-9_]*)$"
136
+ },
137
+ "additionalProperties": {
138
+ "type": "array",
139
+ "uniqueItems": true,
140
+ "minItems": 1,
141
+ "items": { "type": "string", "enum": ["api", "web", "admin", "deploy"] }
142
+ }
143
+ },
144
+ "envOptional": {
145
+ "type": "array",
146
+ "description": "Keys from `envVars` that may stay empty. `saasaloy add` writes a `# Blank on purpose` line above each one in `packages/env/.env.example`, which is what stops `pnpm env:setup` failing on a key the capability's schema gives a default or marks optional. Every name here must also appear in `envVars`.",
147
+ "uniqueItems": true,
148
+ "items": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]*$" }
149
+ },
120
150
  "devVars": {
121
151
  "type": "object",
122
- "description": "Local-development values for a subset of `envVars`: NAME → the value `saasaloy add` pre-fills into the generated `apps/api/.dev.vars.example`. Only for a value that is the same on every machine and is not a secret (a loopback URL, a fixed port). Every key here must also appear in `envVars`; a key absent here is written with an empty value for the owner to fill in.",
152
+ "description": "Local-development values for a subset of `envVars`: NAME → the value `saasaloy add` pre-fills into the generated `packages/env/.env.example`. Only for a value that is the same on every machine and is not a secret (a loopback URL, a fixed port). Every key here must also appear in `envVars`; a key absent here is written with an empty value for the owner to fill in.",
123
153
  "propertyNames": {
124
154
  "pattern": "^[A-Z][A-Z0-9_]*$"
125
155
  },
@@ -10,9 +10,6 @@ This is a **pnpm workspace monorepo** managed by **Turborepo**.
10
10
  - **Root**: Configuration files, shared tooling
11
11
  - **`apps/*`**: Applications (Next.js, Astro apps - currently empty, will be added)
12
12
  - **`packages/*`**: Shared packages
13
- - **`infra`**: Centralized Cloudflare deployment (Pulumi), if the `infra` capability is installed —
14
- a root-level workspace, not nested under `apps/*` or `packages/*` (it's neither an app nor a
15
- shared package)
16
13
 
17
14
  ### Dev Port Map
18
15
 
@@ -25,7 +22,7 @@ drifting port turns into an unexplained CORS rejection.
25
22
  | `apps/web` (Astro) | 3000 | `http://localhost:3000` | `apps/web/astro.config.mjs` (`server.port`) |
26
23
  | `apps/admin` (TanStack Start) | 3001 | `http://localhost:3001` | `apps/admin/vite.config.ts` (`server.port`, `strictPort`) |
27
24
  | `apps/api` (Hono on Workers) | 4000 | `http://localhost:4000` | `apps/api/wrangler.jsonc` (`dev.port`) |
28
- | Postgres (local, `database-postgres`) | 5432 | `postgres://postgres:postgres@127.0.0.1:5432/app` | `DATABASE_URL` in `.env` |
25
+ | Postgres (local, `database-postgres`) | 5432 | `postgres://postgres:postgres@127.0.0.1:5432/app` | `DATABASE_URL` in `packages/env/.env.example` |
29
26
 
30
27
  The `apps/admin` and `apps/api` rows apply once you run `saasaloy add admin` or
31
28
  `saasaloy add api`.
@@ -36,8 +33,9 @@ Rules for a new app or service:
36
33
  from 4000. Add a row to this table in the same change.
37
34
  - **Set `strictPort` (or the framework's equivalent).** A busy port must fail loudly. A
38
35
  silent `+1` moves the app to an origin nothing allows.
39
- - **Name the origin, don't infer it.** A browser-side caller reads `PUBLIC_API_URL` and
40
- falls back to `http://localhost:4000`; a server-side caller reads its own env var.
36
+ - **Name the origin, don't infer it.** A browser-side caller reads `PUBLIC_API_URL`
37
+ through `webEnv()` or `adminEnv()`, with no fallback — an unset key fails the build
38
+ naming the key. A server-side caller reads its own key through `apiEnv(c.env)`.
41
39
  - **Update both allow-lists when you add a browser origin**: `DEV_ORIGINS` in
42
40
  `apps/api/src/index.ts` and `DEV_ORIGINS` in `packages/auth/src/auth.ts`.
43
41
 
@@ -90,16 +88,70 @@ without one is silently skipped by `turbo run clean` and leaves stale build outp
90
88
  and expecting it to mount: nothing globs that folder, and the entry file's `AppType` is
91
89
  what the typed `hc` client reads.
92
90
 
93
- ### The `@repo/ui` Design Layer
91
+ ### The `@repo/env` Environment Layer
92
+
93
+ `packages/env` owns every environment value this project reads. One tracked key list, one
94
+ command that distributes it, one typed gate in front of the capabilities.
95
+
96
+ - **`packages/env/.env.example` is the key list**, and it is the only example file. It is
97
+ tracked, and `saasaloy add` regenerates it from the installed modules' own descriptions.
98
+ A value you type in it survives that, and nothing is ever removed.
99
+ - **`packages/env/.env` holds the values**, and it is gitignored. `saasaloy env` prompts
100
+ for each unset key and writes it.
101
+ - **`pnpm env:setup` writes every service's own `.env`** from those two files. A *service*
102
+ is a workspace that reads a `.env` — `api`, `web`, `admin` — and
103
+ `packages/env/src/services.ts` is the table. The `deploy` row has no file: it holds the
104
+ Cloudflare credentials a deploy loads in memory.
105
+ - **`pnpm deploy:production` deploys every Worker.** It checks every service's production
106
+ values first, then runs `turbo run deploy`, where each deployable workspace runs
107
+ `env-deploy <service>`: build with the production values in the environment, then
108
+ `wrangler deploy --secrets-file`. The `local` source reads production values from the
109
+ gitignored `packages/env/.env.prod`. Nothing writes a production value under `apps/`.
110
+ - **`createEnv` is the gate.** `apiEnv(c.env)` on the server, `webEnv()` in a frontend.
111
+ It validates on the first call and throws once naming every failing key, its module and
112
+ what the key is for.
113
+ - **A `client` key must start with `PUBLIC_`**, and the type system enforces it. Client
114
+ code reading a server key throws by name.
115
+ - **There is no `.dev.vars`.** Every runtime here reads `.env`. A leftover `.dev.vars`
116
+ hides `.env` from wrangler without warning, so `saasaloy doctor` reports one and
117
+ `pnpm env:setup` carries its values across and deletes it.
118
+ - **Never read `process.env`** in a file a Worker imports. Node tooling reads it freely.
119
+
120
+ The `saasaloy-env` skill is the runbook: adding a key, the section rule, the server/client
121
+ split, blank-on-purpose keys, and the offline rule.
94
122
 
95
- `packages/ui` owns the design layer: the Tailwind 4 theme (`src/styles/globals.css`),
96
- the `cn()` helper, the vendored [shadcn](https://ui.shadcn.com) primitives in
97
- `src/components/`, the marketing **blocks** in `src/blocks/`, the store-bound
98
- **containers** in `src/containers/`, and the landing page's copy in `src/content/`.
99
- `apps/web` pulls the theme in once, through the shared layout that imports
100
- `@repo/ui/globals.css`.
123
+ ### The `@repo/ui` Design Layer
101
124
 
102
- `DESIGN.md` at the repository root records the token values and the rules behind them. Read it before you add or change UI.
125
+ `packages/ui` owns the design layer: the Tailwind 4 theme in `src/styles/`, the `cn()`
126
+ helper, the vendored [shadcn](https://ui.shadcn.com) primitives in `src/components/`, the
127
+ marketing **blocks** in `src/blocks/`, the store-bound **containers** in
128
+ `src/containers/`, and the landing page's copy in `src/content/`.
129
+
130
+ Those layer folders are the whole top level of `src/`: `assets` (images, SVGs and icons, added when the first one arrives), `blocks`, `components`, `containers`, `content`, `lib` and `styles`. Do not add an app folder such as `src/admin/` or `src/web/` beside them. When one app needs its own files inside a layer, give that layer a child folder (`blocks/admin/`, `components/shared/`).
131
+
132
+ The theme is one shared file plus one theme file per app:
133
+
134
+ - **`src/styles/globals.css`** is the shared layer: the Tailwind imports, the `@source`
135
+ globs and the base layer. It holds no token values, and no app imports it directly.
136
+ - **`src/styles/web.css`** is the web app's theme: the `dark` custom variant, its `:root`
137
+ and `.dark` token values, its `@theme inline` colour mapping and its radius scale. It
138
+ imports `globals.css`, and `apps/web` pulls it in once, through the shared layout that
139
+ imports `@repo/ui/styles/web.css`. `components.json` points here, so a shadcn preset
140
+ lands here. The landing page's layout, type and motion utilities sit at the end of the
141
+ file, after the colour tokens, and a preset leaves them alone.
142
+ - **`src/styles/admin.css`** is the admin app's theme, in the same shape, written by
143
+ `saasaloy add admin`.
144
+
145
+ Each theme file repeats the `dark` variant and the `@theme inline` mapping on purpose.
146
+ shadcn merges a preset in place only when the target file itself holds them, and it does
147
+ not look through an `@import`. Move either one into `globals.css` and a preset appends a
148
+ duplicate to `web.css` instead.
149
+
150
+ **Each app has its own `DESIGN.md`, in its own folder.** `apps/web/DESIGN.md` records the
151
+ web tokens in `web.css` and the rules behind them, and fingerprints `web.css`. An app
152
+ that carries its own theme file gets its own `apps/<app>/DESIGN.md`, which fingerprints
153
+ that file. There is no `DESIGN.md` at the repository root. Read the document for the app
154
+ you are changing before you add or change UI.
103
155
 
104
156
  Primitives, blocks and containers are reached by subpath — none is re-exported from the
105
157
  package root, so importing one never drags in the rest. The root export is project-wide
@@ -152,7 +204,7 @@ pnpm --filter @repo/ui exec shadcn add dialog
152
204
  imports and a blanket glob charges every app for every primitive. A primitive the
153
205
  landing page or `src/blocks/` renders gets an `@source "../components/<name>.tsx";` line
154
206
  in `globals.css`; a primitive only one app renders gets its `@source` line in that app's
155
- own stylesheet, the way `apps/admin/src/styles/admin.css` takes the whole directory.
207
+ own theme file, the way `src/styles/admin.css` takes the whole directory.
156
208
  Nothing errors when you skip this — the classes just never reach the CSS.
157
209
  - `style` is `base-nova` (Base UI) and is fixed at init — the CLI cannot change it later.
158
210
  - `rsc` is `false`, so the CLI strips the `"use client"` directive for you. It means
@@ -160,8 +212,10 @@ pnpm --filter @repo/ui exec shadcn add dialog
160
212
 
161
213
  Primitives are source you own. Edit them in place rather than wrapping them.
162
214
 
215
+ **No UI kit or component library as a dependency.** When a design comes from a kit (Tailark, coss ui, a licensed dashboard kit), copy or rebuild the component into `src/components/` or `src/blocks/` as source this project owns. Do not install the kit's package, and keep no link to its repository for later updates. A copied component changes by hand, like any other file here. The shadcn CLI above is the one sanctioned way to fetch a primitive, because it writes source and leaves no runtime dependency behind.
216
+
163
217
  **Swapping the whole theme for a preset.** The token set in
164
- `packages/ui/src/styles/globals.css` is shadcn's `neutral` with `cssVariables: true`, and
218
+ `packages/ui/src/styles/web.css` is shadcn's `neutral` with `cssVariables: true`, and
165
219
  any shadcn **`registry:style`** item is a drop-in replacement for it. The same
166
220
  `--filter … exec` form applies — there is no separate theme command:
167
221
 
@@ -170,10 +224,11 @@ pnpm --filter @repo/ui exec shadcn add https://tweakcn.com/r/themes/modern-minim
170
224
  ```
171
225
 
172
226
  The CLI merges the preset's `:root`, `.dark` and `@theme inline` blocks **into** the
173
- existing ones, so the file's hand-written parts — the `@source` rules, the
174
- `@custom-variant dark`, the `@layer base` rules — stay put, and `components.json` is not
175
- touched. It usually *extends* `@theme inline` with mappings the base does not carry
176
- (fonts, tracking, shadows); that is expected.
227
+ existing ones in `web.css` and keeps its `@custom-variant dark`, so the hand-written
228
+ parts in `globals.css` — the `@source` rules and the `@layer base` rules — stay put, and
229
+ `components.json` is not touched. The preset reaches the web app only; the admin theme
230
+ in `admin.css` keeps its own values. It usually *extends* `@theme inline` with mappings
231
+ the base does not carry (fonts, tracking, shadows); that is expected.
177
232
 
178
233
  Two places to get an item from:
179
234
 
@@ -185,19 +240,78 @@ Two places to get an item from:
185
240
  Neither is special: the mechanism is the `registry:style` shape, so any URL that serves
186
241
  one works.
187
242
 
188
- **This edits a base file, and base files have no update path.** Saasaloy hands you the
189
- template once; it never comes back to migrate it. A swapped theme is yours to maintain,
190
- including re-applying anything you had customised in `globals.css` that the preset
191
- overwrote. Diff the file after running the command rather than assuming.
192
-
193
- A preset swap invalidates `DESIGN.md`, which records the token values the old theme had. The `saasaloy-design` skill's `theme` flow runs the command above for you and re-derives the contract from the merged result, so prefer it over running the command by hand. Pick one or the other — running both applies the preset twice.
194
-
195
- Light/dark/system switching is unaffected by any of this — it keys off the `.dark` class,
196
- which every preset keeps.
197
-
198
- **Blocks — the page-level compositions.** `src/blocks/` holds the marketing blocks the
199
- landing page is built from: `navbar`, `hero`, `feature-grid`, `pricing-table`, `faq`,
200
- `cta`, `footer`. The rules are not stylistic — each one prevents a real failure:
243
+ **This edits a file the project owns.** `web.css` is an owned base file: `saasaloy
244
+ update` never overwrites it, and a later template change to it reaches you only through
245
+ the merge plan. A swapped theme is yours to maintain, including re-applying anything you
246
+ had customised in `web.css` that the preset overwrote. Diff the file after running the
247
+ command rather than assuming.
248
+
249
+ A preset swap invalidates `apps/web/DESIGN.md`, which records the token values the old theme had. The `saasaloy-design` skill's `theme` flow runs the command above for you and re-derives the contract from the merged result, so prefer it over running the command by hand. Pick one or the other — running both applies the preset twice.
250
+
251
+ Theme switching is unaffected by any of this. It keys off the `.dark` class, which every preset keeps. The toggle has two states, light and dark: `system` is only the starting state for a visitor who never pressed it, and no press steps back to `system`. Do not give the toggle a third state or a light/dark/system menu.
252
+
253
+ **A project made before the theme split.** Older projects kept the web tokens in
254
+ `globals.css` and the design document at the repository root. `saasaloy update` creates
255
+ `packages/ui/src/styles/web.css` and lists `globals.css` and `Layout.astro` in the merge
256
+ plan, because the project owns both; it never moves your tokens for you. Finish the split
257
+ by hand, in this order:
258
+
259
+ 1. Move the `@custom-variant dark` line, the `:root` block, the `.dark` block and the
260
+ `@theme inline` block from `globals.css` into `web.css`, replacing the template's
261
+ copies there, below its `@import "./globals.css";`. Keep your own values, not the
262
+ template's.
263
+ 2. Add `"./styles/*": "./src/styles/*"` to the `exports` of `packages/ui/package.json`,
264
+ if the update did not.
265
+ 3. In `apps/web/src/layouts/Layout.astro`, import `@repo/ui/styles/web.css` in place of
266
+ `@repo/ui/globals.css`.
267
+ 4. Set `tailwind.css` in `packages/ui/components.json` to `src/styles/web.css`, if the
268
+ update did not.
269
+ 5. Move `DESIGN.md` to `apps/web/DESIGN.md`, and run the `saasaloy-design` skill's
270
+ `update` flow so its fingerprint names `web.css`.
271
+ 6. Run `pnpm build` and check that `globals.css` and `web.css` hold one `@theme inline`
272
+ and one `@custom-variant dark` between them.
273
+
274
+ With the admin app installed, `saasaloy update` also moves its theme: it creates
275
+ `packages/ui/src/styles/admin.css` and deletes `apps/admin/src/styles/admin.css`, or
276
+ keeps it with a warning when you edited it. Carry any edit across to the new file, then
277
+ delete the old one. The new admin app imports `@repo/ui/styles/admin.css`, so its build
278
+ fails until step 2 adds the `./styles/*` export. Update the base and finish step 2 before
279
+ you build admin, and before you run `saasaloy add admin` on a project made before the
280
+ split.
281
+
282
+ **Blocks — the page-level compositions.** `src/blocks/` holds the thirteen marketing
283
+ blocks the landing page is built from: `navbar`, `hero`, `logo-cloud`, `pain-points`,
284
+ `how-it-works`, `feature-grid`, `showcase`, `audience`, `testimonials`, `pricing-table`,
285
+ `faq`, `cta` and `footer`. `logo-cloud`, `pain-points`, `how-it-works`, `showcase`,
286
+ `audience` and `testimonials` are new; a project scaffolded before them gets the files
287
+ through `saasaloy update`, and adds each to its own `index.astro` by hand, since the page
288
+ is the owner's. The blocks need the landing utilities at the end of `web.css`. `web.css`
289
+ is an owned file, so `update` lists it in the merge plan and never adds them for you.
290
+ Copy the template's block, from the comment that opens "The landing page's layout" to
291
+ the `stylelint-enable` line, to the end of your `web.css`. A project made before the
292
+ theme split skips this: `update` creates its `web.css` from the template, block included.
293
+
294
+ **The section recipe.** `index.astro` composes the page from explicit imports, one line
295
+ per section. To remove a section, delete its line and its import, delete its file in
296
+ `src/blocks/`, and blank its navbar and footer labels in `landing.ts`. To reorder, move
297
+ the lines. Every block uses the same container (`section-container`, `-narrow`, `-wide`)
298
+ and spacing (`section-y`) utilities from `src/styles/web.css`, so any order keeps one
299
+ rhythm. A block sets no width, no section padding and no keyframes of its own; motion
300
+ comes from the named utilities there (`blur-in`, `beam-in`, `reveal`, `marquee`,
301
+ `shine-text`, `nav-float`, `hover-lift`), and every one stops under reduced motion.
302
+
303
+ **The logo recipe.** `landing.logos.items[]` is `{ id, name, mark }`. `mark` is a key in
304
+ `LOGO_MARKS` at the top of `blocks/logo-cloud.tsx`. Paste a customer's SVG there under a
305
+ kebab-case key with `fill="currentColor"`, then name the key in content. An unknown key
306
+ renders `name` as a text wordmark.
307
+
308
+ **The production bar.** The page holds to a rubric: no axe violation at WCAG 2.1 AA in
309
+ either theme, no sideways scroll from 360px up, nothing animating under reduced motion, a
310
+ skip link to `<main>` on every page, link-preview metadata on `/`, and exactly two islands
311
+ (`Navbar`, `PricingTable`). Keep each true when you change a block: a new section with
312
+ state is a third island and needs a reason.
313
+
314
+ The rules below are not stylistic — each one prevents a real failure:
201
315
 
202
316
  - **One block, one file, one component export (plus its prop types).**
203
317
  `pricing-table.tsx` exports `PricingTable` alongside `PricingTier` and
@@ -222,13 +336,13 @@ landing page is built from: `navbar`, `hero`, `feature-grid`, `pricing-table`, `
222
336
  `client:load` on the page hydrates everything and throws away the reason this is a
223
337
  static site — see `apps/web/src/pages/index.astro` for the worked example.
224
338
  - **`theme-toggle` is the exception that proves the rule.** It sits in `src/blocks/`
225
- beside the marketing blocks but is chrome, not copy: `index.astro` places it as a
226
- sibling of `<Navbar />` and it takes **no** client directive despite being
227
- interactive. It has no `onClick` and no state — the pre-paint inline script the shared
228
- layout emits (`THEME_INIT_SCRIPT`, from `packages/ui/src/lib/theme.ts`) drives every
339
+ beside the marketing blocks but is chrome, not copy: `navbar.tsx` renders it inside the
340
+ bar, and it needs **no** client directive of its own despite being interactive. It has
341
+ no `onClick` and no state — the pre-paint inline script the shared layout emits
342
+ (`THEME_INIT_SCRIPT`, from `packages/ui/src/lib/theme.ts`) drives every
229
343
  `[data-theme-toggle]` on the page through one delegated listener, and CSS picks the
230
344
  icon off `<html data-theme>`. Move it, restyle it via its `className`, or delete the
231
- one line in `index.astro` to drop it. Do **not** "fix" it by adding `client:*` or an
345
+ one line in `navbar.tsx` to drop it. Do **not** "fix" it by adding `client:*` or an
232
346
  `onClick`, and do not paste a second copy of the boot script into a page: any document
233
347
  that renders the block must inline that one constant in its `<head>`, or the control
234
348
  stays hidden.
@@ -259,6 +373,8 @@ landing page is built from: `navbar`, `hero`, `feature-grid`, `pricing-table`, `
259
373
  React file under `apps/web/src/components/` builds the client and passes the function
260
374
  down. Astro serializes island props, so the page renders **that** file, never the block
261
375
  directly.
376
+ - **A link that leaves the site opens a new tab.** Spread `externalLinkProps(href)` from `@repo/ui/lib/external-link` onto the anchor and render `newTabHint(href)` inside it. The href decides: an absolute URL gets `target="_blank"`, `rel="noopener noreferrer"` and a screen-reader hint, and a path or a same-page anchor stays in the tab. Do not write `target="_blank"` by hand.
377
+ - **A value that waits on a request shows a skeleton.** Render the `Skeleton` primitive at the size the value will take until the response arrives, never a blank gap or a spinner. Add it with `pnpm --filter @repo/ui exec shadcn add skeleton` the first time a block needs it.
262
378
  - **A block never imports a store either.** Local state is fine; shared state is not. See
263
379
  the containers section below.
264
380
  - **A module's UI arrives here too, and you wire it up by hand.** `saasaloy add <module>`
@@ -328,20 +444,27 @@ record and nothing else. Follow them when you add a block or a key:
328
444
  messages (`ui.pricing.perMonth`, `ui.pricing.perMonthAnnual`) — word order does not
329
445
  survive the seam in every language.
330
446
  5. **Only user-visible strings move.** Section `id`s and the same-page anchors pointing at
331
- them are structure and stay in the block. Three things break the rule, all because a
447
+ them are structure and stay in the block. A few things break the rule, all because a
332
448
  thing rewritten *with* the copy belongs *near* the copy: the whole `tiers` array (prices
333
- and `ctaHref`s included); each feature's `icon`, held as a registry *name* like `"zap"`
334
- and resolved to a component by the map at the top of `blocks/feature-grid.tsx`; and the
335
- two outbound calls to action, `landing.navbar.ctaHref` and
336
- `landing.cta.primaryActionHref`/`.secondaryActionHref`, which leave the page and so
337
- cannot break a section link. A translation layer reads `id`, `icon` and every `*Href` as
338
- non-message data.
449
+ and `ctaHref`s included); every `icon`, held as a registry *name* like `"zap"` and
450
+ resolved to a component by `packages/ui/src/lib/icons.ts`, and every logo `mark`,
451
+ resolved by `blocks/logo-cloud.tsx`; every outbound link (`*Href`), which leaves the
452
+ page and so cannot break a section link; and the code the page quotes (terminal
453
+ commands, module names, file paths). A translation layer reads `id`, `icon`, `mark`,
454
+ every `*Href` and the quoted code as non-message data. A pair of backticks inside a
455
+ message renders as `<code>` through `inlineCode()` from `@repo/ui/lib/inline-code`.
456
+
457
+ **No copy that goes stale.** Do not write a version number, a release date or a count into a message. Say something general and link to where the current value lives: the hero pill says "See what's new" and links to the changelog, so it stays true across every release.
339
458
 
340
459
  Two consequences worth knowing. Blanking a navbar or footer link's label in content drops
341
460
  that link, which is how a removed section loses its nav entry without editing a block. And
342
461
  the theme toggle's labels deliberately stay in `packages/ui/src/lib/theme.ts`: that file is
343
462
  inlined verbatim into a pre-paint `<script>` and is import-free on purpose.
344
463
 
464
+ **The default copy is Saasaloy's own.** A fresh project's page describes Saasaloy, and
465
+ `siteName` in `packages/ui/src/index.ts` defaults to `"Saasaloy"`, so the brand and the
466
+ words agree until you replace both. The two skills below replace them in one pass.
467
+
345
468
  **Making this project yours.** Three skills ship with the base (linked at
346
469
  `.claude/skills/`, real files in `.agents/skills/`). The first two run in order:
347
470
 
@@ -356,12 +479,12 @@ inlined verbatim into a pre-paint `<script>` and is import-free on purpose.
356
479
 
357
480
  The third has no place in that order, because it runs whenever the UI moves:
358
481
 
359
- 3. **`saasaloy-design`** keeps `DESIGN.md` true. Its `theme` flow swaps the preset and
360
- re-derives the contract; `update` re-derives after you change `globals.css` or add
361
- components; `audit` reports where the code and the contract disagree. It reads the
482
+ 3. **`saasaloy-design`** keeps `apps/web/DESIGN.md` true. Its `theme` flow swaps the
483
+ preset and re-derives the contract; `update` re-derives after you change `web.css` or
484
+ add components; `audit` reports where the code and the contract disagree. It reads the
362
485
  product brief when one exists and never writes it.
363
486
 
364
- Invoke them rather than editing eight blocks by hand — and if you do edit by hand, keep the
487
+ Invoke them rather than editing thirteen blocks by hand — and if you do edit by hand, keep the
365
488
  strings in the content module so the next pass finds them.
366
489
 
367
490
  ### Naming Conventions
@@ -418,13 +541,15 @@ hook (which is also how you keep hooks out of CI).
418
541
  - Run type checking: `pnpm typecheck` (must pass before commits)
419
542
  - Run linting: `pnpm lint` (see above — it reports, `pnpm lint:fix` fixes)
420
543
  - Check formatting: `pnpm format:check`, or `pnpm format` to rewrite
421
- - There is no `pnpm test` at the root, and no workspace declares a `test` script. The base ships no test runner: pick one and add it per workspace when you have something to test.
544
+ - Run the tests: `pnpm test`. It runs `turbo run test` (each workspace's own `test` script), then `vitest run` from the root, whose `vitest.config.ts` treats every workspace under `apps/*` and `packages/*` as a project. Write new tests for Vitest: put a `*.test.ts` file beside the code, and give the workspace a `vitest.config.ts` only when it needs setup of its own. Do not add a per-workspace `test` script that calls Vitest, or the file runs twice. `packages/env` is the exception: its tests run on `node --test` through its own `test` script, and the root config leaves it out.
545
+ - Before you hand over UI work, run `pnpm dev`, load the pages you changed, and read the dev server's terminal output. Fix every error and warning it prints. A clean build does not prove a clean dev server.
422
546
 
423
547
  ## Boundaries
424
548
 
425
549
  ### ✅ Always Do
426
550
 
427
- - Read `DESIGN.md` before writing or changing UI
551
+ - Read the app's `DESIGN.md` (`apps/web/DESIGN.md` for the web app) before writing or
552
+ changing UI
428
553
  - Run `pnpm typecheck` before committing code changes
429
554
  - Run `pnpm lint` and fix all errors
430
555
  - Give every new app or package a `clean` script backed by `rimraf` (see above)
@@ -434,6 +559,7 @@ hook (which is also how you keep hooks out of CI).
434
559
  ### ⚠️ Ask First
435
560
 
436
561
  - Adding new dependencies (especially to root `package.json`)
562
+ - Building new UI before the owner has approved a mockup. Show screenshots of the mockup first and get approval. A change after approval gets a new mockup, not a fix after the work lands
437
563
  - Modifying Turborepo configuration (`turbo.json`)
438
564
  - Changing TypeScript strictness settings
439
565
  - Modifying the husky hooks (`.husky/`), `lint-staged.config.js`, or `commitlint.config.js`
@@ -447,11 +573,12 @@ hook (which is also how you keep hooks out of CI).
447
573
 
448
574
  - Never use `npm` or `npx`, instead use `pnpm` & `pnpm dlx`
449
575
  - Never use `rm -rf` in a package script — it breaks on Windows; use `rimraf`
450
- - Commit secrets, API keys, or environment variables
576
+ - Commit secrets or API keys. `packages/env/.env` is gitignored; `packages/env/.env.example`
577
+ is tracked and holds local defaults only
451
578
  - Modify `node_modules/` or `pnpm-lock.yaml` manually (use `pnpm install`)
452
579
  - Remove or disable TypeScript strict mode
453
580
  - Remove or disable the lint-staged or commitlint hooks, or commit with `--no-verify`
454
581
  as a habit rather than an emergency
455
582
  - Use `any` type without explicit `@ts-expect-error` or `@ts-ignore` with justification
456
- - Break the workspace structure (don't move packages outside `apps/*`, `packages/*`, or `infra`)
583
+ - Break the workspace structure (don't move packages outside `apps/*` or `packages/*`)
457
584
  - Commit without running type checks and linting
@@ -30,7 +30,7 @@ saasaloy list # what the registry offers, and what you already hav
30
30
 
31
31
  ## UI components
32
32
 
33
- `DESIGN.md` records the project's design tokens and UI rules for people and agents.
33
+ `apps/web/DESIGN.md` records the web app's design tokens and UI rules for people and agents.
34
34
 
35
35
  `packages/ui` ships a Tailwind 4 theme, a small set of [shadcn](https://ui.shadcn.com)
36
36
  primitives, and the marketing blocks the landing page is built from. Import primitives and
@@ -54,14 +54,26 @@ Components land in `packages/ui/src/components/` as source you own and can edit.
54
54
 
55
55
  ## Landing page
56
56
 
57
- `apps/web/src/pages/index.astro` composes the blocks in `packages/ui/src/blocks/` —
58
- `navbar`, `hero`, `feature-grid`, `pricing-table`, `faq`, `cta`, `footer`. Each is one
59
- self-contained `.tsx` with its copy as in-file defaults, so editing the file is how you
60
- change the page.
57
+ `apps/web/src/pages/index.astro` composes thirteen blocks from `packages/ui/src/blocks/`: `navbar`, `hero`, `logo-cloud`, `pain-points`, `how-it-works`, `feature-grid`, `showcase`, `audience`, `testimonials`, `pricing-table`, `faq`, `cta` and `footer`. The page ships describing Saasaloy itself, so it reads as a finished product on day one. You make it yours with two edits and no others:
61
58
 
62
- Blocks render to static HTML by default. Only the three that need browser state carry a
63
- client directive (`Navbar` is `client:idle`; `PricingTable` and `Faq` are
64
- `client:visible`) — the rest ship no JavaScript at all. Don't reach for `client:load`.
59
+ 1. **Rewrite the words** in `packages/ui/src/content/landing.ts`: every heading, link, price and icon name. The `saasaloy-landing-copy` skill drafts it from your product brief. The brand is `siteName` in `packages/ui/src/index.ts`, which `saasaloy-setup` sets.
60
+ 2. **Choose the sections** in `index.astro`. Each section is one import and one line.
61
+
62
+ ### The section recipe
63
+
64
+ - **Remove a section:** delete its line and its import in `index.astro`, then delete its file in `packages/ui/src/blocks/`. Blank its navbar and footer labels in `landing.ts` so no link points at a section that is gone.
65
+ - **Reorder:** move the lines. Every block uses the same container and spacing utilities, so any order keeps one rhythm.
66
+ - **Bring one back:** copy the block file in from a fresh `saasaloy init`, then add the import and the line.
67
+
68
+ ### The logo recipe
69
+
70
+ The logo row names each logo in `landing.logos.items` as `{ id, name, mark }`. `mark` is a key in the `LOGO_MARKS` registry at the top of `packages/ui/src/blocks/logo-cloud.tsx`. To add a customer's logo, paste its SVG into that registry under a kebab-case key with `fill="currentColor"`, then put the key in the item's `mark`. A key the registry doesn't know renders the item's `name` as a text wordmark, which is what the default row does.
71
+
72
+ ### What the page promises
73
+
74
+ Blocks render to static HTML by default. Only two carry a client directive: `Navbar` (`client:idle`) and `PricingTable` (`client:visible`). The FAQ and the pain points open with native `<details>`, all motion is CSS and stops under reduced motion, and the footer adds one inline script that writes the current year. Don't reach for `client:load`.
75
+
76
+ The page is checked against a written rubric: no axe violation at WCAG 2.1 AA in either theme, no sideways scroll from 360px up, a skip link on every page, and link-preview metadata on `/`. Keep those true when you change a block.
65
77
 
66
78
  The page composes itself from explicit imports, and nothing discovers a file behind your
67
79
  back. A module that ships UI writes its block into `packages/ui/src/blocks/` like every
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: saasaloy-design
3
- description: Keep DESIGN.md true of the Saasaloy design layer. Use theme to choose and apply a registry style, update after packages/ui changes, or audit the design contract for token drift.
3
+ description: Keep apps/web/DESIGN.md true of the Saasaloy design layer. Use theme to choose and apply a registry style, update after packages/ui changes, or audit the design contract for token drift.
4
4
  derived-from: "designkit (MIT), narrowed 2026-08-09"
5
5
  ---
6
6
 
@@ -8,7 +8,9 @@ derived-from: "designkit (MIT), narrowed 2026-08-09"
8
8
 
9
9
  This skill records the design system that the project already uses. It writes no components and no pages. Use `uikit` for that work.
10
10
 
11
- The token home is `packages/ui/src/styles/globals.css`. The design contract is `DESIGN.md` at the project root.
11
+ The token home is `packages/ui/src/styles/web.css`. The design contract is `apps/web/DESIGN.md`. `web.css` imports `packages/ui/src/styles/globals.css`, which holds the shared Tailwind setup and no token values.
12
+
13
+ This skill covers the web app only. Another app's theme file, such as the admin module's `packages/ui/src/styles/admin.css`, is out of scope. Do not read its tokens into this contract, and do not edit it.
12
14
 
13
15
  The grounding rule controls every mode. Every token value must exist in the project code or in the Tailwind utility that the project uses. Do not choose a nearby value because it looks cleaner.
14
16
 
@@ -24,15 +26,15 @@ If the user does not name a mode, ask which mode to use. Do not choose between a
24
26
 
25
27
  | Mode | Files |
26
28
  |------|-------|
27
- | `theme` | `packages/ui/src/styles/globals.css`, `DESIGN.md`, and `docs/product-brief.md` only when the brief is absent |
28
- | `update` | `DESIGN.md` only |
29
+ | `theme` | `packages/ui/src/styles/web.css`, `apps/web/DESIGN.md`, and `docs/product-brief.md` only when the brief is absent |
30
+ | `update` | `apps/web/DESIGN.md` only |
29
31
  | `audit` | none |
30
32
 
31
33
  Do not edit `components.json` during a theme change. Do not add a dependency, script, component, page, or Tailwind class.
32
34
 
33
35
  ## Shared rules
34
36
 
35
- Read `DESIGN.md`, `packages/ui/src/styles/globals.css`, and all files under `packages/ui/src/` before you derive tokens. Read the application files that use `@repo/ui` when you need to confirm usage.
37
+ Read `apps/web/DESIGN.md`, `packages/ui/src/styles/web.css`, `packages/ui/src/styles/globals.css`, and all files under `packages/ui/src/` before you derive tokens. Read the application files that use `@repo/ui` when you need to confirm usage.
36
38
 
37
39
  Find the system in this order. Stop at the first match.
38
40
 
@@ -50,6 +52,15 @@ Write elevation as prose under `## Elevation & Depth`. Never add `elevation` to
50
52
 
51
53
  Write motion as prose under `## Motion`. The alpha schema has no motion token group and no component transition token.
52
54
 
55
+ The landing page's layout, type and motion vocabulary lives at the end of `web.css`, after the colour tokens, and `DESIGN.md` must name all of it:
56
+
57
+ - The type ramp in `@theme`: `--text-display`, `--text-section-title` and `--text-lead`. Record the largest size of each fluid `clamp()` step as the token's `fontSize`, and describe the fluid range in the Typography prose.
58
+ - The container widths in `@theme` (`--container-page`, `--container-narrow`, `--container-wide`) and the `section-container*` and `section-y` utilities. Describe them under `## Layout`. Spacing stays omitted, because these are named widths and one section rhythm, not a spacing scale.
59
+ - The motion utilities: `blur-in`, `beam-in`, `reveal`, `marquee`, `shine-text`, `nav-float` and `hover-lift`. Describe each under `## Motion`, and say that each one stops under reduced motion.
60
+ - The decoration utility `grid-pattern`. Describe it under `## Components` with the landing blocks it decorates.
61
+
62
+ A theme preset replaces the colour tokens and leaves this vocabulary alone. After a `theme` run, check that the vocabulary survived and that `DESIGN.md` still describes it.
63
+
53
64
  Use only these component token keys: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, and `width`.
54
65
 
55
66
  Keep the sections in this order: Overview, Colors, Typography, Layout, Elevation & Depth, Shapes, Components, Do's and Don'ts, Motion, Dark Mode.
@@ -58,24 +69,24 @@ The seed is a one-time base file. ADR 0022 means Saasaloy has no update path for
58
69
 
59
70
  ## Fingerprint
60
71
 
61
- The fingerprint covers only `packages/ui/src/styles/globals.css`. It does not cover components, blocks, or other files.
72
+ The fingerprint covers only `packages/ui/src/styles/web.css`. It does not cover `globals.css`, components, blocks, or other files.
62
73
 
63
74
  Compute the fingerprint from the file bytes. Use the first 12 lowercase hexadecimal characters of SHA-256.
64
75
 
65
76
  ```sh
66
- node --input-type=module -e "import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; process.stdout.write(createHash('sha256').update(readFileSync('packages/ui/src/styles/globals.css')).digest('hex').slice(0, 12))"
77
+ node --input-type=module -e "import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; process.stdout.write(createHash('sha256').update(readFileSync('packages/ui/src/styles/web.css')).digest('hex').slice(0, 12))"
67
78
  ```
68
79
 
69
80
  The scaffold seed ends with this stamp.
70
81
 
71
82
  ```markdown
72
- _Seeded from the saasaloy base template · CLI <version> · tokens sha256:<12 hex> of packages/ui/src/styles/globals.css_
83
+ _Seeded from the saasaloy base template · CLI <version> · tokens sha256:<12 hex> of packages/ui/src/styles/web.css_
73
84
  ```
74
85
 
75
86
  After `theme` or `update`, replace the seed stamp with this stamp.
76
87
 
77
88
  ```markdown
78
- _Updated from packages/ui on YYYY-MM-DD · tokens sha256:<12 hex> of packages/ui/src/styles/globals.css_
89
+ _Updated from packages/ui on YYYY-MM-DD · tokens sha256:<12 hex> of packages/ui/src/styles/web.css_
79
90
  ```
80
91
 
81
92
  Do not change the fingerprint during `audit`.
@@ -85,7 +96,7 @@ Do not change the fingerprint during `audit`.
85
96
  Run the official linter after each write.
86
97
 
87
98
  ```sh
88
- pnpm dlx @google/design.md lint DESIGN.md
99
+ pnpm dlx @google/design.md lint apps/web/DESIGN.md
89
100
  ```
90
101
 
91
102
  The format is alpha. Read the current schema when a lint finding conflicts with this skill.
@@ -126,13 +137,13 @@ Use the package-local executable from the project root.
126
137
  pnpm --filter @repo/ui exec shadcn add <registry-style-url>
127
138
  ```
128
139
 
129
- Confirm that the `@source` rules, `@custom-variant dark`, and `@layer base` remain in `globals.css`. Confirm that `components.json` did not change.
140
+ `components.json` points shadcn at `web.css`, so the preset merges into the `:root`, `.dark` and `@theme inline` blocks there. Confirm that `globals.css` did not change and still holds the `@source` rules and `@layer base`. Confirm that `web.css` and `globals.css` hold one `@theme inline` and one `@custom-variant dark` between them. Confirm that `components.json` did not change.
130
141
 
131
142
  Stop and report the failure when the preset removes a required rule or changes `components.json`. Do not repair an unknown preset merge without approval.
132
143
 
133
144
  ### 4. Re-derive the contract
134
145
 
135
- Run the shared extraction process against the merged `globals.css` and its usage. Update the token front matter and only the prose sections that the new tokens affect.
146
+ Run the shared extraction process against the merged `web.css` and its usage. Update the token front matter and only the prose sections that the new tokens affect.
136
147
 
137
148
  Use the product brief to update Overview and Do's and Don'ts. Do not add product claims that the brief does not support.
138
149
 
@@ -168,7 +179,7 @@ Audit is read-only. Do not edit or format any file.
168
179
 
169
180
  ### 1. Check the fingerprint offline
170
181
 
171
- Read the recorded 12-character fingerprint from the final stamp. Compute the current fingerprint from `globals.css`.
182
+ Read the recorded 12-character fingerprint from the final stamp. Compute the current fingerprint from `web.css`.
172
183
 
173
184
  - `current` means the two fingerprints match.
174
185
  - `stale` means they differ.
@@ -182,7 +193,7 @@ Use `orphaned` for a documented token with no current source. Use `uncovered` fo
182
193
 
183
194
  ### 3. Lint when possible
184
195
 
185
- Run `pnpm dlx @google/design.md lint DESIGN.md` when the tool and network are available. If it cannot run, state that the fingerprint check ran offline and structural lint did not run.
196
+ Run `pnpm dlx @google/design.md lint apps/web/DESIGN.md` when the tool and network are available. If it cannot run, state that the fingerprint check ran offline and structural lint did not run.
186
197
 
187
198
  ### 4. Report
188
199
 
@@ -192,7 +203,7 @@ Open with the file count that the audit scanned. Give one verdict for the finger
192
203
 
193
204
  - Never write a component or a page. Use `uikit` for that work.
194
205
  - Never add a token value that the project does not contain.
195
- - Never change `globals.css` in `update` or `audit`.
206
+ - Never change `globals.css` in any mode, or `web.css` in `update` or `audit`.
196
207
  - Never write during `audit`.
197
208
  - Never claim that lint passed when the linter did not run.
198
209
  - Never treat a matching fingerprint as proof that component usage has no drift.