# `src/sass` — Implementation Plan
**Input:** [SASS_ARCHITECTURE_REVIEW.md](SASS_ARCHITECTURE_REVIEW.md) (Phase A)
**Scope:** `src/sass/**`, `Theme.php`, `DartSass.php`, `JHSCompiler.php`, consumer `.jhs` files, docs
**Baseline version:** `mmo.client` 3.0.13
**Status:** plan only — no code changed.
Every fix below was validated by **simulating it on a copy of `src/sass` and compiling with the vendored
`vendor/dart/sass`**. Claims marked **[verified]** were produced that way; the rest are specification.
---
## Part 0 — Corrections to Phase A
Mandatory before planning: four Phase A statements were wrong or incomplete.
### C1 — Dark mode is a shipped, user-facing feature (raises FIX-001 from latent to visible)
Phase A asked *"is theme switching expected to be the application's job?"* and said `.theme-dark` was
unreachable. **Wrong.** I grepped `src/js`; the logic lives in a `.jhs` file:
```text
mmo-app.jhs:53 ← moon icon, "Thème sombre"
mmo-app.jhs:914-923 setThemeMode() → document.body.classList.add('theme-' + scheme)
→ localStorage.setItem('themeMode', scheme)
→ dispatchEvent(window, 'theme', scheme)
```
So dark mode has a toggle in the app shell's user card, persists across reloads, and emits a `theme`
event. **Every user who has ever flipped that switch has been looking at the broken palette.**
FIX-001 stops being a latent defect and becomes a visible bug fix — and correspondingly, it *will*
change what existing users see.
### C2 — Finding 2's implied fix was wrong; pass-through is load-bearing
Phase A noted `size()` returns unrecognised names verbatim while every other guard `@error`s, implying
it should `@error` too. **That would break the framework.** Actual first-argument census across
`src/sass` + `src/customElements`:
| value | count | must keep working |
|---|---|---|
| `'s' 'xs' 'm' 'l' 'xl' 'xxl'` | 499 | scale words |
| `0` | 67 | ✅ number |
| `50%` | 10 | ✅ number |
| `auto` | 7 | ✅ keyword |
| `2px`, `-8px`, `100px`, `$gap`, `$nav-size` | 5 | ✅ number/variable |
| `'xxs'` | 1 | ❌ **bug** |
| `'0'` (quoted) | 1 | ❌ **bug** — new, not in Phase A |
The fix is a *type-aware* guard, not an unconditional error. And there is a second live instance:
[mmo-notification.jhs:132](src/customElements/elements/mmo-notification.jhs:132) emits
`padding-bottom: "0"`.
### C3 — Finding 4 overstated; the intended model is in-tree co-evolution
Phase A said applications have "no supported extension point at all". Two corrections:
- `Theme::$stylesheet` **is** configurable ([Theme.php:45](src/Theme.php:45)), so an app *can* own the
entry point and `@use 'styles'` from it. **[verified]** — a test app entry compiled to 787 rules vs
the framework's 786, framework styles intact.
- More importantly, I missed the framework's own stated position
([guides/frontEnd/dependencies/sass.md](demo/www/contents/pages/guides/frontEnd/dependencies/sass.md)):
> The styles in `MMO/libs/client/src/sass` are open to change and can be modified if necessary.
> Any changes must be validated by the entire team before implementation.
and [addingTheme.md](demo/www/contents/pages/guides/frontEnd/themes/addingTheme.md) instructs
authors to add theme files **inside** `/libs/client/src/sass/themes`.
So this is an in-house framework where app and framework evolve together, and "fork the tree" is the
*documented* workflow, not a workaround. That lowers the priority of a formal extension point — but
see D2 below, because the same docs also promise an app-side override path that does not work.
### C4 — There are five documentation surfaces, not two
Phase A audited `docs/Styles.md` and `docs/mcp/Styling.md`. Also present:
```text
demo/www/contents/pages/sass/*.phtml 11 pages (border, color, flex, font,
margin, padding, shadow, typography, …)
demo/www/contents/pages/guides/frontEnd/themes/addingTheme.md
demo/www/contents/pages/guides/frontEnd/sass/z-index.phtml
demo/www/contents/pages/guides/frontEnd/dependencies/sass.md
```
`addingTheme.md` drifts too — its template lists a `'link'` token that does not exist (same phantom as
`docs/Styles.md`). Documentation work is therefore larger than Phase A scoped it.
---
## Part 1 — Master defect register
| ID | Problem | Evidence | Impact | Root cause | Risk of fix | Deps | Migration |
|---|---|---|---|---|---|---|---|
| **D1** | Dark theme resolves palette refs against the **default** palette; 16 of 27 tokens wrong | `_theme.scss:524-529` `include-colors()` drops `$theme` | Visible: dark mode ships with wrong colors (C1) | `$theme` not forwarded to `set-color()` | Low — light theme byte-identical **[verified]** | D0 | Visual change for dark users |
| **D2** | App config overrides are **silently discarded** | `config.merge`/`config.theme` do `deep-merge($-config, $new)` → framework defaults win | Apps cannot retheme; failure is silent, compiles clean | Merge direction; defaults registered as overrides not defaults | Low — output identical when no app config **[verified]** | D0 | None (nothing works today) |
| **D3** | Invalid size names emit invalid CSS silently | `_theme.scss:170-171`; `_basic.scss:139` → `font-size:"xxs"`; `mmo-notification.jhs:132` → `padding-bottom:"0"` | `
` has no font size; one element has no padding | Pass-through has no type discrimination (C2) | Low–Med — must not reject numbers/`auto` | D0 | None |
| **D4** | Dark tokens not contrast-checked; after D1, `muted` ≈ `text` | measured 13.09 vs 13.24 on `app-bg` **[verified]** | "Muted" text is indistinguishable from body text in dark | Token values never validated against a background | Low | D1 | Visual change |
| **D5** | `dark` declares neither `background` nor `icon` | compiled `.theme-dark` **[verified]** | Works only by `:root` cascade accident | `$all`/`$merge-map` has nothing to merge — no root `colors` map | Low | D1 | None |
| **D6** | `docs/mcp/Styling.md` token values wrong (font scale, base sizes) | Phase A §6 F6 | Agents generate off-scale CSS | Doc written from memory, never re-derived | None | D0 | None |
| **D7** | `docs/Styles.md` describes a framework that does not exist (palettes, `danger`, 4 phantom themes, wrong signature, wrong `tl`) | Phase A §6 F6 | Onboarding actively misleads | Doc predates implementation | None | — | None |
| **D8** | `addingTheme.md` promises default-theme inheritance the config layer does not implement; `sass.md` promises an app override path that is never compiled | C3, C4 | Developers follow instructions that silently do nothing | Docs describe intent, not implementation | None | D2 | None |
| **D9** | Sass color math escapes the theming layer | `classes/_inputs.scss:3-6,27,596` | Focus rings frozen to `default` theme | `rgba()` cannot take `var()` | Med — browser support | D0 | None |
| **D10** | `$-in-shadow-dom` / `in-shadow-dom()` never read | `_theme.scss:29-32`; 4 occurrences, all definitional | Dead abstraction; misleads readers | Unknown — needs author | Low | Q2 | None |
| **D11** | Dead code: `$CSS_COLORS`; 27 of 77 placeholders never extended; stale `mmo.client.demo.css` from a deleted source | Phase A §6 F7 | Maintenance noise; two parallel color systems | Drift | Low | — | None |
| **D12** | `assets['scss']` reads as compile targets, is a staleness list | `Theme.php:41`; `../doc/src/bootstrap.php:121` | Cost a sibling package real debugging time | Misleading key name | Low | — | Rename = breaking |
| **D13** | Two unrelated asset-URL mechanisms; `{ASSETS_URL}` silently inert in `styles.scss` | `DartSass.php:61` (only in `compileText`) vs `theme.printf` | Confusing; a `{ASSETS_URL}` in a `.scss` file fails silently | Two features added at different times | Low | — | None |
| **D14** | `const.xss` / `font-xss` unreachable — not in `size()`'s accepted set | `_default_config.scss:9,16` vs `_theme.scss:159` | Dead config, invites the `'xxs'` typo | Scale vocabulary drift | Low | D3 | None |
Not included: per-shadow-root CSS duplication. See Part 10 — it is inherent, measured, and small.
---
## Part 2 — Classification and challenge pass
Type: **A** bug fix · **B** hardening · **C** refactor · **D** API · **E** new capability · **F** architectural.
| ID | Type | Significant? | Root cause? | Simpler option? | Verdict |
|---|---|---|---|---|---|
| D1 | **A** | Yes — user-visible (C1) | Yes | Editing dark's values would mask it | **Fix now** |
| D2 | **A** | Conditional (see below) | Yes | — | **Fix now**, cheap either way |
| D3 | **A** | Yes — 2 live bugs | Yes | Patch 2 call sites only | **Fix guard + sites** |
| D4 | **B** | Yes | Yes | — | **Fix after D1** |
| D5 | **A** | Low but trivial | Yes | Rely on cascade | **Fix, make explicit** |
| D6 | **A** | Yes — agent-facing | Yes | — | **Fix now** |
| D7 | **A** | Yes | Yes | Delete the file | **Rewrite** |
| D8 | **A** | Yes | Yes | — | **Fix with D2** |
| D9 | **B** | Low — 2 sites | Yes | — | **Postpone (P2)** |
| D10 | **C** | Low | Unknown | — | **Blocked on Q2** |
| D11 | **C** | Low | Yes | — | **P3** |
| D12 | **D** | Low, already cost time | Yes | Document instead of rename | **Document (P3)** |
| D13 | **C** | Low | Yes | Document | **P3** |
| D14 | **C** | Low | Yes | Fold into D3 | **Fold into D3** |
### Challenge notes worth recording
**D2 — is it worth fixing given C3?** If the model is genuinely "fork the tree", app-level config
override is unnecessary. Two reasons to fix anyway: (a) the current behaviour is not "unsupported",
it is a **silent trap** — `@use 'inc/config' with (...)` compiles cleanly and does nothing
**[verified]**; (b) `sass.md` already tells developers to create app-side override files. The fix is
~8 lines and provably output-neutral, so cost is near zero. Fixing it converts a trap into a feature.
**D9 — postponed deliberately.** `color-mix()` needs Safari 16.2+/Chrome 111+, and relative color
syntax is newer still. Two call sites, both focus rings, both currently correct in light mode.
Revisit once a browser baseline is stated. Postponing costs nothing.
**D12 — rename rejected.** Renaming `assets['scss']` is a config-breaking change for aesthetics. A
doc-comment at [Theme.php:41](src/Theme.php:41) solves the actual problem (a reader misreading it) at
zero risk.
---
## Part 3 — Priorities
```text
P0 MUST FIX D0 (harness) D1 D2 D3
P1 FIX SOON D4 D5 D6 D7 D8
P2 PLANNED D9 + dark-mode hardening (prefers-color-scheme)
P3 OPTIONAL D10 D11 D12 D13
```
P0 rationale: D1 is a visible defect in a shipped feature; D3 produces invalid CSS today; D2 is a
silent trap that is nearly free to remove. D0 precedes all three because all three change compiled
output and nothing currently checks compiled output — which is exactly why D1 survived.
---
## Part 4 — Dependency graph and order
```text
D0 (regression harness)
│
├── D1 (forward $theme) ──┬── D4 (dark contrast pass)
│ └── D5 (declare background/icon)
│
├── D2 (merge direction) ──── D8 (fix the docs that describe it)
│
├── D3 (size guard) ───────── D14 (retire xss / align scale)
│
└── D9 (color-mix) [P2 — independent, gated on browser baseline]
D6, D7 (documentation) ── independent, parallelisable, but land AFTER D1-D3
so they document the corrected behaviour
D10, D11, D12, D13 ── independent cleanups, any time
```
**Parallelisable:** {D1, D2, D3} touch disjoint code (`include-colors`, `config` mixins, `size`).
**Must not precede:** D4 before D1 (you would be tuning values the compiler never reads);
D6/D7 before D1–D3 (you would document behaviour about to change).
**Blocked:** D10 on author Q2.
---
## Part 5 — Baseline measurements
Collected on this machine before any change. Timings are hardware-dependent — treat the *ratios* and
the byte counts as the contract, not the milliseconds.
| Metric | Method | Baseline | Target | Regression threshold |
|---|---|---|---|---|
| Global sheet compile | `sass --load-path=src/sass src/sass/styles.scss`, 3 runs | **~82 ms** | unchanged | >120 ms |
| Per-element compile | same, on a 2-line `@use "shadow_theme"` file, 10 runs | **11.2 ms** | unchanged | >20 ms |
| Marginal theme-parse cost | per-element minus a no-theme file (8.2 ms) | **~3 ms** | unchanged | >6 ms |
| Full jhs pass (40 elements) | 40 × per-element | **~0.45 s** | unchanged | >1 s |
| `mmo.client.css` size | `wc -c`, expanded | **110 195 B** | ±2 % | >115 000 B |
| `mmo.client.jhs.js` size | `wc -c` built artifact | **849 317 B** | ±2 % | >880 000 B |
| Distinct custom properties | `grep -o '--mmo-[a-z-]*:' \| sort -u \| wc -l` | **35** | 37 after D5 | any unexplained change |
| `var()` references | `grep -o 'var(--mmo-' \| wc -l` | **978** | ≥978 | a *drop* means a token was inlined |
| Invalid declarations | grep compiled CSS for `: "` | **1** (`font-size:"xxs"`) | **0** | >0 |
| `:root` token block | byte-compare | fixed snapshot | identical through D1–D3 | any diff |
| Test coverage of sass | — | **none** | D0 harness green | — |
**Cannot be measured objectively here:** rendering performance and memory. Neither is plausibly
affected by any P0/P1 fix (no selector or property counts change materially), so I am not proposing
benchmarks for them. If D9 later replaces static colors with `color-mix()`, paint cost should be
measured then.
---
## Part 6 — FIX-000 — Sass regression harness *(prerequisite)*
### Problem
Nothing validates compiled CSS. D1 survived indefinitely because no test reads the token blocks, and
D3 emits invalid CSS that no test would catch.
### Proposed change
A minimal PHP CLI harness — not a framework. The repo already has `DartSass::Shared()->compileText()`
and a PHP bootstrap, so this adds no dependency. Existing `tests/` is Closure-based browser JS and is
the wrong tool for compiler output.
```text
tests/sass/run.php compile fixtures, assert, print pass/fail
tests/sass/fixtures/*.scss small inputs
tests/sass/expected/*.css golden snapshots for token blocks only
```
Three assertion kinds, which is enough to cover every P0 fix:
1. **Token snapshot** — compile `styles.scss`, extract `:root{…}` and `.theme-*{…}`, compare to golden.
2. **Contains / not-contains** — e.g. no `: "` anywhere in output (catches D3 class permanently).
3. **Compile-must-fail** — assert `theme.padding('xxs')` raises, once D3 lands.
### Complexity
**S** — Implementation S, Tests (self), Docs XS.
### Acceptance
- [ ] `php tests/sass/run.php` exits non-zero on any failure
- [ ] Golden snapshot of current `:root` committed **before** D1
- [ ] Assertion 2 passes only after D3
- [ ] Runs in CI or is documented in `docs/Workflow.md`
---
## Part 7 — P0 / P1 fix specifications
### FIX-001 — Forward `$theme` in `include-colors()` · Type A · P0 · **XS**
**Problem.** `.theme-dark` resolves every palette reference against the `default` palette. 16 of 27
tokens are wrong, including `--mmo-color-info: #00498A` (near-black navy used as an informational
accent *on a dark background*).
**Evidence.** [`_theme.scss:524-529`](src/sass/_theme.scss:524):
```scss
@mixin include-colors($name, $all: false) {
@each $key,$value in config.get('colors', null, $name, $all) {
@include set-color($key, $value); // $name is not passed
}
}
```
`set-color` defaults `$theme: null`, which [`_theme.scss:45`](src/sass/_theme.scss:45) resolves as
*the currently active theme*. `theme.include()` never calls `config.set('theme', …)` — only
`theme.set()` does — so during `.theme-dark` emission the active theme is still `default`.
**Root cause.** A dropped parameter. Consequence: `themes/_dark.scss`'s entire `palettes` block
(13 lines) is **dead** — it is never read. *This also settles Phase A Q1:* the reversal cannot have
been written to compensate for this bug, because reversing data nobody reads has no effect. The
reversal is deliberate intent.
**Proposed change.** Pass `$name` as the third argument. One token.
**Files.** `src/sass/_theme.scss` · `tests/sass/expected/theme-dark.css` · `docs/mcp/Styling.md`
**API impact.** None — `include-colors` is internal, called only by `theme.include()`.
**Backwards compatibility.** *Mostly compatible.* **[verified]** the `:root` block is
**byte-identical** before and after; only `.theme-dark` changes. Any app that applies `.theme-dark`
(i.e. any app using `mmo-app`'s toggle) sees different colors — better ones, but different.
**Verified effect** (16 changed tokens; the consequential ones):
| token | before | after | WCAG on `app-bg` |
|---|---|---|---|
| `info` | `#00498A` | `#8CC9FF` | 2.01 → **8.10** |
| `title` | `#778B99` | `#E8EFF3` | 5.15 → **12.29** |
| `secondary` | `#FFFFFF` | `#AAB3BA` | 18.20 → 6.71 |
| `light` | `#000000` | `#2C343F` | — |
| `dark-grey` | `#F0F6F9` | `#5A6D7A` | — |
**Tests (must fail before, pass after).**
1. Golden `.theme-dark` snapshot asserting `--mmo-color-info: #8CC9FF`.
2. Regression: `:root` snapshot must remain byte-identical.
3. A partial theme (only `primary`) emits exactly one color var.
**Validation.** Load the demo, toggle "Thème sombre", confirm informational surfaces are legible.
**Risks.** Any hand-tuned CSS written to compensate for the wrong dark colors will now be wrong in
the other direction. Grep for `.theme-dark` overrides in app code before releasing.
**Acceptance.**
- [ ] `:root` byte-identical · [ ] `.theme-dark` matches new golden · [ ] demo toggle reviewed visually
- [ ] FIX-004 scheduled (fix alone leaves `muted` broken) · [ ] `docs/mcp/Styling.md` note added
---
### FIX-002 — Register framework defaults as defaults · Type A · P0 · **S**
**Problem.** An application cannot override any token. `@use 'inc/config' with ($-config: (…))`
compiles cleanly and is silently discarded. **[verified]** an app setting `padding: 99px` gets `1rem`.
**Evidence.** [`inc/_config.scss`](src/sass/inc/_config.scss:97) — both registration mixins merge
*new over old*:
```scss
@mixin merge($config) { $-config: map.deep-merge($-config, $config) !global; }
@mixin theme($name, $config) { $-config: map.deep-merge($-config, ('themes': ($name: $config))) !global; }
```
`_default_config.scss` and `themes/*.scss` call these, so framework defaults overwrite whatever the
app configured. Because Sass requires `@use` before any rule, the app has no other injection point.
**Root cause.** Defaults are registered through an *override* API. Two sites, not one — fixing only
`merge` leaves colors broken **[verified]**.
**Proposed change.** Add two mixins expressing "fill gaps only", and switch the three framework
registration sites to them. `merge`/`theme` keep their current semantics untouched.
```scss
@mixin defaults($config) { $-config: map.deep-merge($config, $-config) !global; }
@mixin theme-defaults($name, $config) { $-config: map.deep-merge(('themes': ($name: $config)), $-config) !global; }
```
**Files.** `inc/_config.scss` · `_default_config.scss` · `themes/_default.scss` · `themes/_dark.scss`
· `tests/sass/` · `addingTheme.md` · `sass.md`
**API impact.** Additive. `config.merge()` and `config.theme()` are unchanged, so the documented
theme-authoring workflow in `addingTheme.md` still works verbatim.
**Backwards compatibility.** *Fully compatible.* When `$-config` is empty — always true inside the
framework, since these are the first things evaluated — `deep-merge(A, {})` ≡ `deep-merge({}, A)`.
**[verified]** compiled output identical except the two tokens a test app deliberately overrode. No
`.jhs` file calls `config.merge`/`config.theme` (only `mmo-workflow.jhs` uses an unrelated JS
`config.set`).
**Tests.** (1) no-app-config → output byte-identical to baseline; (2) app overriding `sizes/padding`
→ `--mmo-size-padding: 99px`; (3) app overriding `themes/default/colors/primary` → resolves to the
app's palette color; (4) `config.merge()` still overrides, protecting theme authors.
**Risks.** Low. The only way to regress is if some file depends on defaults clobbering earlier
config — impossible, as nothing runs before them.
**Acceptance.**
- [ ] Baseline output unchanged · [ ] Both override tests pass · [ ] `merge`/`theme` semantics unchanged
- [ ] `sass.md`'s app-override claim now true, or corrected (FIX-008)
---
### FIX-003 — Type-aware guard in `size()` · Type A · P0 · **S**
**Problem.** An unrecognised scale word is emitted as a quoted string, producing invalid CSS that the
browser discards silently. Two live instances: `
` has had no `font-size`
([`_basic.scss:139`](src/sass/styles/_basic.scss:139) → `font-size: "xxs"` **[verified]**), and
[`mmo-notification.jhs:132`](src/customElements/elements/mmo-notification.jhs:132) →
`padding-bottom: "0"`.
**Evidence.** [`_theme.scss:159-172`](src/sass/_theme.scss:159) — the `@else` falls through to
`@return $size` with no validation.
**Root cause.** Pass-through is *required* (C2: 67 uses of `0`, 10 of `50%`, 7 of `auto`) but is
implemented as an untyped catch-all, so a typo is indistinguishable from a legitimate CSS value.
**Proposed change.** Discriminate by type before falling through:
```text
number (0, 12px, 50%, -8px, $var) → return as-is ← keeps all 85 legitimate uses
string in scale set → calc(factor * var()) ← existing behaviour
string in ('auto','inherit','initial','unset','none')
→ return as-is ← keeps the 7 `auto` uses
anything else → @error "unknown size [x] — expected one of …"
```
Then fix the two call sites (`'xxs'` → `'xs'`, `'0'` → `0`) and, as D14, either add `xss` to the
accepted set or delete it from `_default_config.scss`. **Recommend deleting** — a `0.1` multiplier
between `xss` and `xs` has no call sites and its name is one keystroke from the typo that caused this.
**Files.** `_theme.scss` · `styles/_basic.scss` · `mmo-notification.jhs` · `_default_config.scss` ·
`tests/sass/` · `docs/Styles.md`
**API impact.** Public behaviour change: previously-silent misuse now fails the build. This is the
point of the fix.
**Backwards compatibility.** *Mostly compatible.* Every value used anywhere in the repo continues to
work — census in C2 is exhaustive over `src/sass` and `src/customElements`. **An application passing
a bad size will now fail to compile instead of silently producing no style.** That is the desired
trade, but it is the one fix here that can break someone's build, so it should ship with a release
note. Application `.jhs` files outside this repo were not surveyed — see Q1.
**Tests.** (1) each of `0`, `50%`, `auto`, `2px`, `-8px` passes through unchanged; (2) each scale word
produces `calc(f * var(--mmo-size-*))`; (3) `theme.font-size('xxs')` **fails compilation**;
(4) compiled CSS contains no `: "` anywhere; (5) `h6` has a real `font-size`.
**Risks.** An allow-list that is too narrow breaks a legitimate CSS keyword. Mitigation: the census is
exhaustive for this repo, and the error message names the accepted set.
**Acceptance.**
- [ ] All 85 legitimate values unchanged · [ ] Both bad sites fixed · [ ] Invalid-CSS assertion green
- [ ] `xss` removed or reachable · [ ] Release note: sizes now validated
---
### FIX-004 — Dark theme contrast pass · Type B · P1 · **M**
**Problem.** Dark token values have never been validated against a background. After FIX-001, `muted`
lands at **13.09** contrast on `app-bg` against `text` at **13.24** **[verified]** — a 1 % difference.
"Muted" text becomes indistinguishable from body text.
**Root cause.** The ramp-reversal model assumes a weight means the same *role* in both themes, but the
author picked `greys-400` for `muted` in light and `greys-800` in dark. Under reversal those land at
opposite ends. This is a data problem, not a code problem — which is why it must follow FIX-001.
**Proposed change.** Audit all 27 dark tokens against `app-bg` and `surface` with the WCAG formula
already implemented in [`inc/_functions.scss:237`](src/sass/inc/_functions.scss:237) (`luminance()`).
Targets: body text ≥ 4.5, large/title ≥ 3.0, `muted` deliberately **between 4.5 and text's ratio**,
non-text borders ≥ 3.0. Expected change: `muted` → a mid grey (`greys-200`/`greys-400` under dark's
reversed ramp).
Also worth recording: `border` fails 3.0 in **both** themes (light 1.95, dark 1.10). Decorative
borders are exempt from WCAG; borders that convey state (`%invalid`, `%required`) are not. Scope this
fix to the latter only — do not chase a number for hairlines.
**Files.** `themes/_dark.scss` · optionally `themes/_default.scss` · `tests/sass/`
**Backwards compatibility.** *Mostly compatible* — dark-mode appearance changes again. Bundle with
FIX-001 in a single release so users see one visual change, not two.
**Tests.** A contrast assertion in the harness: parse the compiled token block, compute ratios for a
declared pair list, fail below threshold. This is the test that makes the whole theming layer
self-checking, and is reusable for every future theme.
**Risks.** Over-tuning to hit numbers can flatten the palette. Require visual review alongside the
numeric gate.
---
### FIX-005 — `dark` declares `background` and `icon` · Type A · P1 · **XS**
`.theme-dark` omits both **[verified]**; they work only because `:root` values cascade. Declare them
explicitly in `themes/_dark.scss`. Also correct the `$all` parameter's doc comment at
[`_theme.scss:520`](src/sass/_theme.scss:520): it maps onto `get()`'s `$merge-map` and is currently a
no-op for colors, since no `colors` map exists at config root. Either document that or drop the
parameter. **XS**, no API change.
---
### FIX-006 — Correct `docs/mcp/Styling.md` · Type A · P1 · **S**
The agent-facing doc has wrong numbers, so agents generate off-scale CSS.
| Claim | Actual |
|---|---|
| `font-xs: 0.5, font-s: 0.75, font-m: 1, font-l: 1.25, font-xl: 1.5` | `font-xs: 0.8, font-s: 1, font-m: 1.25, font-l: 1.5, font-xl: 2` |
| base `font: 1rem`, `icon: 24px` | `0.8rem`, `22px` |
| const omits `xxl` | `xxl: 5` exists |
| "both themes define this key set" | `dark` omits `background`, `icon` (until FIX-005) |
Add: the `classes/` placeholder layer is the shadow-DOM-facing API (Phase A §4) — currently
undocumented anywhere, despite 16 `.jhs` files using it. **Derive every number from
`_default_config.scss` at edit time; do not transcribe.**
---
### FIX-007 — Rewrite `docs/Styles.md` · Type A · P1 · **M**
Contains phantom palettes (`oceans`, `greylights`, `whites`), a phantom `danger`/`black`/`link`, four
phantom themes, a wrong `color-bg()` signature, and a worked example whose prose contradicts its code
(`'tl'` described as "top and bottom"). Rewrite from source; add the real palettes (`lights`, `limes`)
and the three-tier color model. Consider making this the canonical human doc and reducing
`demo/www/contents/pages/sass/*.phtml` to runnable examples that link to it.
---
### FIX-008 — Make the app-extension contract true · Type D · P1 · **S**
`sass.md` tells developers to create `my_app/src/sass/*/_your_file.scss`; nothing compiles it.
`addingTheme.md` claims alternative themes inherit the default theme's values; the config layer has no
such fallback (it works at runtime only via the `:root` CSS cascade — an accident worth stating).
After FIX-002, document the one path that actually works and that I verified end to end:
```text
1. app entry point: app/src/sass/app.scss
2. @use 'inc/config' with ($-config: ( … app overrides … ));
3. @use 'styles'; // the whole framework sheet
4. app's own rules below
5. point Theme::$stylesheet at app.scss
```
Also correct `addingTheme.md`'s token list (drop the phantom `link`) and note at
[`Theme.php:41`](src/Theme.php:41) that `assets['scss']` is a staleness list, not compile targets
(D12 — comment, not rename).
---
## Part 8 — Quick wins
High value, low complexity, low risk, independently shippable:
| Fix | Why it qualifies |
|---|---|
| **FIX-001** | One parameter. Light theme byte-identical. Fixes a visible bug. |
| **FIX-005** | Two lines of data. |
| **FIX-006** | Docs only; directly improves every agent-authored `.jhs`. |
| **D11 partial** | Delete `$CSS_COLORS` and the stale `mmo.client.demo.css`. Zero risk. |
| **D12 comment** | One doc-comment; already cost a sibling package real time. |
FIX-001 is the standout: smallest possible diff, largest user-visible improvement, and *provably*
cannot affect the light theme.
---
## Part 9 — High-risk changes
**FIX-003 (size guard)** — the only P0 that can break a build.
*What breaks:* an application `.jhs` passing a size value not in my census. I surveyed `src/sass` and
`src/customElements`; **application code outside this repo was not surveyed.**
*Before:* run the harness across every `.jhs` in every consuming app.
*Mitigation:* ship behind a warning first — emit `@warn` for one minor version, promote to `@error`
in the next. This costs one release cycle and removes essentially all risk.
*Rollback:* revert the guard; the two call-site corrections are independently safe and stay.
**FIX-001 + FIX-004 (dark appearance)** — not risky technically, but *visible*.
*What breaks:* any app CSS hand-tuned against the broken dark palette.
*Before:* grep consuming apps for `.theme-dark`.
*Mitigation:* ship FIX-001 and FIX-004 in the **same** release so users see one change. A feature flag
is not appropriate — two dark themes is worse than one corrected one.
*Rollback:* single-commit revert; no data migration.
**Not risky, contrary to expectation:** FIX-002. It reads like a semantic change to the config core,
but is provably output-neutral when no app supplies config.
---
## Part 10 — What should NOT be changed
**Per-shadow-root CSS duplication.** Phase A measured ~21 KB of repeated `%button` rules across
8 shadow roots in an 849 KB bundle (~2.5 %). Deduplicating means constructable stylesheets and
`adoptedStyleSheets` — a category F change to the compiler and every element's init path, to recover
2.5 %. **Rejected.** The duplication is inherent to shadow DOM, and the current design trades bytes
for zero runtime machinery. Revisit only if the element count roughly triples.
**The `calc(factor * var(--size))` idiom.** Looks redundant next to plain custom properties; it is the
mechanism that makes the whole scale runtime-rescalable. Do not "simplify".
**`config.merge()` / `config.theme()` semantics.** FIX-002 adds siblings rather than changing these,
specifically so `addingTheme.md`'s documented workflow keeps working.
**The `'a/b/c'` path-string config API.** The `meta.call(meta.get-function(...))` implementation is
opaque, but rewriting it is pure churn: identical behaviour, identical cost, a diff across the most
load-bearing file in the folder. If it is ever touched, it should be *while* fixing something else.
**The `classes/` ÷ `styles/` split.** The eight colliding basenames look like duplication and are not.
Document it (FIX-006); do not merge it.
**`@error` in `color-value()`, `sizeToIndex()`, `device-bound()`.** Already correct. D3 brings `size()`
*up* to this standard rather than relaxing the others.
**The dark palette reversal.** Now that FIX-001 makes it actually take effect, it is a coherent model.
FIX-004 tunes values within it; it should not be replaced with per-theme absolute palettes before
seeing how it behaves once it is live.
---
## Part 11 — Backlog
| ID | Title | Pri | Type | Deps | Cx | Main files | Tests |
|---|---|---|---|---|---|---|---|
| FIX-000 | Sass regression harness | P0 | — | — | S | `tests/sass/run.php` | self |
| FIX-001 | Forward `$theme` in `include-colors()` | P0 | A | 000 | XS | `_theme.scss:524` | dark golden; `:root` invariant |
| FIX-002 | `config.defaults()` / `theme-defaults()` | P0 | A | 000 | S | `inc/_config.scss`, `_default_config.scss`, `themes/*` | output-neutral; 2 override tests |
| FIX-003 | Type-aware `size()` guard (+2 call sites, retire `xss`) | P0 | A | 000 | S | `_theme.scss:159`, `_basic.scss:139`, `mmo-notification.jhs:132` | 85 pass-through; 1 must-fail; no `: "` |
| FIX-004 | Dark contrast pass | P1 | B | 001 | M | `themes/_dark.scss` | contrast assertions |
| FIX-005 | Declare dark `background`/`icon`; clarify `$all` | P1 | A | 001 | XS | `themes/_dark.scss`, `_theme.scss:520` | token-count assertion |
| FIX-006 | Correct `docs/mcp/Styling.md` | P1 | A | 001-003 | S | `docs/mcp/Styling.md` | values re-derived from source |
| FIX-007 | Rewrite `docs/Styles.md` | P1 | A | 001-003 | M | `docs/Styles.md` | — |
| FIX-008 | True app-extension contract | P1 | D | 002 | S | `sass.md`, `addingTheme.md`, `Theme.php:41` | documented path compiles |
| FIX-009 | `prefers-color-scheme` default | P2 | E | 001,004 | M | `styles/_basic.scss`, `mmo-app.jhs` | first-load honours OS |
| FIX-010 | `color-mix()` for focus rings | P2 | B | browser baseline | M | `classes/_inputs.scss` | theme-switch visual |
| FIX-011 | Dead code sweep | P3 | C | — | S | `classes/_colors.scss`, `demo/www/bin/*` | output-neutral |
| FIX-012 | Resolve `$-in-shadow-dom` | P3 | C | **Q2** | XS | `_theme.scss:29`, `_shadow_theme.scss` | output-neutral |
| FIX-013 | Unify asset-URL mechanisms | P3 | C | — | S | `DartSass.php:61`, `_theme.scss:20` | — |
---
## Part 12 — Phases
**Phase 0 — Safety net.** FIX-000. *Outcome:* compiled output becomes assertable; current `:root`
frozen as a golden file. *Validation:* harness green on unmodified tree. *Risk:* none.
**Phase 1 — Correctness.** FIX-001, FIX-002, FIX-003 (as `@warn`), FIX-005.
*Outcome:* dark mode renders its own palette; app config works; no invalid CSS.
*Validation:* `:root` byte-identical; dark golden matches; zero `: "`.
*Risk:* dark appearance changes visibly — hold until FIX-004 lands with it.
**Phase 2 — Theme quality.** FIX-004, promote FIX-003 to `@error`.
*Outcome:* dark mode is contrast-validated and self-checking. *Validation:* contrast gate + visual review.
**Phase 3 — Documentation truth.** FIX-006, FIX-007, FIX-008.
*Outcome:* all five doc surfaces describe the implementation. *Dependency:* must follow Phases 1–2.
**Phase 4 — Capability.** FIX-009, then FIX-010 once a browser baseline exists.
**Phase 5 — Cleanup.** FIX-011, FIX-012, FIX-013.
---
## Part 13 — Release and migration
Current version is `3.0.13` (`mmo.json`), so `x.y.z` applies.
```text
3.0.14 Phase 0 + docs-only no compiled-output change
3.1.0 Phase 1 + 2 together dark mode visibly changes; config.defaults() added;
size() warns. MINOR: new behaviour, no removed API
3.2.0 Phase 3 + FIX-009 size() errors (announced in 3.1.0); OS colour scheme
3.3.0 Phase 5 cleanup
4.0.0 only if FIX-012/013 force a breaking change
```
**Why 3.1.0 and not 3.0.14 for Phase 1:** dark-mode colors change visibly. Even though every change is
a bug fix, "the app looks different after a patch release" is the wrong signal.
**Deprecation.** Only `size()`: `@warn` in 3.1.0, `@error` in 3.2.0 — one minor version, which is
generous for a compile-time diagnostic.
**Migration tooling.** Not warranted. The only possible app-side breakage is an invalid size literal,
which the compiler will name precisely, at the exact line. A codemod would be more work than the fix.
**Rollback.** Every fix is a single-commit revert with no persisted state. `localStorage['themeMode']`
survives a rollback and still means the same thing.
---
## Part 14 — Documentation plan
| Surface | Change | Fix |
|---|---|---|
| `docs/mcp/Styling.md` | correct token values; document `classes/` layer | FIX-006 |
| `docs/Styles.md` | full rewrite from source | FIX-007 |
| `docs/SASS_ARCHITECTURE_REVIEW.md` | append Part 0 corrections | this plan |
| `guides/frontEnd/dependencies/sass.md` | make the app-override path real | FIX-008 |
| `guides/frontEnd/themes/addingTheme.md` | drop phantom `link`; state that theme inheritance is CSS-cascade, not config | FIX-005/008 |
| `demo/www/contents/pages/sass/*.phtml` | re-derive every number after FIX-003 | FIX-007 |
| `Theme.php:41` | doc-comment: `assets['scss']` is a staleness list | FIX-008 |
| `docs/Workflow.md` | how to run the sass harness | FIX-000 |
| release notes | dark appearance change; `size()` deprecation | 3.1.0 |
**Needs a diagram, and has none today:** the two-compile-path / custom-property-inheritance model
(Phase A §2.3). It is the single thing a newcomer must understand, and it is currently nowhere.
---
## Part 15 — Recommended order and First Sprint
```text
FIX-000 ──┬── FIX-001 ──┬── FIX-004 ──┐
│ └── FIX-005 ──┤
├── FIX-002 ────────────────┼── FIX-006 ── FIX-007 ── FIX-008
└── FIX-003 ────────────────┘
└── FIX-009 ── FIX-010
```
1. **FIX-000** — everything after this changes compiled output; nothing currently checks it. This is
the fix that would have caught D1.
2. **FIX-001** — largest user-visible gain per line changed, and provably cannot touch the light theme.
3. **FIX-005** — trivial, same file family, ships with 001.
4. **FIX-003 (warn mode)** — starts the deprecation clock early so the error can land a version sooner.
5. **FIX-002** — independent; removes a silent trap for near-zero cost.
6. **FIX-004** — must follow 001, and must ship *with* it so dark mode changes exactly once.
### First Sprint
**FIX-000 → FIX-001 → FIX-005 → FIX-004**, released together as **3.1.0**, with **FIX-003 in warn
mode** and **FIX-002** if capacity allows.
This satisfies the criteria: it establishes the safety net first; fixes the most consequential issue
(a shipped feature rendering wrong colors); introduces zero architectural churn — the code delta is
one parameter, two data lines, and a set of tuned hex values; produces a measurable improvement
(`info` contrast 2.01 → 8.10, `title` 5.15 → 12.29); and leaves the tree stable, because the light
theme is byte-identical throughout **[verified]**.
Explicitly **not** in the first sprint: documentation (it should describe the corrected behaviour,
not the in-flight one) and anything in Phase 4–5.
---
## Part 16 — Questions still open
Phase A's Q1 and Q4 are now answered from source (Part 0). Three remain, and only Q1 blocks work.
1. **(Blocks FIX-003.)** Which repositories consume `mmo.client`? I need to run the size census across
their `.jhs` files before promoting `size()` to `@error`. If that survey is impractical, ship
warn-only indefinitely.
2. **(Blocks FIX-012.)** Was `$-in-shadow-dom` meant to branch something? If it is vestigial, deleting
it and folding `* {box-sizing}` into `_theme.scss` removes a file.
3. **(Shapes FIX-008's priority, not its content.)** Given that `sass.md` says the framework tree is
modified in place by the team, is app-level config override something you actually want — or
should `sass.md` simply stop promising it? FIX-002 is worth doing either way; only the
documentation differs.