initial commit

This commit is contained in:
2026-09-11 02:46:56 +02:00
commit 20b384039c
288 changed files with 46557 additions and 0 deletions
+285
View File
@@ -0,0 +1,285 @@
# SHARP-ER design tokens
Two tiers, so changing the brand is a one-line edit in one file.
```
app/app.css entry - imports tailwindcss then the three files below
app/styles/primitives.css TIER 1 raw values, no meaning (@theme)
app/styles/semantic.css TIER 2 roles that alias tier 1 (per surface)
app/styles/typography.css TIER 2 type roles, @font-face, EN/AR font switch
```
## The one rule
**Components may reference only tier-2 tokens.**
Never a raw hex, never a tier-1 primitive.
This binds `app/app.css` too. Its `@layer base` block styles the document, so
the body type is set from `--type-body-base-size` / `--type-body-base-leading`
(the semantic alias of the Body/Medium role, declared in `semantic.css`) rather
than from `--text-md` / `--leading-6` directly. Only `primitives.css`,
`semantic.css` and `typography.css` may name a tier-1 token.
```tsx
// yes
<button className="bg-[var(--interactive-primary-bg)] text-[var(--interactive-primary-fg)]" />
<button className="bg-interactive-primary text-text-on-accent rounded-control" />
<h2 className="heading-h2 text-[var(--text-primary)]" />
// no
<button className="bg-[#008764]" /> // raw hex
<button className="bg-[var(--color-green-600)]" /> // tier-1 primitive
<h2 className="text-[28px] leading-[36px]" /> // raw type
```
Grep for violations: `grep -rn "#[0-9a-fA-F]\{6\}\|--color-green\|--color-neutral\|--color-red" app/components app/routes`.
### Two ways to spend a tier-2 token
1. `bg-[var(--surface-page)]` - works for every token, always correct.
2. A Tailwind utility from the bridge at the bottom of `semantic.css`. The name
is always `<tailwind-prefix>-<token-name>`:
`--surface-page` -> `bg-surface-page`, `--text-primary` -> `text-text-primary`,
`--border-subtle` -> `border-border-subtle`, `--radius-card` -> `rounded-card`.
The bridge uses `@theme inline`, so these still re-resolve inside a surface.
Type roles are utilities: `display-sm`, `heading-h1`, `heading-h2`, `heading-h3`,
`body-lg`, `body-lg-bold`, `body-md`, `body-md-bold`, `body-sm`, `body-sm-bold`,
`label-md`. They set family, size, line-height, tracking and weight together, and
they compose with variants (`md:heading-h1`).
## Tier 1 - primitives (`primitives.css`)
A Tailwind 4 `@theme static` block. Raw ramps only, named by value:
`--color-green-600`, never `--color-accent`.
* colour - `white`, `black`, `transparent`, `neutral-50..900`, `green-50..900`,
`red-50..900`, plus alpha steps (`white-a12`, `black-a56`, `green-a24`, ...)
* type - `--font-en` / `--font-ar` / `--font-sans` / `--font-mono`,
`--text-xs..5xl`, `--leading-4..12`, `--tracking-tighter..wider`,
`--font-weight-regular..black`
* space `--spacing-2xs..5xl` - radius `--radius-none..full` -
border width `--border-width-sm..xl` - opacity `--opacity-0..100`
* shadow `--shadow-xs..xl` - duration `--duration-instant..slower` -
easing `--ease-standard | decelerate | accelerate | emphasized`
* z ladder `--z-0..70` - containers `--container-prose | content | wide`
The hexes taken verbatim from the published Figma variables are marked `[figma]`;
the rest of each ramp is derived from them (tints mix with white, shades multiply
toward black).
### Changing a brand colour
Edit the ramp in `primitives.css` and nothing else. To move the brand green:
```css
--color-green-600: #008764; /* brand -> every accent, primary button, link */
--color-green-700: #006b4f; /* hover */
--color-green-800: #00563f; /* active */
--color-green-400: #4daa93; /* disabled fill, accent text on dark */
```
Same for `--color-red-*` (danger) and `--color-neutral-800/900` (the dark
surface). No component file changes.
## Tier 2 - semantics (`semantic.css`)
Plain CSS custom properties whose value is always `var(--<primitive>)`. Families:
`--surface-*` `--text-*` `--border-*` `--interactive-<variant>-*` `--field-*`
`--focus-ring-*` `--overlay-*` `--radius-*` `--control-*` `--layout-*`
`--elevation-*` `--motion-*` `--layer-*` `--type-body-base-*`
Interactive tokens are fully regular - for each of the five Figma button
variants (`primary`, `secondary`, `dark`, `danger`, `ghost`) there is
```
--interactive-<variant>-bg -bg-hover -bg-active -bg-disabled
--interactive-<variant>-fg -fg-hover -fg-disabled
--interactive-<variant>-border -border-hover -border-disabled
```
Figma renders the disabled state as the default fill at 40% opacity; that
opacity is available as `--control-opacity-disabled`, and the `-disabled`
tokens above give the equivalent opaque colour for cases where a translucent
button would show the page through it.
## Surfaces, not a global theme
The home page is dark by default (`#1d1d1b`), while the nav and footer also have
light variants, so the whole tier-2 palette is redefined per surface rather than
per theme. `:root` **is** the dark surface; any element can flip its own subtree:
```html
<html lang="en" dir="ltr"> <!-- dark, the default -->
<header data-surface="light"> <!-- light nav on a dark page -->
<main>...</main>
<footer data-surface="light"> <!-- light footer -->
</html>
```
`data-surface` also sets `color-scheme` and paints `background-color` /
`color` on itself, so a flipped section is complete without extra classes.
Because both surface blocks define the *same* token names, a component written
against `--text-primary` is automatically correct on either one - which is why
components must never reach past a role to a primitive.
Utility variants `surface-dark:` and `surface-light:` exist for the rare case a
utility (not a token) has to differ, and `rtl:` / `ltr:` for direction. Prefer
CSS logical properties (`ps-`, `pe-`, `ms-`, `me-`, `start-`, `end-`) so RTL
mirrors without either.
## Fonts
Family stacks live in `primitives.css`; `@font-face` and the language switch live
in `typography.css`. `<html lang="ar">` (or any `:lang(ar)` subtree) rebinds
`--font-heading` / `--font-body` / `--font-ui` to the Arabic stack and resets all
letter-spacing to normal, because Arabic is a joined script. Nothing else changes.
Files in `apps/web/public/fonts/`:
| file | family | weight | status |
| --- | --- | --- | --- |
| `Panton-Regular.woff2` | Panton | 400 | present |
| `Panton-Bold.woff2` | Panton | 700 | present |
| `Panton-SemiBold.woff2` | Panton | 500 | **not supplied** |
| `Montserrat-Arabic-Regular.woff2` | Montserrat Arabic | 400 | present |
| `Montserrat-Arabic-Medium.woff2` | Montserrat Arabic | 500-600 | present |
| `Montserrat-Arabic-Bold.woff2` | Montserrat Arabic | 700 | present |
> The communication guideline PDF names the Arabic face "Montaser Arabic". That
> is a **typo in the guideline** - the correct face is **Montserrat Arabic**,
> confirmed with the client on 2026-09-11. Do not add a "Montaser Arabic"
> family back to `--font-ar`.
One gap, handled without any code change when the file arrives:
* **Panton SemiBold.** `label-md` asks for weight 500 and, with no SemiBold face
declared, CSS font matching resolves it to Panton Regular. When the cut is
supplied, add one `@font-face` for `"Panton"` at `font-weight: 500 600` in
`typography.css`. Do not declare that face before the file exists - a face that
fails to load drops weight-500 text out of the family altogether.
Filenames are case-sensitive in production - match the table exactly.
## Known deviation from Figma
The published Ghost button hover paints `background/accent` (`#008764`) and keeps
`base/turquoise-dark` (`#006b4f`) as the label - 1.40:1, which fails WCAG AA.
`--interactive-ghost-fg-hover` is therefore `--color-white` on **both** surfaces
(4.52:1, passes AA for body text). The two lines are marked `(a11y)` in
`semantic.css`; revert them to `var(--color-green-700)` for pixel-exact parity
if the designer prefers the failure.
## Contrast audit
WCAG 2.1 relative luminance, computed from the sRGB hexes in `primitives.css`.
Translucent surfaces are composited over their own `--surface-page` first.
AA needs **4.5:1** for body text, **3:1** for large text (>=24px, or >=18.66px
bold) and for non-text UI boundaries (1.4.11).
### Dark surface (`:root`, `[data-surface="dark"]`)
| text token | page `#1d1d1b` | raised `#2e2e2c` | sunken `#0b0b0a` | muted (white 8%) | accent-subtle (green 24%) | danger-subtle (red 24%) |
| --- | --- | --- | --- | --- | --- | --- |
| `--text-primary` neutral-50 | 16.02 | 12.91 | 18.68 | 12.71 | 12.40 | 12.86 |
| `--text-secondary` neutral-300 | 9.46 | 7.63 | 11.04 | 7.51 | 7.32 | 7.60 |
| `--text-tertiary` neutral-400 | 5.56 | 4.48 | 6.48 | 4.41 | 4.30 | 4.46 |
| `--text-accent` / `--text-link` green-400 | 6.02 | 4.85 | 7.02 | 4.78 | 4.66 | 4.83 |
| `--text-link-hover` green-300 | 8.56 | 6.90 | 9.99 | 6.80 | 6.63 | 6.87 |
| `--text-danger` red-400 | 5.39 | 4.34 | 6.29 | 4.28 | 4.17 | 4.33 |
| `--text-disabled` neutral-500 | 2.36 | 1.91 | 2.76 | 1.88 | 1.83 | 1.90 |
`--text-primary`, `--text-secondary` and `--text-link-hover` pass AA everywhere.
Everything below them is only cleared against `--surface-page` and
`--surface-sunken`, which is where the design actually uses them:
* `--text-tertiary`, `--text-accent`, `--text-link` and `--text-danger` land
between 4.17 and 4.85 on `--surface-raised` and on the three translucent
fills - AA for large text, **not** for body text. Card bodies use
`--text-secondary`; keep tertiary/accent/danger body copy on the page or
sunken surface, or raise the text a step.
* `--text-disabled` is 2.36:1 by design. WCAG 1.4.3 exempts the label of a
disabled control, so this is deliberate and must not be repointed - it is
the only token allowed below 3:1.
### Light surface (`[data-surface="light"]`)
| text token | page/raised `#ffffff` | sunken `#f8f9fa` | muted `#e8e8e8` | accent-subtle `#dbeee9` | danger-subtle `#fae2e2` |
| --- | --- | --- | --- | --- | --- |
| `--text-primary` neutral-800 | 16.88 | 16.02 | 13.78 | 14.00 | 13.70 |
| `--text-secondary` neutral-500 | 7.14 | 6.77 | 5.83 | 5.92 | 5.80 |
| `--text-tertiary` neutral-500 | 7.14 | 6.77 | 5.83 | 5.92 | 5.80 |
| `--text-link-hover` green-800 | 8.74 | 8.29 | 7.13 | 7.25 | 7.09 |
| `--text-link` green-700 | 6.53 | 6.19 | 5.33 | 5.42 | 5.30 |
| `--text-danger` red-600 | 5.78 | 5.48 | 4.72 | 4.79 | 4.69 |
| `--text-accent` green-600 | 4.52 | 4.28 | 3.69 | 3.75 | 3.67 |
| `--text-disabled` neutral-400 | 3.04 | 2.88 | 2.48 | 2.52 | 2.47 |
* `--text-tertiary` used to point at neutral-400 (`#949492`), 3.04:1 on white -
a body-text failure. It now points at neutral-500, the same primitive as
`--text-secondary`; the two are kept as separate roles so the ramp can be
re-split when a compliant mid-grey is added. `#949492` survives on
`--text-disabled` only, where 1.4.3 exempts it.
* `--text-accent` (the brand green) is 4.52:1 on white and passes, but only just
- it drops to 3.67-3.75 on the muted and subtle fills, which is large-text
only. Use `--text-link` (green-700, 5.30+ everywhere) for accent body copy on
a tinted light panel.
### Foreground on a filled background
| pair | ratio |
| --- | --- |
| `--text-on-accent` white on `--surface-accent` green-600 | 4.52 |
| `--text-on-accent` white on `--surface-danger` red-500 | 4.59 |
| dark `--text-on-inverse` neutral-800 on `--surface-inverse` white | 16.88 |
| light `--text-on-inverse` neutral-50 on `--surface-inverse` neutral-900 | 18.68 |
### Interactive variants (identical on both surfaces unless noted)
| pair | ratio |
| --- | --- |
| primary `fg` white / `bg` green-600 | 4.52 |
| primary `fg-hover` white / `bg-hover` green-700 | 6.53 |
| primary white / `bg-active` green-800 | 8.74 |
| **primary `fg-disabled` white / `bg-disabled` green-400** | **2.80** |
| danger `fg` white / `bg` red-500 | 4.59 |
| danger white / `bg-hover` red-600 | 5.78 |
| danger white / `bg-active` red-700 | 7.32 |
| **danger `fg-disabled` white / `bg-disabled` red-300** | **2.11** |
| dark `fg` white / `bg` neutral-900 | 19.69 |
| dark white / `bg-hover` black | 21.00 |
| dark `fg-disabled` neutral-300 / `bg-disabled` neutral-600 (dark surface) | 5.39 |
| dark `fg-disabled` neutral-200 / `bg-disabled` neutral-500 (light surface) | 4.91 |
| **ghost `fg-hover` white / `bg-hover` green-600** (was green-900, 2.62) | **4.52** |
| ghost white / `bg-active` green-700 | 6.53 |
| ghost `fg` green-400 / dark page | 6.02 |
| ghost `fg` green-600 / light page | 4.52 |
| secondary `fg` neutral-50 / dark page | 16.02 |
| secondary `fg` neutral-800 / light page | 16.88 |
| **secondary `fg-disabled` neutral-500 / dark page** | **2.36** |
| **secondary `fg-disabled` neutral-400 / light page** | **3.04** |
The four bolded disabled rows are the WCAG 1.4.3 exemption again - a disabled
control has no contrast minimum. Every enabled pair clears 4.5:1.
### Fields and boundaries
| pair | dark | light |
| --- | --- | --- |
| `--field-fg` on `--field-bg` | 12.71 | 16.88 |
| `--field-placeholder` on `--field-bg` | 4.41 | 3.04 |
| `--field-border-focus` / `--focus-ring-color` against the page | 6.02 | 4.52 |
| `--field-border` against the page | 2.20 | 1.46 |
| `--border-subtle` against the page | 1.44 | 1.23 |
Two things to know here:
* **`--field-placeholder` is 3.04:1 on the light surface.** A placeholder is not
a label and never carries information in this codebase (`Input` always renders
a real `<label>`), so it is treated like disabled text. Do not promote it to
hint text.
* **`--field-border` and `--border-subtle` are below the 3:1 that 1.4.11 asks of
a control boundary.** They are decorative hairlines: the field's *state* is
carried by `--field-border-focus` (6.02 / 4.52), by the focus ring, and by the
`--text-danger` error message, none of which rely on the resting border. A
field whose only affordance is the resting border would fail.
+249
View File
@@ -0,0 +1,249 @@
/* ============================================================================
TIER 1 - PRIMITIVES
----------------------------------------------------------------------------
Raw values only. No meaning, no roles, no component names.
"green-600", never "accent". "neutral-800", never "page-background".
Nothing in app/ may reference these directly except app/styles/semantic.css
and app/styles/typography.css. Components use TIER 2 (semantic.css).
Values sourced from the published Figma variables of
file yvKHp3Ha2rQmv9kt9tCC7j - the hexes marked [figma] are exact; the
surrounding ramp steps are derived from them (tint = mix with white,
shade = multiply toward black).
========================================================================= */
@theme static {
/* --------------------------------------------------------------------
COLOUR - base
-------------------------------------------------------------------- */
--color-white: #ffffff; /* [figma] base/white */
--color-black: #000000; /* [figma] base/black */
--color-transparent: transparent;
/* --------------------------------------------------------------------
COLOUR - neutral ramp (built around the brand Dark Grey #1d1d1b)
-------------------------------------------------------------------- */
--color-neutral-50: #f8f9fa; /* [figma] text/primary on dark */
--color-neutral-100: #e8e8e8; /* [figma] stroke/subtle */
--color-neutral-200: #d6d6d4;
--color-neutral-300: #c2c2c0; /* [figma] text/secondary */
--color-neutral-400: #949492; /* [figma] text/tertiary */
--color-neutral-500: #5c565c; /* [figma] base/grey-500 */
--color-neutral-600: #454543;
--color-neutral-700: #2e2e2c;
--color-neutral-800: #1d1d1b; /* [figma] Neutral Black/800 - brand Dark Grey */
--color-neutral-900: #0b0b0a; /* [figma] Neutral Black/900 */
/* --------------------------------------------------------------------
COLOUR - green ramp (brand Dark Turquoise #008764)
600 is the brand colour. 700 is the Figma button hover shade.
-------------------------------------------------------------------- */
--color-green-50: #f0f8f5;
--color-green-100: #dbeee9;
--color-green-200: #b3dbd1;
--color-green-300: #85c5b5;
--color-green-400: #4daa93; /* [figma] brand Green/400 - primary button disabled fill */
--color-green-500: #26997b;
--color-green-600: #008764; /* [figma] background/accent, interactive/primary, text/accent */
--color-green-700: #006b4f; /* [figma] base/turquoise-dark, interactive/primary-hover */
--color-green-800: #00563f;
--color-green-900: #00402f;
/* --------------------------------------------------------------------
COLOUR - red ramp (brand Red #dd322f)
500 is the brand colour. 600/700 are derived press states.
-------------------------------------------------------------------- */
--color-red-50: #fdf3f3;
--color-red-100: #fae2e2;
--color-red-200: #f5c2c1;
--color-red-300: #ef9d9b;
--color-red-400: #e76b69;
--color-red-500: #dd322f; /* [figma] interactive/danger, base/red */
--color-red-600: #c02c29;
--color-red-700: #a42523;
--color-red-800: #851e1c;
--color-red-900: #631715;
/* --------------------------------------------------------------------
COLOUR - alpha steps (translucent overlays, hairlines, scrims)
-------------------------------------------------------------------- */
--color-white-a04: rgb(255 255 255 / 0.04);
--color-white-a08: rgb(255 255 255 / 0.08);
--color-white-a12: rgb(255 255 255 / 0.12);
--color-white-a16: rgb(255 255 255 / 0.16);
--color-white-a24: rgb(255 255 255 / 0.24);
--color-white-a40: rgb(255 255 255 / 0.4);
--color-white-a64: rgb(255 255 255 / 0.64);
--color-black-a04: rgb(0 0 0 / 0.04);
--color-black-a08: rgb(0 0 0 / 0.08);
--color-black-a12: rgb(0 0 0 / 0.12);
--color-black-a16: rgb(0 0 0 / 0.16);
--color-black-a24: rgb(0 0 0 / 0.24);
--color-black-a40: rgb(0 0 0 / 0.4);
--color-black-a56: rgb(0 0 0 / 0.56);
--color-black-a72: rgb(0 0 0 / 0.72);
--color-green-a12: rgb(0 135 100 / 0.12);
--color-green-a16: rgb(0 135 100 / 0.16);
--color-green-a24: rgb(0 135 100 / 0.24);
--color-green-a40: rgb(0 135 100 / 0.4);
--color-red-a12: rgb(221 50 47 / 0.12);
--color-red-a16: rgb(221 50 47 / 0.16);
--color-red-a24: rgb(221 50 47 / 0.24);
/* --------------------------------------------------------------------
TYPEFACES
Panton (EN) and Montserrat Arabic (AR) are the licensed brand faces,
licensed into the repo - the first family in each stack is the real
one, everything after it is the free fallback that ships today.
@font-face declarations live in typography.css.
-------------------------------------------------------------------- */
--font-en: "Panton", "Figtree", ui-sans-serif, system-ui, -apple-system,
"Segoe UI", Roboto, sans-serif;
--font-ar: "Montserrat Arabic", "IBM Plex Sans Arabic",
"Noto Sans Arabic", ui-sans-serif, system-ui, sans-serif;
--font-sans: var(--font-en);
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
"Liberation Mono", monospace;
/* --------------------------------------------------------------------
FONT SIZE ramp (px in the comment, rem in the value)
-------------------------------------------------------------------- */
--text-xs: 0.75rem; /* 12 - [figma] Label/Medium */
--text-sm: 0.875rem; /* 14 - [figma] Body/Small */
--text-md: 1rem; /* 16 - [figma] Body/Medium Bold */
--text-lg: 1.125rem; /* 18 - [figma] Body/Large Bold */
--text-xl: 1.375rem; /* 22 */
--text-2xl: 1.75rem; /* 28 - [figma] Heading/H2 */
--text-3xl: 2.25rem; /* 36 */
--text-4xl: 2.75rem; /* 44 - [figma] Display/Small */
--text-5xl: 3.5rem; /* 56 */
/* --------------------------------------------------------------------
LINE HEIGHT ramp - fixed steps, named after their px value / 4
-------------------------------------------------------------------- */
--leading-4: 1rem; /* 16 */
--leading-5: 1.25rem; /* 20 */
--leading-6: 1.5rem; /* 24 */
--leading-7: 1.75rem; /* 28 */
--leading-8: 1.875rem; /* 30 */
--leading-9: 2.25rem; /* 36 */
--leading-10: 2.75rem; /* 44 */
--leading-11: 3.25rem; /* 52 */
--leading-12: 4rem; /* 64 */
/* --------------------------------------------------------------------
LETTER SPACING ramp
-------------------------------------------------------------------- */
--tracking-tighter: -1px; /* [figma] Display/Small */
--tracking-tight: -0.6px;
--tracking-snug: -0.3px; /* [figma] Heading/H2 */
--tracking-normal: 0px;
--tracking-wide: 0.5px; /* [figma] Label/Medium */
--tracking-wider: 1px;
/* --------------------------------------------------------------------
FONT WEIGHT ramp
Panton "SemiBold" is published at weight 500 in the Figma library,
which is why medium is 500 and semibold is 600.
-------------------------------------------------------------------- */
--font-weight-regular: 400;
--font-weight-medium: 500; /* [figma] Label/Medium */
--font-weight-semibold: 600;
--font-weight-bold: 700; /* [figma] every Bold role */
--font-weight-black: 900;
/* --------------------------------------------------------------------
SPACING ramp
-------------------------------------------------------------------- */
--spacing: 0.25rem; /* base unit for p-1, gap-2, ... */
--spacing-2xs: 0.25rem; /* 4 */
--spacing-xs: 0.5rem; /* 8 - [figma] space/xs */
--spacing-sm: 0.75rem; /* 12 - [figma] space/sm */
--spacing-md: 1rem; /* 16 - [figma] space/md */
--spacing-lg: 1.5rem; /* 24 - [figma] space/lg */
--spacing-xl: 2rem; /* 32 */
--spacing-2xl: 3rem; /* 48 */
--spacing-3xl: 4rem; /* 64 */
--spacing-4xl: 6rem; /* 96 */
--spacing-5xl: 8rem; /* 128 */
/* --------------------------------------------------------------------
RADIUS ramp
-------------------------------------------------------------------- */
--radius-none: 0px;
--radius-xs: 2px;
--radius-sm: 4px; /* [figma] radius/sm */
--radius-md: 8px; /* [figma] radius/md */
--radius-lg: 12px;
--radius-xl: 16px;
--radius-2xl: 24px;
--radius-full: 9999px;
/* --------------------------------------------------------------------
BORDER WIDTH ramp
-------------------------------------------------------------------- */
--border-width-sm: 1px;
--border-width-md: 1.5px; /* [figma] secondary button stroke */
--border-width-lg: 2px;
--border-width-xl: 3px;
/* --------------------------------------------------------------------
OPACITY ramp
-------------------------------------------------------------------- */
--opacity-0: 0;
--opacity-8: 0.08;
--opacity-16: 0.16;
--opacity-40: 0.4; /* [figma] every disabled button */
--opacity-64: 0.64;
--opacity-100: 1;
/* --------------------------------------------------------------------
SHADOW ramp
-------------------------------------------------------------------- */
--shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.06);
--shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.08);
--shadow-md: 0 4px 12px -2px rgb(0 0 0 / 0.12), 0 2px 4px -2px rgb(0 0 0 / 0.08);
--shadow-lg: 0 12px 28px -6px rgb(0 0 0 / 0.18), 0 4px 8px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 24px 48px -12px rgb(0 0 0 / 0.28);
--shadow-none: 0 0 #0000;
/* --------------------------------------------------------------------
DURATION ramp
-------------------------------------------------------------------- */
--duration-instant: 0ms;
--duration-fast: 120ms;
--duration-base: 200ms;
--duration-slow: 320ms;
--duration-slower: 520ms;
/* --------------------------------------------------------------------
EASING ramp
-------------------------------------------------------------------- */
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
--ease-decelerate: cubic-bezier(0, 0, 0, 1);
--ease-accelerate: cubic-bezier(0.3, 0, 1, 1);
--ease-emphasized: cubic-bezier(0.2, 0, 0, 1.2);
/* --------------------------------------------------------------------
Z-INDEX ladder - raw ordinals, semantic layer names live in tier 2
-------------------------------------------------------------------- */
--z-0: 0;
--z-10: 10;
--z-20: 20;
--z-30: 30;
--z-40: 40;
--z-50: 50;
--z-60: 60;
--z-70: 70;
/* --------------------------------------------------------------------
CONTAINER widths
-------------------------------------------------------------------- */
--container-prose: 68ch;
--container-content: 80rem; /* 1280 */
--container-wide: 90rem; /* 1440 */
}
+423
View File
@@ -0,0 +1,423 @@
/* ============================================================================
TIER 2 - SEMANTIC TOKENS
----------------------------------------------------------------------------
Every token here carries MEANING and every value is a var() pointing at a
TIER 1 primitive (app/styles/primitives.css). No raw hex is allowed below.
Components may use ONLY these tokens - never a primitive, never a hex.
SURFACES, not a global theme.
The SHARP-ER home page is dark by default (bg #1d1d1b), while the nav and
footer also ship light variants, so the whole tier-2 palette is redefined
per surface. Any element can flip the surface for its own subtree:
<html data-surface="dark"> <!-- default, may be omitted -->
<footer data-surface="light"> <!-- light footer inside a dark page -->
:root and [data-surface="dark"] share one block; [data-surface="light"]
comes afterwards so it wins on the same element, and proximity makes the
nearest data-surface ancestor win for any subtree.
========================================================================= */
/* ---------------------------------------------------------------------------
DARK SURFACE - the default
------------------------------------------------------------------------ */
:root,
[data-surface="dark"] {
color-scheme: dark;
/* --- surfaces / backgrounds ------------------------------------------ */
--surface-page: var(--color-neutral-800); /* figma background/primary */
--surface-raised: var(--color-neutral-700); /* cards sitting on the page */
--surface-sunken: var(--color-neutral-900); /* figma Neutral Black/900 */
--surface-muted: var(--color-white-a08); /* subtle fills, chips, wells */
--surface-accent: var(--color-green-600); /* figma background/accent */
--surface-accent-subtle: var(--color-green-a24);
--surface-danger: var(--color-red-500);
--surface-danger-subtle: var(--color-red-a24);
--surface-inverse: var(--color-white); /* figma background/inverse */
--surface-selection: var(--color-green-a40);
/* --- text ------------------------------------------------------------- */
--text-primary: var(--color-neutral-50); /* figma text/primary */
--text-secondary: var(--color-neutral-300); /* figma text/secondary */
--text-tertiary: var(--color-neutral-400); /* figma text/tertiary */
--text-disabled: var(--color-neutral-500);
--text-accent: var(--color-green-400); /* green-600 is unreadable on #1d1d1b */
--text-danger: var(--color-red-400);
--text-on-accent: var(--color-white); /* figma text/inverse - label on a filled button */
--text-on-inverse: var(--color-neutral-800); /* text sitting on --surface-inverse */
--text-inverse: var(--color-neutral-800); /* alias of --text-on-inverse */
--text-link: var(--color-green-400);
--text-link-hover: var(--color-green-300);
/* --- borders ----------------------------------------------------------- */
--border-subtle: var(--color-white-a12); /* figma stroke/subtle, dark equivalent */
--border-default: var(--color-white-a24);
--border-strong: var(--color-neutral-50);
--border-accent: var(--color-green-600);
--border-danger: var(--color-red-500);
--border-inverse: var(--color-neutral-800);
/* --- interactive: primary (filled green) ------------------------------- */
--interactive-primary-bg: var(--color-green-600);
--interactive-primary-bg-hover: var(--color-green-700);
--interactive-primary-bg-active: var(--color-green-800);
--interactive-primary-bg-disabled: var(--color-green-400);
--interactive-primary-fg: var(--color-white);
--interactive-primary-fg-hover: var(--color-white);
--interactive-primary-fg-disabled: var(--color-white);
--interactive-primary-border: var(--color-transparent);
--interactive-primary-border-hover: var(--color-transparent);
--interactive-primary-border-disabled: var(--color-transparent);
/* --- interactive: secondary (outlined) --------------------------------- */
--interactive-secondary-bg: var(--color-transparent);
--interactive-secondary-bg-hover: var(--color-white-a12);
--interactive-secondary-bg-active: var(--color-white-a16);
--interactive-secondary-bg-disabled: var(--color-transparent);
--interactive-secondary-fg: var(--color-neutral-50);
--interactive-secondary-fg-hover: var(--color-neutral-50);
--interactive-secondary-fg-disabled: var(--color-neutral-500);
--interactive-secondary-border: var(--color-neutral-50);
--interactive-secondary-border-hover: var(--color-neutral-50);
--interactive-secondary-border-disabled: var(--color-white-a24);
/* --- interactive: dark (near-black fill) -------------------------------- */
--interactive-dark-bg: var(--color-neutral-900);
--interactive-dark-bg-hover: var(--color-black);
--interactive-dark-bg-active: var(--color-black);
--interactive-dark-bg-disabled: var(--color-neutral-600);
--interactive-dark-fg: var(--color-white);
--interactive-dark-fg-hover: var(--color-white);
--interactive-dark-fg-disabled: var(--color-neutral-300);
--interactive-dark-border: var(--color-white-a24);
--interactive-dark-border-hover: var(--color-white-a24);
--interactive-dark-border-disabled: var(--color-white-a12);
/* --- interactive: danger ------------------------------------------------ */
--interactive-danger-bg: var(--color-red-500);
--interactive-danger-bg-hover: var(--color-red-600);
--interactive-danger-bg-active: var(--color-red-700);
--interactive-danger-bg-disabled: var(--color-red-300);
--interactive-danger-fg: var(--color-white);
--interactive-danger-fg-hover: var(--color-white);
--interactive-danger-fg-disabled: var(--color-white);
--interactive-danger-border: var(--color-transparent);
--interactive-danger-border-hover: var(--color-transparent);
--interactive-danger-border-disabled: var(--color-transparent);
/* --- interactive: ghost (text-only) -------------------------------------
DEVIATION FROM FIGMA (a11y). The published component paints the hover
background/accent (#008764) and keeps base/turquoise-dark (#006b4f) as
the label - 1.40:1, far below WCAG AA. Both surfaces therefore use white
on the accent fill, which is 4.52:1 and passes AA for body text. The two
lines marked (a11y) are the only ones that differ from Figma; reverting
them to var(--color-green-700) restores pixel parity and the failure. */
--interactive-ghost-bg: var(--color-transparent);
--interactive-ghost-bg-hover: var(--color-green-600);
--interactive-ghost-bg-active: var(--color-green-700);
--interactive-ghost-bg-disabled: var(--color-transparent);
--interactive-ghost-fg: var(--color-green-400);
--interactive-ghost-fg-hover: var(--color-white); /* (a11y) 4.52:1 on green-600; figma: green-700 = 1.40:1 */
--interactive-ghost-fg-disabled: var(--color-green-400);
--interactive-ghost-border: var(--color-transparent);
--interactive-ghost-border-hover: var(--color-transparent);
--interactive-ghost-border-disabled: var(--color-transparent);
/* --- form fields --------------------------------------------------------- */
--field-bg: var(--color-white-a08);
--field-bg-disabled: var(--color-white-a04);
--field-fg: var(--color-neutral-50);
--field-fg-disabled: var(--color-neutral-500);
--field-placeholder: var(--color-neutral-400);
--field-border: var(--color-white-a24);
--field-border-hover: var(--color-white-a40);
--field-border-focus: var(--color-green-400);
--field-border-error: var(--color-red-400);
/* --- focus ring ---------------------------------------------------------- */
--focus-ring-color: var(--color-green-400);
--focus-ring-color-inverse: var(--color-white);
/* --- overlay / scrim ------------------------------------------------------ */
--overlay-scrim: var(--color-black-a56);
--overlay-scrim-strong: var(--color-black-a72);
--overlay-veil: var(--color-white-a08);
}
/* ---------------------------------------------------------------------------
LIGHT SURFACE - light nav, light footer, light content sections
Same token names, re-pointed at different primitives.
------------------------------------------------------------------------ */
[data-surface="light"] {
color-scheme: light;
/* --- surfaces / backgrounds ------------------------------------------ */
--surface-page: var(--color-white); /* figma background/primary (light) */
--surface-raised: var(--color-white);
--surface-sunken: var(--color-neutral-50);
--surface-muted: var(--color-neutral-100); /* figma stroke/subtle as a fill */
--surface-accent: var(--color-green-600);
--surface-accent-subtle: var(--color-green-100);
--surface-danger: var(--color-red-500);
--surface-danger-subtle: var(--color-red-100);
--surface-inverse: var(--color-neutral-900); /* figma background/inverse #0b0b0a */
--surface-selection: var(--color-green-200);
/* --- text ------------------------------------------------------------- */
--text-primary: var(--color-neutral-800); /* figma text/primary (light) */
--text-secondary: var(--color-neutral-500);
/* neutral-400 (#949492) is only 3.04:1 on white - below AA for body text.
Tertiary is real, readable copy (eyebrows, meta lines), so it takes
neutral-500 (7.14:1). #949492 stays on --text-disabled, where WCAG 1.4.3
exempts a disabled control's own label. On the dark surface neutral-400
is 5.56:1 and needs no change. */
--text-tertiary: var(--color-neutral-500);
--text-disabled: var(--color-neutral-400);
--text-accent: var(--color-green-600); /* figma text/accent */
--text-danger: var(--color-red-600);
--text-on-accent: var(--color-white); /* figma text/inverse */
--text-on-inverse: var(--color-neutral-50);
--text-inverse: var(--color-neutral-50);
--text-link: var(--color-green-700);
--text-link-hover: var(--color-green-800);
/* --- borders ----------------------------------------------------------- */
--border-subtle: var(--color-neutral-100); /* figma stroke/subtle #e8e8e8 */
--border-default: var(--color-neutral-200);
--border-strong: var(--color-black); /* figma base/black button stroke */
--border-accent: var(--color-green-600);
--border-danger: var(--color-red-500);
--border-inverse: var(--color-neutral-50);
/* --- interactive: primary (filled green) ------------------------------- */
--interactive-primary-bg: var(--color-green-600);
--interactive-primary-bg-hover: var(--color-green-700);
--interactive-primary-bg-active: var(--color-green-800);
--interactive-primary-bg-disabled: var(--color-green-400);
--interactive-primary-fg: var(--color-white);
--interactive-primary-fg-hover: var(--color-white);
--interactive-primary-fg-disabled: var(--color-white);
--interactive-primary-border: var(--color-transparent);
--interactive-primary-border-hover: var(--color-transparent);
--interactive-primary-border-disabled: var(--color-transparent);
/* --- interactive: secondary (outlined) --------------------------------- */
--interactive-secondary-bg: var(--color-white);
--interactive-secondary-bg-hover: var(--color-neutral-100);
--interactive-secondary-bg-active: var(--color-neutral-200);
--interactive-secondary-bg-disabled: var(--color-white);
--interactive-secondary-fg: var(--color-neutral-800);
--interactive-secondary-fg-hover: var(--color-neutral-800);
--interactive-secondary-fg-disabled: var(--color-neutral-400);
--interactive-secondary-border: var(--color-black);
--interactive-secondary-border-hover: var(--color-black);
--interactive-secondary-border-disabled: var(--color-neutral-200);
/* --- interactive: dark (near-black fill) -------------------------------- */
--interactive-dark-bg: var(--color-neutral-900);
--interactive-dark-bg-hover: var(--color-black);
--interactive-dark-bg-active: var(--color-black);
--interactive-dark-bg-disabled: var(--color-neutral-500);
--interactive-dark-fg: var(--color-white);
--interactive-dark-fg-hover: var(--color-white);
--interactive-dark-fg-disabled: var(--color-neutral-200);
--interactive-dark-border: var(--color-transparent);
--interactive-dark-border-hover: var(--color-transparent);
--interactive-dark-border-disabled: var(--color-transparent);
/* --- interactive: danger ------------------------------------------------ */
--interactive-danger-bg: var(--color-red-500);
--interactive-danger-bg-hover: var(--color-red-600);
--interactive-danger-bg-active: var(--color-red-700);
--interactive-danger-bg-disabled: var(--color-red-300);
--interactive-danger-fg: var(--color-white);
--interactive-danger-fg-hover: var(--color-white);
--interactive-danger-fg-disabled: var(--color-white);
--interactive-danger-border: var(--color-transparent);
--interactive-danger-border-hover: var(--color-transparent);
--interactive-danger-border-disabled: var(--color-transparent);
/* --- interactive: ghost (text-only) - see the a11y note above ---------- */
--interactive-ghost-bg: var(--color-transparent);
--interactive-ghost-bg-hover: var(--color-green-600);
--interactive-ghost-bg-active: var(--color-green-700);
--interactive-ghost-bg-disabled: var(--color-transparent);
--interactive-ghost-fg: var(--color-green-600);
--interactive-ghost-fg-hover: var(--color-white); /* (a11y) 4.52:1 on green-600; figma: green-700 = 1.40:1 */
--interactive-ghost-fg-disabled: var(--color-green-600);
--interactive-ghost-border: var(--color-transparent);
--interactive-ghost-border-hover: var(--color-transparent);
--interactive-ghost-border-disabled: var(--color-transparent);
/* --- form fields --------------------------------------------------------- */
--field-bg: var(--color-white);
--field-bg-disabled: var(--color-neutral-50);
--field-fg: var(--color-neutral-800);
--field-fg-disabled: var(--color-neutral-400);
--field-placeholder: var(--color-neutral-400);
--field-border: var(--color-neutral-200);
--field-border-hover: var(--color-neutral-500);
--field-border-focus: var(--color-green-600);
--field-border-error: var(--color-red-500);
/* --- focus ring ---------------------------------------------------------- */
--focus-ring-color: var(--color-green-600);
--focus-ring-color-inverse: var(--color-neutral-900);
/* --- overlay / scrim ------------------------------------------------------ */
--overlay-scrim: var(--color-black-a40);
--overlay-scrim-strong: var(--color-black-a56);
--overlay-veil: var(--color-black-a04);
}
/* ---------------------------------------------------------------------------
SURFACE-INDEPENDENT SEMANTICS
Sizes, shapes, motion and stacking do not change between surfaces, so they
are declared once. Values still alias tier 1 only.
------------------------------------------------------------------------ */
:root {
/* --- shape -------------------------------------------------------------
--radius-control / --radius-control-lg / --radius-card / --radius-surface
/ --radius-pill are declared in the @theme block near the bottom of this
file so that they also generate rounded-* utilities. They are tier-2
tokens like everything else here. */
/* --- control metrics (from the Figma Button frame) ---------------------- */
--control-border-width: var(--border-width-md); /* 1.5px outlined button */
--control-gap: var(--spacing-xs); /* 8px label <-> icon */
--control-icon-size: var(--spacing-lg); /* 24px icon box */
--control-padding-inline-sm: var(--spacing-md); /* 16 */
--control-padding-block-sm: var(--spacing-xs); /* 8 */
--control-padding-inline-md: var(--spacing-lg); /* 24 */
--control-padding-block-md: var(--spacing-sm); /* 12 */
--control-padding-inline-lg: var(--spacing-lg); /* 24 */
--control-padding-block-lg: var(--spacing-sm); /* 12 */
--control-opacity-disabled: var(--opacity-40); /* figma disabled state */
/* --- base document type -------------------------------------------------
The only type the base layer in app/app.css sets on <body>; everything
else is a type-role utility from typography.css. It lives here so that
app.css never has to reach past a role to --text-md / --leading-6.
Values match the `body-md` role (Body/Medium, 16/24). */
--type-body-base-size: var(--text-md);
--type-body-base-leading: var(--leading-6);
/* --- layout ------------------------------------------------------------- */
--layout-gutter: var(--spacing-lg);
--layout-gutter-lg: var(--spacing-xl);
--layout-section-gap: var(--spacing-3xl);
--layout-stack-gap: var(--spacing-md);
--layout-content-max: var(--container-content);
--layout-prose-max: var(--container-prose);
/* --- elevation ---------------------------------------------------------- */
--elevation-flat: var(--shadow-none);
--elevation-raised: var(--shadow-sm);
--elevation-card: var(--shadow-md);
--elevation-popover: var(--shadow-lg);
--elevation-modal: var(--shadow-xl);
/* --- focus ring geometry ------------------------------------------------- */
--focus-ring-width: var(--border-width-lg);
--focus-ring-offset: var(--border-width-lg);
/* --- motion --------------------------------------------------------------- */
--motion-duration-fast: var(--duration-fast);
--motion-duration-base: var(--duration-base);
--motion-duration-slow: var(--duration-slow);
--motion-ease-standard: var(--ease-standard);
--motion-ease-entrance: var(--ease-decelerate);
--motion-ease-exit: var(--ease-accelerate);
--motion-ease-emphasized: var(--ease-emphasized);
/* --- stacking layers ------------------------------------------------------- */
--layer-base: var(--z-0);
--layer-raised: var(--z-10);
--layer-sticky: var(--z-20);
--layer-header: var(--z-30);
--layer-dropdown: var(--z-40);
--layer-overlay: var(--z-50);
--layer-modal: var(--z-60);
--layer-toast: var(--z-70);
}
/* ---------------------------------------------------------------------------
TAILWIND BRIDGE
`@theme inline` exposes the semantic tokens as Tailwind utilities without
freezing their value, so `bg-surface-page` still re-resolves inside a
[data-surface] subtree. The utility name is always
<tailwind-prefix>-<token-name>: --surface-page -> bg-surface-page,
--text-primary -> text-text-primary, --border-subtle -> border-border-subtle.
Using bg-[var(--surface-page)] directly is equally valid.
------------------------------------------------------------------------ */
@theme inline {
--color-surface-page: var(--surface-page);
--color-surface-raised: var(--surface-raised);
--color-surface-sunken: var(--surface-sunken);
--color-surface-muted: var(--surface-muted);
--color-surface-accent: var(--surface-accent);
--color-surface-accent-subtle: var(--surface-accent-subtle);
--color-surface-danger: var(--surface-danger);
--color-surface-danger-subtle: var(--surface-danger-subtle);
--color-surface-inverse: var(--surface-inverse);
--color-text-primary: var(--text-primary);
--color-text-secondary: var(--text-secondary);
--color-text-tertiary: var(--text-tertiary);
--color-text-disabled: var(--text-disabled);
--color-text-accent: var(--text-accent);
--color-text-danger: var(--text-danger);
--color-text-on-accent: var(--text-on-accent);
--color-text-on-inverse: var(--text-on-inverse);
--color-text-inverse: var(--text-inverse);
--color-text-link: var(--text-link);
--color-text-link-hover: var(--text-link-hover);
--color-border-subtle: var(--border-subtle);
--color-border-default: var(--border-default);
--color-border-strong: var(--border-strong);
--color-border-accent: var(--border-accent);
--color-border-danger: var(--border-danger);
--color-border-inverse: var(--border-inverse);
--color-interactive-primary: var(--interactive-primary-bg);
--color-interactive-primary-hover: var(--interactive-primary-bg-hover);
--color-interactive-primary-active: var(--interactive-primary-bg-active);
--color-interactive-primary-disabled: var(--interactive-primary-bg-disabled);
--color-interactive-secondary: var(--interactive-secondary-bg);
--color-interactive-secondary-hover: var(--interactive-secondary-bg-hover);
--color-interactive-dark: var(--interactive-dark-bg);
--color-interactive-dark-hover: var(--interactive-dark-bg-hover);
--color-interactive-danger: var(--interactive-danger-bg);
--color-interactive-danger-hover: var(--interactive-danger-bg-hover);
--color-interactive-ghost-hover: var(--interactive-ghost-bg-hover);
--color-field-bg: var(--field-bg);
--color-field-border: var(--field-border);
--color-field-border-focus: var(--field-border-focus);
--color-field-border-error: var(--field-border-error);
--color-field-placeholder: var(--field-placeholder);
--color-focus-ring: var(--focus-ring-color);
--color-overlay-scrim: var(--overlay-scrim);
}
/* Shape semantics that also need to exist as Tailwind utilities
(rounded-control / rounded-card / rounded-surface). They alias tier 1
directly - never a tier-2 name, which would self-reference. */
@theme static {
--radius-control: var(--radius-sm);
--radius-control-lg: var(--radius-md);
--radius-card: var(--radius-md);
--radius-surface: var(--radius-lg);
--radius-pill: var(--radius-full);
}
/* ---------------------------------------------------------------------------
SURFACE VARIANTS - target a surface explicitly from a utility class,
e.g. `surface-light:underline`.
------------------------------------------------------------------------ */
@custom-variant surface-dark (&:where([data-surface="dark"], [data-surface="dark"] *));
@custom-variant surface-light (&:where([data-surface="light"], [data-surface="light"] *));
+224
View File
@@ -0,0 +1,224 @@
/* ============================================================================
TYPE ROLES (tier 2 for typography)
----------------------------------------------------------------------------
Composes tier-1 primitives into the named roles published in Figma
(Display/Small, Heading/H2, Body/*, Label/Medium) plus the two headings the
scale needs but Figma has not drawn yet (H1, H3), derived from the same ramp.
Components use the utilities below - `display-sm`, `heading-h2`, `body-md`,
`label-md` - never a raw font-size / line-height / letter-spacing.
Panton (EN) and Montserrat Arabic (AR) are the brand faces, both supplied
and present in apps/web/public/fonts/. Licensed to the project, not to this
repository - .gitignore keeps every woff2 out of version control.
See ./README.md for the filename of every face.
========================================================================= */
/* ---------------------------------------------------------------------------
@font-face - Panton (English / Latin)
Regular and Bold are supplied and live in public/fonts/. A SemiBold cut has
not been supplied, so Label/Medium (weight 500) resolves to Panton Regular
through normal CSS font matching - see ./README.md to upgrade it.
------------------------------------------------------------------------ */
@font-face {
font-family: "Panton";
src: url("/fonts/Panton-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Panton";
src: url("/fonts/Panton-Bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* ---------------------------------------------------------------------------
@font-face - Arabic
"Montserrat Arabic" is THE Arabic brand face. The communication guideline
PDF names "Montaser Arabic", but that is a typo in the guideline - confirmed
with the client on 2026-09-11. Do not reintroduce a "Montaser Arabic"
family: it does not exist and would silently shadow the real one, because it
would sit ahead of Montserrat in --font-ar and match any similarly-named
face a visitor happens to have installed.
Three weights are supplied in public/fonts/. unicode-range keeps every
Arabic file off English pages entirely.
------------------------------------------------------------------------ */
@font-face {
font-family: "Montserrat Arabic";
src: url("/fonts/Montserrat-Arabic-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+08A0-08FF,
U+FB50-FDFF, U+FE70-FEFF, U+200C-200F, U+2010-2011, U+204F, U+2E41,
U+0660-0669;
}
@font-face {
font-family: "Montserrat Arabic";
src: url("/fonts/Montserrat-Arabic-Medium.woff2") format("woff2");
font-weight: 500 600;
font-style: normal;
font-display: swap;
unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+08A0-08FF,
U+FB50-FDFF, U+FE70-FEFF, U+200C-200F, U+2010-2011, U+204F, U+2E41,
U+0660-0669;
}
@font-face {
font-family: "Montserrat Arabic";
src: url("/fonts/Montserrat-Arabic-Bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+08A0-08FF,
U+FB50-FDFF, U+FE70-FEFF, U+200C-200F, U+2010-2011, U+204F, U+2E41,
U+0660-0669;
}
/* ---------------------------------------------------------------------------
TYPE SEMANTIC TOKENS
The EN/AR switch is a token rebind, not a second set of utilities: every
role below reads --font-heading / --font-body and the role tracking tokens,
so `<html lang="ar">` - or any Arabic subtree - re-points them once and the
whole scale follows.
Latin tracking is negative on display sizes and positive on labels; Arabic
is a joined script where letter-spacing breaks the connections, so all
Arabic tracking resets to normal.
------------------------------------------------------------------------ */
:root {
--font-heading: var(--font-en);
--font-body: var(--font-en);
--font-ui: var(--font-en);
--type-tracking-display: var(--tracking-tighter);
--type-tracking-h1: var(--tracking-tight);
--type-tracking-h2: var(--tracking-snug);
--type-tracking-h3: var(--tracking-snug);
--type-tracking-body: var(--tracking-normal);
--type-tracking-label: var(--tracking-wide);
}
[lang="ar"],
:lang(ar) {
--font-heading: var(--font-ar);
--font-body: var(--font-ar);
--font-ui: var(--font-ar);
--type-tracking-display: var(--tracking-normal);
--type-tracking-h1: var(--tracking-normal);
--type-tracking-h2: var(--tracking-normal);
--type-tracking-h3: var(--tracking-normal);
--type-tracking-body: var(--tracking-normal);
--type-tracking-label: var(--tracking-normal);
}
/* ---------------------------------------------------------------------------
TYPE ROLE UTILITIES
------------------------------------------------------------------------ */
/* Display/Small - Bold 44/52, ls -1 [figma] */
@utility display-sm {
font-family: var(--font-heading);
font-size: var(--text-4xl);
line-height: var(--leading-11);
letter-spacing: var(--type-tracking-display);
font-weight: var(--font-weight-bold);
}
/* Heading/H1 - Bold 36/44 (derived - not yet drawn in Figma) */
@utility heading-h1 {
font-family: var(--font-heading);
font-size: var(--text-3xl);
line-height: var(--leading-10);
letter-spacing: var(--type-tracking-h1);
font-weight: var(--font-weight-bold);
}
/* Heading/H2 - Bold 28/36, ls -0.3 [figma] */
@utility heading-h2 {
font-family: var(--font-heading);
font-size: var(--text-2xl);
line-height: var(--leading-9);
letter-spacing: var(--type-tracking-h2);
font-weight: var(--font-weight-bold);
}
/* Heading/H3 - Bold 22/30 (derived - not yet drawn in Figma) */
@utility heading-h3 {
font-family: var(--font-heading);
font-size: var(--text-xl);
line-height: var(--leading-8);
letter-spacing: var(--type-tracking-h3);
font-weight: var(--font-weight-bold);
}
/* Body/Large - 18/28 */
@utility body-lg {
font-family: var(--font-body);
font-size: var(--text-lg);
line-height: var(--leading-7);
letter-spacing: var(--type-tracking-body);
font-weight: var(--font-weight-regular);
}
/* Body/Large Bold - Bold 18/28 [figma] */
@utility body-lg-bold {
font-family: var(--font-body);
font-size: var(--text-lg);
line-height: var(--leading-7);
letter-spacing: var(--type-tracking-body);
font-weight: var(--font-weight-bold);
}
/* Body/Medium - 16/24 */
@utility body-md {
font-family: var(--font-body);
font-size: var(--text-md);
line-height: var(--leading-6);
letter-spacing: var(--type-tracking-body);
font-weight: var(--font-weight-regular);
}
/* Body/Medium Bold - Bold 16/24 [figma] */
@utility body-md-bold {
font-family: var(--font-body);
font-size: var(--text-md);
line-height: var(--leading-6);
letter-spacing: var(--type-tracking-body);
font-weight: var(--font-weight-bold);
}
/* Body/Small - Regular 14/20 [figma] */
@utility body-sm {
font-family: var(--font-body);
font-size: var(--text-sm);
line-height: var(--leading-5);
letter-spacing: var(--type-tracking-body);
font-weight: var(--font-weight-regular);
}
/* Body/Small Bold - Bold 14/20 [figma] */
@utility body-sm-bold {
font-family: var(--font-body);
font-size: var(--text-sm);
line-height: var(--leading-5);
letter-spacing: var(--type-tracking-body);
font-weight: var(--font-weight-bold);
}
/* Label/Medium - SemiBold 12/16, ls 0.5 [figma] */
@utility label-md {
font-family: var(--font-ui);
font-size: var(--text-xs);
line-height: var(--leading-4);
letter-spacing: var(--type-tracking-label);
font-weight: var(--font-weight-medium);
}