From 1562478a54725360ecd43e9fcbd296154a7e7e8d Mon Sep 17 00:00:00 2001 From: ux-frontend Date: Thu, 21 May 2026 20:30:41 +0200 Subject: [PATCH] feat(frontend): provisional design system tokens + Logo placeholder (F0.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- frontend/src/components/brand/Logo.test.tsx | 23 ++ frontend/src/components/brand/Logo.tsx | 77 +++++ frontend/src/components/ui/Button.tsx | 81 +++++ frontend/src/components/ui/Panel.tsx | 58 ++++ frontend/src/components/ui/Pill.tsx | 58 ++++ frontend/src/styles/fonts.css | 90 ++++++ frontend/src/styles/globals.css | 2 + frontend/src/styles/theme.css | 339 ++++++++++++++++++++ 8 files changed, 728 insertions(+) create mode 100644 frontend/src/components/brand/Logo.test.tsx create mode 100644 frontend/src/components/brand/Logo.tsx create mode 100644 frontend/src/components/ui/Button.tsx create mode 100644 frontend/src/components/ui/Panel.tsx create mode 100644 frontend/src/components/ui/Pill.tsx create mode 100644 frontend/src/styles/fonts.css create mode 100644 frontend/src/styles/globals.css create mode 100644 frontend/src/styles/theme.css diff --git a/frontend/src/components/brand/Logo.test.tsx b/frontend/src/components/brand/Logo.test.tsx new file mode 100644 index 0000000..2bce8cc --- /dev/null +++ b/frontend/src/components/brand/Logo.test.tsx @@ -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(); + 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(); + expect(screen.getByText('MIMIC')).toBeInTheDocument(); + expect(screen.queryByText('BAS')).not.toBeInTheDocument(); + }); + + it('renders build identifier in full variant', () => { + render(); + expect(screen.getByText('0.1.0')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/brand/Logo.tsx b/frontend/src/components/brand/Logo.tsx new file mode 100644 index 0000000..d25d0c6 --- /dev/null +++ b/frontend/src/components/brand/Logo.tsx @@ -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 ( + + + MIMIC + + + {variant !== 'mark' && ( + <> + + + BAS + + {build && variant === 'full' && ( + + )} + + )} + + ); +} diff --git a/frontend/src/components/ui/Button.tsx b/frontend/src/components/ui/Button.tsx new file mode 100644 index 0000000..02e9598 --- /dev/null +++ b/frontend/src/components/ui/Button.tsx @@ -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, '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 ( + + ); +} diff --git a/frontend/src/components/ui/Panel.tsx b/frontend/src/components/ui/Panel.tsx new file mode 100644 index 0000000..aedc713 --- /dev/null +++ b/frontend/src/components/ui/Panel.tsx @@ -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 ( +
+ {(title || meta) && ( +
+ {title && ( +

+ {title} +

+ )} + {meta &&
{meta}
} +
+ )} +
{children}
+
+ ); +} diff --git a/frontend/src/components/ui/Pill.tsx b/frontend/src/components/ui/Pill.tsx new file mode 100644 index 0000000..2617c59 --- /dev/null +++ b/frontend/src/components/ui/Pill.tsx @@ -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 = { + 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 ( + + {children} + + ); +} diff --git a/frontend/src/styles/fonts.css b/frontend/src/styles/fonts.css new file mode 100644 index 0000000..abbcb5f --- /dev/null +++ b/frontend/src/styles/fonts.css @@ -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'); +} diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css new file mode 100644 index 0000000..702b7f2 --- /dev/null +++ b/frontend/src/styles/globals.css @@ -0,0 +1,2 @@ +@import './fonts.css'; +@import './theme.css'; diff --git a/frontend/src/styles/theme.css b/frontend/src/styles/theme.css new file mode 100644 index 0000000..427ed24 --- /dev/null +++ b/frontend/src/styles/theme.css @@ -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); +}