Files
sharper.ly/apps/web/app/styles/semantic.css
T

462 lines
24 KiB
CSS
Raw Normal View History

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.
========================================================================= */
/* ---------------------------------------------------------------------------
DARK SURFACE - the default
------------------------------------------------------------------------ */
:root,
[data-surface="dark"] {
color-scheme: dark;
/* --- surfaces / backgrounds ------------------------------------------ */
--surface-page: var(--color-neutral-800); /* figma background/primary */
/* 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 */
--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);
/* 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) -------------------------------------
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 --------------------------------------------------------- */
/* 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-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);
--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-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);
/* --- 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);
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-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 -------------------------------------------------------------
--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);
/* 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);
/* --- 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-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-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"] *));