---
status: accepted
---

# RTL ships as a conditional overlay, not doubled into the Monolith

Brightspot resolves text direction as a per-user setting server-side (`Localization.isCurrentUserRtl()`), emitted once into the `<html dir>` attribute. The Tool UI **Monolith** (`v5.css`), however, ships **both** directions to **every** user: `postcss-rtl` doubles each directional rule into `[dir="ltr"]` and `[dir="rtl"]` halves (plus bare-`[dir]` scoping), so ~25% of the post-Lever-#1 bundle is direction-prefixed and roughly half of that is dead weight on any given page. We therefore **split direction out of the Monolith**: all users load a direction-neutral base `v5.css`; RTL users *additionally* load a small `[dir="rtl"]`-only overlay `v5-rtl.css`, gated by the same `Localization.isCurrentUserRtl()` call that drives `<html dir>`. This **refines [ADR 0001](./0001-monolith-css-delivery.md)**: the Monolith stays the single styling vehicle, but materializes as a base + conditional overlay pair rather than one doubled bundle.

## How the split is produced

The base and overlay are a **clean partition of the same source CSS**, produced by one paren-aware direction-filter run with opposite predicates:

| Bundle | Pipeline | Keeps |
| --- | --- | --- |
| `v5.css` (base) | postcss **without** postcss-rtlcss, then **strip-`[dir=rtl]`** | every rule except `[dir="rtl"]`-scoped parts; physical properties stay at their LTR-default values, unscoped |
| `v5-rtl.css` (overlay) | postcss **with** `postcss-rtlcss` `mode: 'override'`, then **keep-only-`[dir=rtl]`** (which also prunes the at-rule wrappers it empties) | only `[dir="rtl"]`-scoped rules — the mirrored overrides, the authored `[dir="rtl"]` rules, and Tailwind `rtl:`-variant output |

`base ⊎ overlay` reconstructs correct rendering for both users with **no gap and no overlap**: an LTR user gets the base alone; an RTL user gets the base (LTR-default physical props) plus the overlay's `[dir="rtl"]` overrides, which win on specificity regardless of load order.

The two filters are **one module, two predicates**, sharing a top-level comma splitter that respects `(`/`[` nesting — so `.a, [dir=rtl] .b { … }` is partitioned, not mangled.

Each stage runs as an **isolated postcss pass** (postcss-rtlcss → filter → cssnano), not one chained pipeline: postcss-rtlcss must fully finish adding `[dir=rtl]` overrides before keep-rtl runs, and keep-rtl must finish before cssnano — otherwise cssnano's empty-discard races keep-rtl's rule removal and the emptied `@media`/`@container` wrappers survive (measured: ~46 KB of husks, ~32% of the overlay). The filter therefore prunes empty at-rules in its own `OnceExit` rather than relying on cssnano.

## Why postcss-rtlcss `override` + keep-rtl (verified empirically)

`postcss-rtlcss` v6 `mode: 'override'` was confirmed (2026-06-02, sandbox `/private/tmp/rtlcss-verify`) to emit **no** `[dir="ltr"]` halves and **no** bare-`[dir]` scaffolding — but it **keeps every unscoped original rule** alongside the `[dir="rtl"]` overrides. Override-alone is therefore not a lean overlay (it would carry a full copy of the base). The keep-rtl filter drops those originals, leaving the `[dir="rtl"]`-only overlay.

Override mode also emits the explicit cascade-resets the overlay needs (e.g. `[dir="rtl"] .x { border-left: none; border-right: … }`). This is what makes it beat the considered alternatives:

- **`mode: 'combined'`** — reproduces postcss-rtl's doubling (`[dir=ltr]` + `[dir=rtl]` halves). Rejected.
- **`mode: 'diff'`** — emits flipped declarations **unscoped** (relying on load order, not specificity) and **silently drops authored `[dir="rtl"]` rules**. Rejected.
- **"postcss-rtl extraction"** (filter postcss-rtl's doubled output down to its `[dir=rtl]` half) — the originally-feared fallback. Unnecessary, and *less correct*: postcss-rtl puts the cascade-resets in the `[dir=ltr]` half, so the extracted `[dir=rtl]` half would be missing them.
- **Parallel model** (RTL users load `v5-rtl.css` *instead of* `v5.css`) — two large bundles, lower edge-cache hit rate, no free graceful degradation. Rejected in favor of the overlay.

## Consequences

- A **CI overlay-integrity assertion** guards the partition every build: the base must contain zero `[dir="rtl"]` rules; the overlay must be non-empty and contain only `[dir="rtl"]` rules (zero unscoped / `[dir="ltr"]` / bare-`[dir]`). This catches a strip-rtl leak into the base, a silently no-op'd postcss-rtlcss (empty overlay), and a keep-rtl leak of originals into the overlay — each diagnostically. Stronger than a bare "byte counts differ" check.
- **Graceful degradation is free — and better than worst-case.** The base is built on CSS **logical properties**, which respond to the server-set `<html dir="rtl">` natively (no `[dir=rtl]` rules needed). So if a deployment is missing `v5-rtl.css`, an RTL user still gets **mostly-correct RTL** — only the physical-property elements (the overlay's job) stay unflipped — never a blank page or a fully-LTR layout. No runtime fallback logic. (Validated live 2026-06-02: at `dir=rtl` the base alone mirrored the bulk of the Dashboard and Content Edit pages; the overlay then corrected 16 and 46 physical-property elements respectively, including reversing the RTE toolbar.)
- **Measured impact (2026-06-02, same shared compile, decimal MB):** LTR base 2.63 MB — **−12.8%** vs the 3.02 MB doubled build (which cross-checks against the shipped container artifact ≈ 3.01 MB); RTL base+overlay 2.73 MB — **−9.7%**; overlay **95 KB** (beats the ~130–140 KB projection after empty-wrapper pruning). The overlay's size is effectively a **proxy for physical-property debt** in the legacy CSS: it shrinks toward zero as source migrates from physical (`padding-left`) to logical (`padding-inline-start`) properties, at which point native `dir` handling would leave the overlay nearly empty.
- The direction-filter is the **deep module** of Lever #2, golden-tested in both directions independent of the build orchestration.
- **`v4.css` is out of scope** and keeps `postcss-rtl` until v4 is separately retired (it also has hand-authored `[dir='rtl']` Less rules that need their own audit).
- The change surface is entirely Host-CMS-consumer-side (`build-css.mjs` + a conditional second `<link>` in `DariHtmlToolPageUtils`); the **Preset** is unchanged, so other `@brightspot/ui` consumers are insulated.

See also: [ADR 0001](./0001-monolith-css-delivery.md) (Monolith as single delivery vehicle — refined here), and the full Lever #2 design this records the decision for: [`../../plans/lever-2-rtl-overlay-prd.md`](../../plans/lever-2-rtl-overlay-prd.md).
