initial commit
This commit is contained in:
@@ -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.
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -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"] *));
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user