3 Commits

Author SHA1 Message Date
fitse-yotor
e74ef1c672 fix(i18n): render Amharic in an Ethiopic font
Both themes set `fontFamily: Inter`, and Inter carries no Ge'ez glyphs. So
every Amharic string in a bilingual system — 45-73 KB of locale data per app —
has been rendering in whatever font the OS happened to substitute: different on
Windows, macOS and Android, and matching neither the design nor each other.

Noto Sans Ethiopic goes directly after Inter in one stack rather than being
swapped in under `[lang='am']`. Browsers fall back per glyph, not per element,
so a single stack renders Latin in Inter and Ge'ez in Noto automatically —
including within one string. That is the case that matters here: a registry is
full of mixed-script lines like an Amharic name beside a Latin IMO number, and
a language-scoped swap renders half of those in the wrong face.

Heading line-heights go up a step at the same time. Ge'ez has taller ascenders
and deeper descenders than Latin, so headings set to Inter's natural leading
clip once Ethiopic is actually being rendered. This was deliberately held back
from the theme refactor so that change could prove it altered nothing.

The gallery gains Ge'ez headings and a mixed-script line, so the baselines
cover the case rather than only Latin.

Follow-up: both fonts still load from Google Fonts via a render-blocking CSS
@import. Self-hosting with a subset and preload links is worth doing —
government networks frequently make fonts.googleapis.com slow or unreachable,
and silent Amharic fallback is precisely the failure this commit fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:20:49 +03:00
fitse-yotor
b63e07305a feat(theme): add semantic tokens, focus ring, and reduced-motion support
Adds the layer feature code has been missing: tokens that name a role — page
surface, subtle border, danger — instead of a colour. Every one resolves to a
Mantine variable rather than a literal, so they follow the colour scheme for
free and cannot drift from the theme. A parallel palette of raw hexes would
have recreated exactly the problem this exists to fix.

Also closes three accessibility gaps that had no implementation anywhere in the
codebase: no :focus-visible rule, no screen-reader-only utility, and no global
prefers-reduced-motion handling.

The focus work found a real bug, and nearly introduced a worse one. Mantine
already rings its own controls, so the first attempt deferred to it with
`outline: none` on .mantine-focus-auto. That suppressed Mantine's ring without
replacing it, leaving portal buttons with no focus indicator at all — and it
only showed up in one app, because which rule won depended on stylesheet order.
The rules use identical values, so overlapping them is invisible and safe;
opting out is not. The focus test now asserts computed outline width and style,
not just pixels, since a screenshot alone would not have caught this.

`status-tone.ts` establishes the six-tone vocabulary that the 48 scattered
status→colour maps will eventually collapse onto. Nothing consumes it yet.

No visual change: the 8 existing baselines pass unmodified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:11:20 +03:00
fitse-yotor
659e954306 refactor(theme): share one base theme between backoffice and portal
The two apps had drifted into unrelated themes. The portal's carried a full
type scale, radius scale, shadow ramp and component defaults; the backoffice's
had none of them — 55 lines defining two colour ramps and little else. With
nothing to inherit, its 23 features each invented their own sizing, which is
the real source of the inconsistency the UI reads with.

Promote the portal's structure to `libs/shared` as `baseTheme`, and reduce both
themes to what they should differ on: brand. The backoffice keeps #1e40af and
the portal keeps Coastal Modern — a distinct accent tells an officer which of
the two systems they are in, and the ramps are not interchangeable in contrast.

Both export names are preserved, so no consumer import changes.

Two properties are deliberately held back rather than shared:
- `colors.gray`: the portal's blue-tinted neutrals retint every dimmed label,
  neutral badge and table border. The backoffice adopts them as its own
  reviewed change, not as a side effect of sharing a base.
- `primaryShade.dark`: moves every filled control in dark mode; waits until
  dark mode is verified end to end.

Also fixes a live bug: PageLoader coloured its primary label `navy.9`, which is
defined in neither theme. Mantine drops unresolved colour keys silently, so the
label in a component used by 20 files had been rendering an inherited colour.

Adds a visual-regression harness to make all of this reviewable. It runs
against a static gallery route rather than real pages, so it needs no API,
database or auth — a theme diff cannot be masked by a migration or an expired
token. The portal is the control group: it is pixel-identical across all four
light/dark × desktop/tablet baselines, which is what makes the refactor
provably lossless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:25:26 +03:00