Files
cyclone/src/index.css
T
Tyler 9bca4b608a feat(release): v0.2.0 — batch 837 export, ClaimCard, theme tokens
Backend:

- New POST /api/batches/{id}/export-837: regenerate X12 837 files
  for a list of claim_ids into a ZIP using HCPF file naming standards,
  with a unique interchange/group control number per export. Wire
  the clearhouse Loop 1000A (NM1*41 + PER) and per-payer receiver
  (NM1*40) blocks so the serializer no longer falls back to
  CYCLONE / RECEIVER placeholders.
- /api/parse-837 and /api/parse-835 now surface the server-side
  batch_id in both JSON and NDJSON response shapes so the frontend
  can hit batch-scoped endpoints without an extra listBatches
  round-trip.
- Filename helpers and the 837 serializer updated to match the new
  HCPF envelope; tests cover batch export, parse batch_id, and the
  serializer's control-number uniqueness guarantee.

Frontend:
- New shared components: ClaimCard, ClaimCard837, DominantKpiCard,
  EditorialNote, ExportBar, TickerTape, and a charts/ set
  (BarChart, HBarChart, SegmentedBar, AgingBars).
- New useBatchExport hook driving ExportBar's download flow against
  the new endpoint.
- ClaimDrawer, Lane, and Layout migrated from raw CSS-variable
  colors to Tailwind theme tokens (bg-card, text-foreground,
  border/60, etc.) for consistency with the rest of the instrument
  chrome; the active tab indicator gains a subtle accent glow.
- Upload, Inbox, Batches, BatchDiff, Reconciliation, and Acks pages
  reworked to compose the new shared components and consume the new
  batch-scoped API surface (notably ExportBar wired into Batches).

Tooling / Docs:
- Add audit-uiux.mjs and a docs/goodclaim.x12 sample fixture.
- Update ClaimDrawer testids and add coverage for the new
  components and the useBatchExport hook.

Rolls up into the v0.2.0 release tag.
2026-06-22 11:01:58 -06:00

462 lines
15 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
Cyclone — design system
---------------------------------------------------------------------
A precision instrument for one operator on one machine. The visual
language borrows from Bloomberg terminals and Linear-grade
engineering tools: dark, hairline-strict, mono-heavy, with one
editorial serif accent for the moments that deserve weight.
---------------------------------------------------------------------
Type stack:
- Geist Sans · UI, body, controls
- Geist Mono · every number, every id, every code-like datum
- Instrument Serif · editorial display (hero numbers, headlines)
===================================================================== */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* ---- Core surfaces ----
A graphite canvas with a whisper of blue. Slightly warm at the
midtones (--card) to keep the chrome from feeling sterile. */
--background: 222 18% 5%; /* #0b0c10 — near-black, cool */
--foreground: 220 14% 96%; /* #f4f5f7 — paper white */
--card: 222 16% 8%; /* #11131a */
--card-foreground: 220 14% 96%;
--popover: 222 18% 9%; /* #13151c */
--popover-foreground: 220 14% 96%;
--muted: 222 12% 13%; /* #1f2229 */
--muted-foreground: 220 8% 64%; /* #9b9ea6 */
--secondary: 222 12% 13%;
--secondary-foreground: 220 14% 96%;
/* ---- Accents ----
Electric blue (primary) for actions and emphasis. Amber (signal)
is reserved for "needs attention" states and the Inbox terminal
accents — never decorative. */
--accent: 212 100% 60%; /* #2e8aff — electric, modern */
--accent-foreground: 0 0% 100%;
--signal: 36 92% 56%; /* #f3a531 — warm amber */
--primary: 212 100% 60%;
--primary-foreground: 0 0% 100%;
--destructive: 358 82% 60%; /* #ec4458 */
--destructive-foreground: 0 0% 100%;
--success: 152 64% 46%; /* #2cbd76 */
--success-foreground: 152 70% 8%;
--warning: 36 92% 56%; /* #f3a531 */
--warning-foreground: 26 80% 12%;
--border: 222 10% 16%; /* #262932 */
--input: 222 10% 16%;
--ring: 212 100% 60%;
/* ---- Sidebar ---- */
--sidebar: 222 20% 4%; /* darker than the page */
--sidebar-foreground: 220 14% 96%;
--sidebar-accent: 212 100% 60%;
--sidebar-border: 222 12% 12%;
/* ---- Document surface (drawer detail panels) ----
A faint warm paper. Keeps detail views distinct from the chrome
without going full light. Pairs with editorial serif headlines. */
--surface: 36 18% 96%; /* #f6f3ec */
--surface-ink: 30 12% 9%; /* #181614 */
--surface-ink-2: 30 6% 32%; /* #4f4b46 */
--surface-ink-3: 30 4% 52%; /* #85807a */
--surface-line: 30 14% 14%; /* #24221f */
--surface-line-soft: 30 14% 14% / 0.12;
/* ---- Ticker-tape terminal (Inbox) ----
Bloomberg-coded: three accent rails on near-black, with the
paper-tone ink for legibility. */
--tt-bg: 220 22% 5%; /* #0a0d12 */
--tt-bg-elev: 220 18% 9%; /* #13171f */
--tt-ink: 36 18% 88%; /* #e2ddd0 */
--tt-ink-dim: 220 6% 58%; /* #8e9098 */
--tt-amber: 36 88% 56%; /* #efa836 */
--tt-oxblood: 354 50% 40%; /* #8a2d35 */
--tt-ink-blue: 212 38% 36%; /* #2e4d77 */
--tt-muted: 220 4% 38%; /* #5b5d62 */
/* ---- Legacy aliases (--m-*)
Kept as thin shims so any older component still using the
"warm paper" surface family resolves to the same hue family.
New components should prefer --surface-* / --destructive / etc. */
--m-surface: hsl(var(--surface));
--m-ink-primary: hsl(var(--surface-ink));
--m-ink-secondary: hsl(var(--surface-ink-2));
--m-ink-tertiary: hsl(var(--surface-ink-3));
--m-border-heavy: hsl(var(--surface-line));
--m-font-mono: var(--font-mono);
--m-error: hsl(var(--destructive));
--m-error-bg: hsl(var(--destructive) / 0.08);
--m-success: hsl(var(--success));
--m-warning: hsl(var(--warning));
--m-accent: hsl(var(--accent));
--radius: 0.625rem; /* 10px — slightly tighter */
/* ---- Type stack ---- */
--font-sans: "Geist", ui-sans-serif, -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
--font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
--font-display: "Instrument Serif", "Iowan Old Style", "Apple Garamond", "Baskerville", "Times New Roman", serif;
}
* {
@apply border-border;
}
html, body, #root {
height: 100%;
}
body {
@apply bg-background text-foreground;
font-family: var(--font-sans);
font-feature-settings: "ss01", "cv11", "calt";
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
/* Subtle "lit from above" wash — gives every page a slightly
lighter zone in the upper third without lifting the base
color. A vertical gradient + a soft top-center halo, both
very low opacity. The composition is additive: a soft glow
behind the page header, fading to nothing well before the
fold. Pointer-events unaffected (it's a background). */
background-image:
radial-gradient(ellipse 90% 38% at 50% -6%, hsla(220, 28%, 26%, 0.22), transparent 62%),
linear-gradient(180deg, hsla(220, 22%, 14%, 0.30) 0%, hsla(220, 22%, 8%, 0.10) 22%, transparent 42%);
background-attachment: fixed;
}
/* A precise top-right accent + a complementary soft top-center
softbox. Two light sources, both very subtle — the result reads
as "the chrome is being lit from above" rather than "there's a
gradient in the background". The softbox adds the small lift
the cards sit in; the accent preserves the instrument character. */
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
background:
radial-gradient(ellipse 70% 38% at 50% 0%, hsla(220, 32%, 28%, 0.16), transparent 65%),
radial-gradient(55rem 35rem at 100% -15%, hsla(212, 100%, 60%, 0.09), transparent 62%);
}
/* Hairline grid — the substrate of a precision instrument.
Sits behind everything, gives the dark surfaces a sense of
scale and the work a sense of "field". Slightly more present
than before so the texture actually reads against the softer
top wash. */
body::after {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
opacity: 0.6;
background-image:
linear-gradient(to right, hsl(222 10% 18% / 0.20) 1px, transparent 1px),
linear-gradient(to bottom, hsl(222 10% 18% / 0.20) 1px, transparent 1px);
background-size: 64px 64px;
mask-image: radial-gradient(ellipse 80% 60% at 50% 30%, black 30%, transparent 80%);
-webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 30%, black 30%, transparent 80%);
}
/* Custom scrollbar — minimal, hairline, Apple-style */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: hsl(var(--border));
border-radius: 999px;
border: 2px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
background: hsl(var(--muted-foreground) / 0.4);
background-clip: content-box;
}
/* Focus ring — accent, 2px, with offset.
Components override this with Tailwind focus-visible:ring-* utilities
for a ring+offset combo that matches the rest of the design system. */
:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
border-radius: 6px;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
}
}
@layer components {
/* Tabular numerals everywhere a number is shown */
.num {
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum", "ss01";
}
/* Display face — Instrument Serif. Used for the moments that need
editorial weight: hero numbers, page titles in document surfaces,
and the brand mark. */
.display {
font-family: var(--font-display);
font-weight: 400;
letter-spacing: -0.015em;
font-feature-settings: "ss01";
}
/* Data mono — Geist Mono with tabular-nums. The voice of every
id, code, dollar amount, and timestamp in the app. */
.mono {
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
letter-spacing: -0.01em;
}
/* Soft card surface — a 1px hairline + a 1px inner highlight to
give the surface a sense of depth without a heavy shadow.
The vertical gradient (lighter at top, darker at bottom) plus
a slightly more present inner highlight makes the card read
as a subtle "lit from above" panel — each card becomes its
own small lighter area without lifting the base card color. */
.surface {
background: linear-gradient(
180deg,
hsl(222 16% 10.5%) 0%,
hsl(222 16% 8.5%) 55%,
hsl(222 16% 7.5%) 100%
);
border: 1px solid hsl(var(--border));
box-shadow:
inset 0 1px 0 0 hsl(0 0% 100% / 0.06),
0 1px 2px 0 hsl(0 0% 0% / 0.4);
}
/* Subtle elevated surface — used for cards that should lift a
little more than the default. Same "lit from above" treatment
as .surface, with a touch more lift in the gradient stops and
a more visible inner highlight. The card body becomes a
slightly more present lighter area than the surrounding
chrome. */
.surface-2 {
background: linear-gradient(
180deg,
hsl(222 16% 11%) 0%,
hsl(222 16% 9%) 55%,
hsl(222 16% 7.5%) 100%
);
border: 1px solid hsl(var(--border));
box-shadow:
inset 0 1px 0 0 hsl(0 0% 100% / 0.08),
0 1px 3px 0 hsl(0 0% 0% / 0.5);
}
/* Hairline — a 1px separator that reads as a divider, not a border */
.hairline {
border-color: hsl(var(--border) / 0.6);
}
/* Active nav indicator — a thin left-aligned accent line with a
subtle glow that matches the instrument aesthetic. */
.nav-active {
background-color: hsl(var(--muted) / 0.6);
color: hsl(var(--foreground));
}
.nav-active::before {
content: "";
position: absolute;
left: -0.75rem;
top: 0.5rem;
bottom: 0.5rem;
width: 2px;
border-radius: 2px;
background-color: hsl(var(--accent));
box-shadow: 0 0 12px hsl(var(--accent) / 0.5);
}
/* Skip link — invisible until focused. */
.skip-link {
position: fixed;
top: 0.75rem;
left: 0.75rem;
z-index: 100;
display: inline-flex;
align-items: center;
height: 2.25rem;
padding: 0 0.875rem;
border-radius: 0.625rem;
background-color: hsl(var(--card));
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
box-shadow:
inset 0 1px 0 0 hsl(0 0% 100% / 0.04),
0 8px 24px hsl(0 0% 0% / 0.45);
font-size: 12.5px;
font-weight: 600;
letter-spacing: 0.01em;
transform: translateY(-200%);
transition: transform 150ms ease-out;
}
.skip-link:focus-visible {
transform: translateY(0);
outline: none;
box-shadow:
inset 0 1px 0 0 hsl(0 0% 100% / 0.04),
0 0 0 2px hsl(var(--background)),
0 0 0 4px hsl(var(--ring)),
0 8px 24px hsl(0 0% 0% / 0.45);
}
@media (prefers-reduced-motion: reduce) {
.skip-link {
transition: none;
}
}
/* Keyboard hint chip */
.kbd {
display: inline-flex;
align-items: center;
gap: 0.15rem;
height: 1.375rem;
min-width: 1.375rem;
padding: 0 0.4rem;
border-radius: 0.375rem;
border: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.5);
color: hsl(var(--muted-foreground));
font-family: var(--font-mono);
font-size: 10.5px;
font-weight: 500;
letter-spacing: 0.04em;
line-height: 1;
}
/* Section label — the all-caps eyebrow used everywhere */
.eyebrow {
font-size: 10.5px;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
color: hsl(var(--muted-foreground));
}
/* Editorial serif headline — for hero numbers and document
surfaces. A single line height that breathes. */
.editorial {
font-family: var(--font-display);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.05;
}
/* Row hover state — slightly more refined than the default
table hover. A subtle tint + a left accent strip. */
.row-hover {
transition: background-color 150ms ease;
}
.row-hover:hover {
background-color: hsl(var(--muted) / 0.4);
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
.text-pretty {
text-wrap: pretty;
}
.ring-inset-hairline {
box-shadow: inset 0 0 0 1px hsl(var(--border) / 0.6);
}
}
/* ----------------------------------------------------------------------
Print styles — a clean paper-record rendering of detail drawers.
---------------------------------------------------------------------- */
@media print {
aside, header, [data-print="hide"], button[aria-label^="Close"] {
display: none !important;
}
:root {
--background: #ffffff;
--foreground: #000000;
--card: #ffffff;
--card-foreground: #000000;
--muted: #f5f5f5;
--muted-foreground: #444444;
--border: #cccccc;
--accent: #1a1a1a;
}
body {
background: #ffffff !important;
color: #000000 !important;
}
[role="dialog"] {
position: static !important;
inset: auto !important;
width: 100% !important;
max-width: 100% !important;
height: auto !important;
transform: none !important;
background: #ffffff !important;
box-shadow: none !important;
border: none !important;
}
[data-state="open"][data-radix-dialog-overlay],
.animate-fade-in,
.animate-shimmer,
.animate-scan {
animation: none !important;
transition: none !important;
}
[role="dialog"] * {
overflow: visible !important;
}
table { page-break-inside: avoid; }
section, h1, h2, h3 { page-break-after: avoid; }
*, *::before, *::after {
animation-duration: 0s !important;
}
}
/* Universal drill-down affordance — applied by DrillableCell. */
.drillable {
cursor: pointer;
transition: background-color 120ms ease;
}
.drillable:hover {
background-color: hsl(var(--accent) / 0.08);
}
.drillable:hover::after {
content: "";
margin-left: 6px;
color: hsl(var(--accent));
font-weight: 600;
}