feat(frontend): provisional design system tokens + Logo placeholder (F0.2)
Aesthetic direction: instrumentation-grade telemetry (mission-control / SDR ops), NOT shadcn defaults, NOT generic AI/SaaS. Mature palette: graphite surface scale, CRT-amber for RT accent, steel-blue for SOC accent, sage/ochre/rust for detection status — no neon, no rainbow. Token layout designed to host the PR3 graphic charter without component churn: 1. Primitives (--mc-*) raw OKLCH scales 2. Semantics (--accent-*, --status-*, --state-*, --surface-*, --line-*, …) 3. Tailwind @theme mapping semantic tokens → utilities Includes: - src/styles/theme.css full token surface + base reset + scrollbars + grain - src/styles/fonts.css IBM Plex @font-face (self-host only) - src/styles/globals.css entry CSS file - Logo (full/compact/mark) with corner-mark composition - Panel, Pill, Button primitives reading exclusively from semantic tokens - Logo.test.tsx (3 cases, Vitest + Testing Library)
This commit is contained in:
23
frontend/src/components/brand/Logo.test.tsx
Normal file
23
frontend/src/components/brand/Logo.test.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Logo } from './Logo';
|
||||
|
||||
describe('Logo', () => {
|
||||
it('renders the MIMIC wordmark by default', () => {
|
||||
render(<Logo />);
|
||||
expect(screen.getByLabelText(/Breach & Attack Simulation/i)).toBeInTheDocument();
|
||||
expect(screen.getByText('MIMIC')).toBeInTheDocument();
|
||||
expect(screen.getByText('BAS')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides ancillary metadata in mark variant', () => {
|
||||
render(<Logo variant="mark" />);
|
||||
expect(screen.getByText('MIMIC')).toBeInTheDocument();
|
||||
expect(screen.queryByText('BAS')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders build identifier in full variant', () => {
|
||||
render(<Logo build="0.1.0" />);
|
||||
expect(screen.getByText('0.1.0')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
77
frontend/src/components/brand/Logo.tsx
Normal file
77
frontend/src/components/brand/Logo.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
/**
|
||||
* Mimic wordmark — provisional placeholder until PR3 (RT graphic charter).
|
||||
*
|
||||
* Design intent: instrument-panel nameplate, not a marketing logo.
|
||||
* Composition:
|
||||
* [ corner-cuts ] M I M I C · BAS · v{version}
|
||||
*
|
||||
* The dotted middle keeps the "instrument panel readout" feel and lets the
|
||||
* version/build identifier sit beside the wordmark without becoming a
|
||||
* secondary brand element.
|
||||
*
|
||||
* When PR3 lands, replace the inner span content with the SVG glyph; the
|
||||
* outer composition (corner marks, tracking, sibling metadata) stays.
|
||||
*/
|
||||
export type LogoVariant = 'full' | 'compact' | 'mark';
|
||||
|
||||
interface LogoProps {
|
||||
variant?: LogoVariant;
|
||||
className?: string;
|
||||
/** Build/version suffix rendered as data. Hidden in `mark`. */
|
||||
build?: string;
|
||||
}
|
||||
|
||||
export function Logo({ variant = 'full', className, build }: LogoProps) {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
'inline-flex items-center gap-3 select-none corner-mark px-2.5 py-1.5',
|
||||
className,
|
||||
)}
|
||||
aria-label="Mimic — Breach & Attack Simulation"
|
||||
>
|
||||
<span
|
||||
className="font-display font-medium text-fg-default"
|
||||
style={{
|
||||
letterSpacing: '0.32em',
|
||||
fontSize: variant === 'compact' ? '11px' : '13px',
|
||||
}}
|
||||
>
|
||||
MIMIC
|
||||
</span>
|
||||
|
||||
{variant !== 'mark' && (
|
||||
<>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="font-mono text-fg-faint"
|
||||
style={{ fontSize: '9px', letterSpacing: '0.15em' }}
|
||||
>
|
||||
··
|
||||
</span>
|
||||
<span
|
||||
className="label-system"
|
||||
style={{
|
||||
fontSize: variant === 'compact' ? '8.5px' : '9.5px',
|
||||
color: 'var(--accent-rt)',
|
||||
letterSpacing: '0.18em',
|
||||
}}
|
||||
>
|
||||
BAS
|
||||
</span>
|
||||
{build && variant === 'full' && (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="font-mono text-fg-faint tabular"
|
||||
style={{ fontSize: '9.5px' }}
|
||||
>
|
||||
{build}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
81
frontend/src/components/ui/Button.tsx
Normal file
81
frontend/src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { clsx } from 'clsx';
|
||||
import type { ButtonHTMLAttributes, CSSProperties, ReactNode } from 'react';
|
||||
|
||||
type ButtonVariant = 'primary' | 'ghost' | 'destructive' | 'subtle';
|
||||
type ButtonSize = 'sm' | 'md';
|
||||
|
||||
interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
||||
children: ReactNode;
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instrument-grade button. Flat, hairline border, no rounded pills.
|
||||
* Variants:
|
||||
* - primary amber accent fill — high-stakes actions (start run, generate report)
|
||||
* - destructive rust accent — abort, revoke, delete
|
||||
* - ghost transparent, hairline border — secondary actions
|
||||
* - subtle no border, label-system only — tertiary / inline actions
|
||||
*/
|
||||
export function Button({
|
||||
children,
|
||||
variant = 'ghost',
|
||||
size = 'md',
|
||||
className,
|
||||
...rest
|
||||
}: ButtonProps) {
|
||||
const base =
|
||||
'inline-flex items-center justify-center gap-1.5 font-sans transition-colors disabled:opacity-50 disabled:cursor-not-allowed';
|
||||
const sizeClass = size === 'sm' ? 'h-6 px-2 text-[11px]' : 'h-7 px-3 text-[12px]';
|
||||
|
||||
let style: CSSProperties = { borderRadius: 'var(--radius-sm)' };
|
||||
let extra: string;
|
||||
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
style = {
|
||||
...style,
|
||||
background: 'var(--accent-rt)',
|
||||
color: 'var(--fg-inverse)',
|
||||
border: '1px solid var(--accent-rt-strong)',
|
||||
boxShadow: 'inset 0 1px 0 0 oklch(100% 0 0 / 0.15)',
|
||||
};
|
||||
extra = 'hover:brightness-110 active:brightness-95';
|
||||
break;
|
||||
case 'destructive':
|
||||
style = {
|
||||
...style,
|
||||
background: 'transparent',
|
||||
color: 'var(--state-failed)',
|
||||
border: '1px solid var(--state-failed)',
|
||||
};
|
||||
extra = 'hover:bg-[oklch(60.5%_0.175_28_/_0.12)]';
|
||||
break;
|
||||
case 'subtle':
|
||||
style = {
|
||||
...style,
|
||||
background: 'transparent',
|
||||
color: 'var(--fg-muted)',
|
||||
border: '1px solid transparent',
|
||||
};
|
||||
extra = 'hover:text-fg-default hover:bg-surface-3';
|
||||
break;
|
||||
case 'ghost':
|
||||
default:
|
||||
style = {
|
||||
...style,
|
||||
background: 'transparent',
|
||||
color: 'var(--fg-default)',
|
||||
border: '1px solid var(--line-strong)',
|
||||
};
|
||||
extra = 'hover:bg-surface-3';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="button" className={clsx(base, sizeClass, extra, className)} style={style} {...rest}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
58
frontend/src/components/ui/Panel.tsx
Normal file
58
frontend/src/components/ui/Panel.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { clsx } from 'clsx';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface PanelProps {
|
||||
children: ReactNode;
|
||||
title?: ReactNode;
|
||||
meta?: ReactNode;
|
||||
className?: string;
|
||||
variant?: 'default' | 'inset';
|
||||
/** Show the four instrument-panel corner marks. */
|
||||
cornered?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base structural unit. Hairline border + optional title strip.
|
||||
* Variants:
|
||||
* - default — raised surface (--surface-2)
|
||||
* - inset — recessed (--surface-inset), used for code/log readouts
|
||||
*/
|
||||
export function Panel({
|
||||
children,
|
||||
title,
|
||||
meta,
|
||||
className,
|
||||
variant = 'default',
|
||||
cornered,
|
||||
}: PanelProps) {
|
||||
return (
|
||||
<section
|
||||
className={clsx(
|
||||
'relative',
|
||||
cornered && 'corner-mark',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: variant === 'inset' ? 'var(--surface-inset)' : 'var(--surface-2)',
|
||||
border: '1px solid var(--line-default)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
boxShadow: variant === 'default' ? 'var(--shadow-panel)' : 'none',
|
||||
}}
|
||||
>
|
||||
{(title || meta) && (
|
||||
<header
|
||||
className="flex items-center justify-between gap-3 px-3 py-2 border-b"
|
||||
style={{ borderColor: 'var(--line-default)' }}
|
||||
>
|
||||
{title && (
|
||||
<h2 className="label-system" style={{ color: 'var(--fg-default)' }}>
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{meta && <div className="label-system">{meta}</div>}
|
||||
</header>
|
||||
)}
|
||||
<div>{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
58
frontend/src/components/ui/Pill.tsx
Normal file
58
frontend/src/components/ui/Pill.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { clsx } from 'clsx';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
type PillTone =
|
||||
| 'neutral'
|
||||
| 'rt'
|
||||
| 'soc'
|
||||
| 'detected'
|
||||
| 'partial'
|
||||
| 'missed'
|
||||
| 'pending'
|
||||
| 'running'
|
||||
| 'success'
|
||||
| 'failed'
|
||||
| 'paused'
|
||||
| 'aborted';
|
||||
|
||||
interface PillProps {
|
||||
children: ReactNode;
|
||||
tone?: PillTone;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const TONE: Record<PillTone, { bg: string; fg: string; border: string }> = {
|
||||
neutral: {
|
||||
bg: 'transparent',
|
||||
fg: 'var(--fg-muted)',
|
||||
border: 'var(--line-strong)',
|
||||
},
|
||||
rt: { bg: 'transparent', fg: 'var(--accent-rt)', border: 'var(--accent-rt-muted)' },
|
||||
soc: { bg: 'transparent', fg: 'var(--accent-soc)', border: 'var(--accent-soc-muted)' },
|
||||
detected: { bg: 'transparent', fg: 'var(--status-detected)', border: 'var(--status-detected)' },
|
||||
partial: { bg: 'transparent', fg: 'var(--status-partial)', border: 'var(--status-partial)' },
|
||||
missed: { bg: 'transparent', fg: 'var(--status-missed)', border: 'var(--status-missed)' },
|
||||
pending: { bg: 'transparent', fg: 'var(--state-pending)', border: 'var(--line-strong)' },
|
||||
running: { bg: 'transparent', fg: 'var(--state-running)', border: 'var(--state-running)' },
|
||||
success: { bg: 'transparent', fg: 'var(--state-success)', border: 'var(--state-success)' },
|
||||
failed: { bg: 'transparent', fg: 'var(--state-failed)', border: 'var(--state-failed)' },
|
||||
paused: { bg: 'transparent', fg: 'var(--state-paused)', border: 'var(--state-paused)' },
|
||||
aborted: { bg: 'transparent', fg: 'var(--state-aborted)', border: 'var(--state-aborted)' },
|
||||
};
|
||||
|
||||
export function Pill({ children, tone = 'neutral', className }: PillProps) {
|
||||
const t = TONE[tone];
|
||||
return (
|
||||
<span
|
||||
className={clsx('inline-flex items-center gap-1.5 label-system px-1.5 py-0.5', className)}
|
||||
style={{
|
||||
color: t.fg,
|
||||
border: `1px solid ${t.border}`,
|
||||
background: t.bg,
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
90
frontend/src/styles/fonts.css
Normal file
90
frontend/src/styles/fonts.css
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* IBM Plex family — self-hosted strategy.
|
||||
*
|
||||
* OPSEC stance: Mimic ships self-contained. No runtime dependency on a
|
||||
* third-party font CDN (Google Fonts et al.). The @font-face declarations
|
||||
* below point at /fonts/, which is served from public/fonts/.
|
||||
*
|
||||
* Sprint 0 status: the .woff2 files are NOT yet vendored — chore/vendor-fonts
|
||||
* follow-up will drop the OFL-licensed binaries into public/fonts/. Until
|
||||
* then, the `font-display: swap` directive renders the system fallback
|
||||
* (ui-sans-serif / ui-monospace) so the UI remains usable.
|
||||
*
|
||||
* Source: https://github.com/IBM/plex (OFL-1.1). Files needed:
|
||||
* public/fonts/IBMPlexSans-{400,500,600,700}.woff2
|
||||
* public/fonts/IBMPlexSansCondensed-{400,500,600,700}.woff2
|
||||
* public/fonts/IBMPlexMono-{400,500,600}.woff2
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/fonts/IBMPlexSans-400.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/fonts/IBMPlexSans-500.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('/fonts/IBMPlexSans-600.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/fonts/IBMPlexSans-700.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Sans Condensed';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/fonts/IBMPlexSansCondensed-500.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Sans Condensed';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('/fonts/IBMPlexSansCondensed-600.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Sans Condensed';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/fonts/IBMPlexSansCondensed-700.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/fonts/IBMPlexMono-400.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/fonts/IBMPlexMono-500.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('/fonts/IBMPlexMono-600.woff2') format('woff2');
|
||||
}
|
||||
2
frontend/src/styles/globals.css
Normal file
2
frontend/src/styles/globals.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import './fonts.css';
|
||||
@import './theme.css';
|
||||
339
frontend/src/styles/theme.css
Normal file
339
frontend/src/styles/theme.css
Normal file
@@ -0,0 +1,339 @@
|
||||
/*
|
||||
* Mimic — provisional design system tokens
|
||||
* --------------------------------------------------------------------------
|
||||
* Aesthetic direction: instrumentation-grade telemetry.
|
||||
* Reference points: ground-station SDR panels, oscilloscope HUDs, aviation
|
||||
* MFDs, mission-control consoles. NOT cyberpunk, NOT shadcn, NOT marketing.
|
||||
*
|
||||
* Token layout
|
||||
* 1. Primitives (--mc-*) raw values, never used directly in components
|
||||
* 2. Semantics (--color-*, --text-*, --surface-*, --status-*, --signal-*)
|
||||
* the API surface for the app — what components consume
|
||||
* 3. Tailwind 4 @theme maps semantics into utility classes (bg-*, text-*…)
|
||||
*
|
||||
* Hosting the PR3 graphic charter
|
||||
* When the RT charter lands, only the primitive layer (1) is touched.
|
||||
* Components reference semantics (2). The Tailwind layer (3) is mechanical.
|
||||
* Charter swap = ~30 lines of primitives, zero component churn.
|
||||
*/
|
||||
|
||||
@import 'tailwindcss';
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* 1. Primitives — raw scales (never reference directly in TSX/HTML)
|
||||
* ------------------------------------------------------------------ */
|
||||
:root {
|
||||
/* Graphite scale — surface foundation, cool-neutral dark */
|
||||
--mc-graphite-50: oklch(98.4% 0.002 247);
|
||||
--mc-graphite-100: oklch(94.2% 0.003 247);
|
||||
--mc-graphite-200: oklch(85.6% 0.004 247);
|
||||
--mc-graphite-300: oklch(72.8% 0.005 247);
|
||||
--mc-graphite-400: oklch(58.2% 0.006 247);
|
||||
--mc-graphite-500: oklch(44.5% 0.007 247);
|
||||
--mc-graphite-600: oklch(32.1% 0.008 247);
|
||||
--mc-graphite-700: oklch(22.8% 0.009 247);
|
||||
--mc-graphite-800: oklch(16.4% 0.009 247);
|
||||
--mc-graphite-850: oklch(13.2% 0.010 247);
|
||||
--mc-graphite-900: oklch(10.2% 0.011 247);
|
||||
--mc-graphite-950: oklch(7.4% 0.012 247);
|
||||
|
||||
/* Amber signal — RT operator accent (CRT-amber lineage, not neon) */
|
||||
--mc-amber-200: oklch(94.0% 0.060 78);
|
||||
--mc-amber-300: oklch(88.5% 0.110 76);
|
||||
--mc-amber-400: oklch(82.2% 0.150 73);
|
||||
--mc-amber-500: oklch(74.0% 0.165 68);
|
||||
--mc-amber-600: oklch(63.5% 0.155 60);
|
||||
--mc-amber-700: oklch(51.0% 0.130 52);
|
||||
|
||||
/* Steel blue — SOC analyst accent, stable telemetry */
|
||||
--mc-steel-300: oklch(82.8% 0.055 235);
|
||||
--mc-steel-400: oklch(72.5% 0.075 232);
|
||||
--mc-steel-500: oklch(62.0% 0.090 228);
|
||||
--mc-steel-600: oklch(51.0% 0.085 226);
|
||||
--mc-steel-700: oklch(40.5% 0.075 224);
|
||||
|
||||
/* Signal: detected (good) — sage green, not jungle, not lime */
|
||||
--mc-sage-400: oklch(76.5% 0.105 152);
|
||||
--mc-sage-500: oklch(66.0% 0.115 148);
|
||||
--mc-sage-600: oklch(54.5% 0.105 146);
|
||||
|
||||
/* Signal: missed (alert) — rust red, not stop-light red */
|
||||
--mc-rust-400: oklch(70.5% 0.155 32);
|
||||
--mc-rust-500: oklch(60.5% 0.175 28);
|
||||
--mc-rust-600: oklch(50.0% 0.165 26);
|
||||
|
||||
/* Signal: partial (caution) — desaturated ochre between amber and rust */
|
||||
--mc-ochre-500: oklch(70.0% 0.130 55);
|
||||
|
||||
/* Type — IBM Plex family. Industrial provenance, mature, OFL-licensed. */
|
||||
--mc-font-ui:
|
||||
'IBM Plex Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||
--mc-font-mono:
|
||||
'IBM Plex Mono', ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace;
|
||||
--mc-font-display:
|
||||
'IBM Plex Sans Condensed', 'IBM Plex Sans', ui-sans-serif, sans-serif;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* 2. Semantics — the public token API
|
||||
* ------------------------------------------------------------------ */
|
||||
:root,
|
||||
.dark {
|
||||
/* Surface scale (z-axis: 0 = deepest, increases toward viewer) */
|
||||
--surface-0: var(--mc-graphite-950); /* page background, void */
|
||||
--surface-1: var(--mc-graphite-900); /* sidebar, primary chrome */
|
||||
--surface-2: var(--mc-graphite-850); /* panels, cards */
|
||||
--surface-3: var(--mc-graphite-800); /* raised: modal, dropdown */
|
||||
--surface-4: var(--mc-graphite-700); /* hover lift, popovers */
|
||||
--surface-inset: oklch(5.8% 0.012 247); /* inputs, code blocks (deeper than 0) */
|
||||
|
||||
/* Foreground (text on surfaces) */
|
||||
--fg-default: var(--mc-graphite-100);
|
||||
--fg-muted: var(--mc-graphite-300);
|
||||
--fg-subtle: var(--mc-graphite-400);
|
||||
--fg-faint: var(--mc-graphite-500);
|
||||
--fg-inverse: var(--mc-graphite-950);
|
||||
|
||||
/* Hairlines — translucent for the instrument-panel layering effect */
|
||||
--line-default: oklch(100% 0 0 / 0.06);
|
||||
--line-strong: oklch(100% 0 0 / 0.12);
|
||||
--line-faint: oklch(100% 0 0 / 0.03);
|
||||
|
||||
/* Role accents */
|
||||
--accent-rt: var(--mc-amber-500); /* RT operator / lead RT */
|
||||
--accent-rt-strong: var(--mc-amber-400);
|
||||
--accent-rt-muted: var(--mc-amber-700);
|
||||
--accent-soc: var(--mc-steel-400); /* SOC analyst */
|
||||
--accent-soc-strong: var(--mc-steel-300);
|
||||
--accent-soc-muted: var(--mc-steel-600);
|
||||
|
||||
/* Detection status (F7 cotation SOC) */
|
||||
--status-detected: var(--mc-sage-500);
|
||||
--status-detected-fg: var(--mc-graphite-950);
|
||||
--status-partial: var(--mc-ochre-500);
|
||||
--status-partial-fg: var(--mc-graphite-950);
|
||||
--status-missed: var(--mc-rust-500);
|
||||
--status-missed-fg: var(--mc-graphite-50);
|
||||
|
||||
/* Run / step state (F5/F6 execution) */
|
||||
--state-pending: var(--mc-graphite-400);
|
||||
--state-running: var(--mc-amber-500);
|
||||
--state-success: var(--mc-sage-500);
|
||||
--state-failed: var(--mc-rust-500);
|
||||
--state-aborted: var(--mc-graphite-300);
|
||||
--state-paused: var(--mc-steel-400);
|
||||
|
||||
/* WebSocket link health */
|
||||
--link-up: var(--mc-sage-500);
|
||||
--link-degraded: var(--mc-ochre-500);
|
||||
--link-down: var(--mc-rust-500);
|
||||
|
||||
/* Focus ring — amber, accessible, distinct from text selection */
|
||||
--focus-ring: var(--mc-amber-400);
|
||||
--selection-bg: oklch(74.0% 0.165 68 / 0.28);
|
||||
|
||||
/* Radii — restrained, instrument-panel feel (no pill except chips) */
|
||||
--radius-xs: 1px;
|
||||
--radius-sm: 2px;
|
||||
--radius-md: 3px;
|
||||
--radius-lg: 5px;
|
||||
--radius-pill: 999px;
|
||||
|
||||
/* Elevation — flat by default; depth via hairlines and inset shadows */
|
||||
--shadow-inset: inset 0 1px 0 0 oklch(100% 0 0 / 0.04);
|
||||
--shadow-panel: 0 1px 0 0 oklch(0% 0 0 / 0.4), inset 0 1px 0 0 oklch(100% 0 0 / 0.03);
|
||||
--shadow-pop:
|
||||
0 8px 24px -8px oklch(0% 0 0 / 0.55),
|
||||
0 2px 6px -2px oklch(0% 0 0 / 0.4),
|
||||
inset 0 1px 0 0 oklch(100% 0 0 / 0.04);
|
||||
--shadow-amber-glow: 0 0 0 1px var(--mc-amber-600), 0 0 12px -2px oklch(74.0% 0.165 68 / 0.35);
|
||||
|
||||
/* Motion — fast, mechanical, no springy bounce */
|
||||
--ease-mech: cubic-bezier(0.2, 0.7, 0.2, 1);
|
||||
--ease-snap: cubic-bezier(0.4, 0, 0.1, 1);
|
||||
--duration-instant: 80ms;
|
||||
--duration-fast: 140ms;
|
||||
--duration-medium: 220ms;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* 3. Tailwind 4 @theme — map semantics into utilities
|
||||
* ------------------------------------------------------------------ */
|
||||
@theme {
|
||||
--font-sans: var(--mc-font-ui);
|
||||
--font-mono: var(--mc-font-mono);
|
||||
--font-display: var(--mc-font-display);
|
||||
|
||||
--color-surface-0: var(--surface-0);
|
||||
--color-surface-1: var(--surface-1);
|
||||
--color-surface-2: var(--surface-2);
|
||||
--color-surface-3: var(--surface-3);
|
||||
--color-surface-4: var(--surface-4);
|
||||
--color-surface-inset: var(--surface-inset);
|
||||
|
||||
--color-fg-default: var(--fg-default);
|
||||
--color-fg-muted: var(--fg-muted);
|
||||
--color-fg-subtle: var(--fg-subtle);
|
||||
--color-fg-faint: var(--fg-faint);
|
||||
--color-fg-inverse: var(--fg-inverse);
|
||||
|
||||
--color-line-default: var(--line-default);
|
||||
--color-line-strong: var(--line-strong);
|
||||
--color-line-faint: var(--line-faint);
|
||||
|
||||
--color-accent-rt: var(--accent-rt);
|
||||
--color-accent-rt-strong: var(--accent-rt-strong);
|
||||
--color-accent-rt-muted: var(--accent-rt-muted);
|
||||
--color-accent-soc: var(--accent-soc);
|
||||
--color-accent-soc-strong: var(--accent-soc-strong);
|
||||
--color-accent-soc-muted: var(--accent-soc-muted);
|
||||
|
||||
--color-status-detected: var(--status-detected);
|
||||
--color-status-partial: var(--status-partial);
|
||||
--color-status-missed: var(--status-missed);
|
||||
|
||||
--color-state-pending: var(--state-pending);
|
||||
--color-state-running: var(--state-running);
|
||||
--color-state-success: var(--state-success);
|
||||
--color-state-failed: var(--state-failed);
|
||||
--color-state-aborted: var(--state-aborted);
|
||||
--color-state-paused: var(--state-paused);
|
||||
|
||||
--color-link-up: var(--link-up);
|
||||
--color-link-degraded: var(--link-degraded);
|
||||
--color-link-down: var(--link-down);
|
||||
|
||||
--radius-xs: var(--radius-xs);
|
||||
--radius-sm: var(--radius-sm);
|
||||
--radius-md: var(--radius-md);
|
||||
--radius-lg: var(--radius-lg);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Base reset — instrument-grade defaults
|
||||
* ------------------------------------------------------------------ */
|
||||
html {
|
||||
font-family: var(--mc-font-ui);
|
||||
font-feature-settings: 'cv11', 'ss01', 'tnum';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
color-scheme: dark;
|
||||
background-color: var(--surface-0);
|
||||
color: var(--fg-default);
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
/* Tabular numerals everywhere data appears */
|
||||
.font-mono,
|
||||
.tabular {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-feature-settings: 'tnum', 'zero', 'ss01';
|
||||
}
|
||||
|
||||
/* System labels — uppercase micro-typography */
|
||||
.label-system {
|
||||
font-family: var(--mc-font-ui);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--fg-subtle);
|
||||
}
|
||||
|
||||
/* Focus ring */
|
||||
:focus-visible {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 1px var(--surface-0),
|
||||
0 0 0 3px var(--focus-ring);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--selection-bg);
|
||||
color: var(--fg-default);
|
||||
}
|
||||
|
||||
/* Custom scrollbars — hairline */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--mc-graphite-700) transparent;
|
||||
}
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: var(--mc-graphite-700);
|
||||
border-radius: var(--radius-xs);
|
||||
}
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--mc-graphite-600);
|
||||
}
|
||||
|
||||
/* Surface grain — subtle noise on raised panels to break flat darkness */
|
||||
.surface-grain {
|
||||
background-image:
|
||||
radial-gradient(oklch(100% 0 0 / 0.012) 1px, transparent 1px),
|
||||
radial-gradient(oklch(100% 0 0 / 0.008) 1px, transparent 1px);
|
||||
background-size: 3px 3px, 7px 7px;
|
||||
background-position: 0 0, 1px 2px;
|
||||
}
|
||||
|
||||
/* Hairline corner mark — used at panel corners to evoke instrument cutmarks */
|
||||
.corner-mark {
|
||||
position: relative;
|
||||
}
|
||||
.corner-mark::before,
|
||||
.corner-mark::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border: 1px solid var(--line-strong);
|
||||
}
|
||||
.corner-mark::before {
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
.corner-mark::after {
|
||||
bottom: -1px;
|
||||
right: -1px;
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
/* Status dot — used in lists, timelines, link-state indicators */
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: currentColor;
|
||||
box-shadow: 0 0 0 1px oklch(100% 0 0 / 0.08);
|
||||
}
|
||||
.status-dot.pulsing {
|
||||
animation: status-pulse 1.6s var(--ease-mech) infinite;
|
||||
}
|
||||
@keyframes status-pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.55; transform: scale(0.85); }
|
||||
}
|
||||
|
||||
/* Mono inline (code in narration, command snippets) */
|
||||
code {
|
||||
font-family: var(--mc-font-mono);
|
||||
font-size: 0.92em;
|
||||
padding: 0.05em 0.35em;
|
||||
background-color: var(--surface-inset);
|
||||
border: 1px solid var(--line-default);
|
||||
border-radius: var(--radius-xs);
|
||||
color: var(--fg-default);
|
||||
}
|
||||
Reference in New Issue
Block a user