2026-09-11 02:46:56 +02:00
|
|
|
/* ============================================================================
|
|
|
|
|
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.
|
2026-09-23 21:57:24 +02:00
|
|
|
|
|
|
|
|
THEME TOGGLE (the declared exception to "no global theme"). The nav's
|
|
|
|
|
theme control flips WHICH surface the DOCUMENT stands on: the root action
|
|
|
|
|
persists `sharper-theme`, the root loader returns it, and <html
|
|
|
|
|
data-surface> follows - so light mode means the whole site stands on
|
|
|
|
|
[data-surface="light"] and every token re-resolves, including the nav and
|
|
|
|
|
footer, which follow the theme unless a route's handle forces a surface.
|
2026-09-11 02:46:56 +02:00
|
|
|
========================================================================= */
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
DARK SURFACE - the default
|
|
|
|
|
------------------------------------------------------------------------ */
|
|
|
|
|
:root,
|
|
|
|
|
[data-surface="dark"] {
|
|
|
|
|
color-scheme: dark;
|
|
|
|
|
|
|
|
|
|
/* --- surfaces / backgrounds ------------------------------------------ */
|
|
|
|
|
--surface-page: var(--color-neutral-800); /* figma background/primary */
|
2026-09-16 20:32:56 +02:00
|
|
|
/* The drafts draw a card on a band DARKER than the band (#141413 on
|
|
|
|
|
#1D1D1B), not lighter - so "raised" is a recessed panel here, and the
|
|
|
|
|
old neutral-700 step is not used by the dark palette at all. */
|
|
|
|
|
--surface-raised: var(--color-neutral-850); /* the drafts' card / panel */
|
2026-09-11 02:46:56 +02:00
|
|
|
--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 */
|
2026-09-16 20:32:56 +02:00
|
|
|
--surface-footer: var(--color-neutral-850); /* the drafts' #141413 - the footer sits below the page, not on it */
|
|
|
|
|
/* The drafts float the bar over the page: the ground colour at 90%, with a
|
|
|
|
|
blur behind it, so content shows through as it scrolls under. */
|
|
|
|
|
--surface-nav: color-mix(in srgb, var(--color-neutral-900) 90%, transparent);
|
|
|
|
|
/* A card is one step off whatever it stands on: #1D1D1B on the #0B0B0A
|
|
|
|
|
ground, #141413 inside a #1D1D1B band. Components paint
|
|
|
|
|
`--surface-card`; a band re-points it for its own subtree (.sh-band). */
|
|
|
|
|
--surface-card: var(--surface-page);
|
2026-09-11 02:46:56 +02:00
|
|
|
--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);
|
2026-09-16 20:32:56 +02:00
|
|
|
/* The redesign's outline button: a mid-grey ring at rest that brightens to
|
|
|
|
|
white under the pointer, rather than a full-white ring throughout. */
|
|
|
|
|
--interactive-secondary-border: var(--color-neutral-400);
|
2026-09-11 02:46:56 +02:00
|
|
|
--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) -------------------------------------
|
2026-09-20 11:14:15 +02:00
|
|
|
The drafts never fill a ghost control. Every "All updates ->" / "What we
|
|
|
|
|
do ->" link is plain text in --text-primary that turns accent green under
|
|
|
|
|
the pointer while the label-to-arrow gap opens; the fill the old Figma
|
|
|
|
|
Button frame painted on hover (background/accent behind a green label,
|
|
|
|
|
1.40:1) is gone, and with it the a11y deviation it forced. */
|
2026-09-11 02:46:56 +02:00
|
|
|
--interactive-ghost-bg: var(--color-transparent);
|
2026-09-20 11:14:15 +02:00
|
|
|
--interactive-ghost-bg-hover: var(--color-transparent);
|
|
|
|
|
--interactive-ghost-bg-active: var(--color-transparent);
|
2026-09-11 02:46:56 +02:00
|
|
|
--interactive-ghost-bg-disabled: var(--color-transparent);
|
2026-09-20 11:14:15 +02:00
|
|
|
--interactive-ghost-fg: var(--color-neutral-50);
|
|
|
|
|
--interactive-ghost-fg-hover: var(--color-green-400); /* 6.02:1 on the page ground */
|
|
|
|
|
--interactive-ghost-fg-disabled: var(--color-neutral-500);
|
2026-09-11 02:46:56 +02:00
|
|
|
--interactive-ghost-border: var(--color-transparent);
|
|
|
|
|
--interactive-ghost-border-hover: var(--color-transparent);
|
|
|
|
|
--interactive-ghost-border-disabled: var(--color-transparent);
|
|
|
|
|
|
|
|
|
|
/* --- form fields --------------------------------------------------------- */
|
2026-09-16 20:32:56 +02:00
|
|
|
/* The drafts sink an input into its panel (#0B0B0A inside #1D1D1B) rather
|
|
|
|
|
than lifting it with a translucent white. */
|
|
|
|
|
--field-bg: var(--color-neutral-900);
|
2026-09-11 02:46:56 +02:00
|
|
|
--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);
|
2026-09-14 12:38:05 +02:00
|
|
|
|
|
|
|
|
/* The home hero's picture wash (figma 253:4800): a flat mint tint painted
|
|
|
|
|
over a flat shade. Both sit on a photograph rather than on the page, so
|
|
|
|
|
they carry the same value on either surface. */
|
|
|
|
|
--overlay-picture-shade: var(--color-black-a20);
|
|
|
|
|
--overlay-picture-tint: var(--color-green-200-a12);
|
2026-09-20 11:14:15 +02:00
|
|
|
|
|
|
|
|
/* --- elevation ----------------------------------------------------------
|
|
|
|
|
The drafts give a resting card a hairline and no shadow at all, and keep
|
|
|
|
|
the black for what genuinely floats: a card under the pointer and the
|
|
|
|
|
navigation dropdown. */
|
|
|
|
|
--elevation-card: var(--shadow-none);
|
|
|
|
|
--elevation-popover: var(--shadow-dark-lg);
|
|
|
|
|
--elevation-modal: var(--shadow-dark-xl);
|
2026-09-11 02:46:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
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);
|
2026-09-16 20:32:56 +02:00
|
|
|
--surface-card: var(--surface-raised);
|
2026-09-11 02:46:56 +02:00
|
|
|
--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 */
|
2026-09-14 12:38:05 +02:00
|
|
|
--surface-footer: var(--color-white); /* figma 140:7812, the light footer */
|
2026-09-23 21:57:24 +02:00
|
|
|
/* The light bar floats the same way the dark one does - the ground colour at
|
|
|
|
|
90% with a blur behind it, so content shows through as it scrolls under. */
|
|
|
|
|
--surface-nav: color-mix(in srgb, var(--color-white) 90%, transparent);
|
2026-09-11 02:46:56 +02:00
|
|
|
--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);
|
|
|
|
|
|
2026-09-20 11:14:15 +02:00
|
|
|
/* --- interactive: ghost (text-only) - see the note above --------------- */
|
2026-09-11 02:46:56 +02:00
|
|
|
--interactive-ghost-bg: var(--color-transparent);
|
2026-09-20 11:14:15 +02:00
|
|
|
--interactive-ghost-bg-hover: var(--color-transparent);
|
|
|
|
|
--interactive-ghost-bg-active: var(--color-transparent);
|
2026-09-11 02:46:56 +02:00
|
|
|
--interactive-ghost-bg-disabled: var(--color-transparent);
|
2026-09-20 11:14:15 +02:00
|
|
|
--interactive-ghost-fg: var(--color-neutral-800);
|
|
|
|
|
--interactive-ghost-fg-hover: var(--color-green-700); /* 6.53:1 on white */
|
|
|
|
|
--interactive-ghost-fg-disabled: var(--color-neutral-400);
|
2026-09-11 02:46:56 +02:00
|
|
|
--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);
|
2026-09-14 12:38:05 +02:00
|
|
|
|
|
|
|
|
/* The home hero's picture wash (figma 253:4800): a flat mint tint painted
|
|
|
|
|
over a flat shade. Both sit on a photograph rather than on the page, so
|
|
|
|
|
they carry the same value on either surface. */
|
|
|
|
|
--overlay-picture-shade: var(--color-black-a20);
|
|
|
|
|
--overlay-picture-tint: var(--color-green-200-a12);
|
2026-09-20 11:14:15 +02:00
|
|
|
|
|
|
|
|
/* --- elevation - the soft ramp; the deep black belongs on the dark ground */
|
|
|
|
|
--elevation-card: var(--shadow-md);
|
|
|
|
|
--elevation-popover: var(--shadow-lg);
|
|
|
|
|
--elevation-modal: var(--shadow-xl);
|
2026-09-11 02:46:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
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 -------------------------------------------------------------
|
2026-09-20 11:14:15 +02:00
|
|
|
--radius-chip / --radius-control / --radius-card / --radius-surface
|
2026-09-11 02:46:56 +02:00
|
|
|
/ --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);
|
2026-09-16 20:32:56 +02:00
|
|
|
/* Hero block padding. The drafts open a hero at 52px and grow it to 92px on
|
|
|
|
|
a wide screen, closing it at 44 -> 68. Kept as tokens because the hero is
|
|
|
|
|
what the drifting wash is clipped by: a short hero crops the glow. */
|
|
|
|
|
--layout-hero-top: clamp(3.25rem, 7vw, 5.75rem);
|
|
|
|
|
--layout-hero-bottom: clamp(2.75rem, 6vw, 4.25rem);
|
|
|
|
|
/* Clearance under the sticky bar for a sticky column (the drafts' 100px). */
|
|
|
|
|
--layout-sticky-top: 6.25rem;
|
2026-09-11 02:46:56 +02:00
|
|
|
--layout-stack-gap: var(--spacing-md);
|
|
|
|
|
--layout-content-max: var(--container-content);
|
|
|
|
|
--layout-prose-max: var(--container-prose);
|
|
|
|
|
|
2026-09-20 11:14:15 +02:00
|
|
|
/* --- elevation ----------------------------------------------------------
|
|
|
|
|
Only the two flattest steps are surface-independent. A shadow is a
|
|
|
|
|
fraction of the ground it falls on, so --elevation-card / -popover /
|
|
|
|
|
-modal are declared inside each surface block above: the dark surface
|
|
|
|
|
takes the drafts' deep black, the light one keeps the soft ramp. */
|
2026-09-11 02:46:56 +02:00
|
|
|
--elevation-flat: var(--shadow-none);
|
|
|
|
|
--elevation-raised: var(--shadow-sm);
|
|
|
|
|
|
|
|
|
|
/* --- 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);
|
2026-09-16 20:32:56 +02:00
|
|
|
--motion-duration-reveal: var(--duration-slowest); /* 800ms - one-off brand reveals */
|
|
|
|
|
--motion-delay-reveal: var(--duration-slow); /* 320ms - lets first paint settle */
|
2026-09-11 02:46:56 +02:00
|
|
|
--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-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
|
2026-09-20 11:14:15 +02:00
|
|
|
(rounded-chip / rounded-control / rounded-card / rounded-surface). They
|
|
|
|
|
alias tier 1 directly - never a tier-2 name, which would self-reference.
|
|
|
|
|
|
|
|
|
|
THE DRAFTS DRAW THREE CORNERS, NOT FIVE. Every panel, card, button, input
|
|
|
|
|
and menu in the Claude Design drafts is 4px; every tag, thumbnail, flag and
|
|
|
|
|
checkbox is 2px; filter chips and the status pill are fully round. Nothing
|
|
|
|
|
in the drafts is 8px or 12px, so `--radius-card` and `--radius-surface` come
|
|
|
|
|
down to radius/sm and the old `--radius-control-lg` (radius/md, a quirk of
|
|
|
|
|
the Figma Button frame's ghost cells) is gone. The tier-1 ramp is untouched:
|
|
|
|
|
only the meanings moved. */
|
2026-09-11 02:46:56 +02:00
|
|
|
@theme static {
|
2026-09-20 11:14:15 +02:00
|
|
|
--radius-chip: var(--radius-xs); /* 2px - tags, thumbnails, checkboxes */
|
|
|
|
|
--radius-control: var(--radius-sm); /* 4px - buttons, fields, menu items */
|
|
|
|
|
--radius-card: var(--radius-sm); /* 4px */
|
|
|
|
|
--radius-surface: var(--radius-sm); /* 4px */
|
2026-09-11 02:46:56 +02:00
|
|
|
--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"] *));
|