Files
Rpa_Skills/rpa-dashboard-ai/templates/reference/web/index.css
T
VasanthandClaude Opus 5.5 8951ac05a5 AI skill: SKILL.md, AI contract, scrubbed references, eval fixture (d)
- rpa-dashboard-ai/SKILL.md: trigger description and the shared question flow;
  points at the no-AI frontend.md, themes and palette instead of copying them.
- templates/contract.md: runs, review rows, endpoints and the server approve gate.
- Superbill extracts: patient names and the client name removed, every file
  carries an EXAMPLE ONLY banner. tests/test_reference_scan.py checks both skills
  for patient data, internal IPs, credentials and keys.
- tests/fixtures/d-ai-extraction and its eval entry, which runs page_check_ai
  on the built folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:13:55 +05:30

1766 lines
54 KiB
CSS

/* EXAMPLE ONLY: an extract from the Superbill Review prototype, kept as reference for porting. ECW/eClinicalWorks, Tebra, 'Example Client', SPB_* tables and patient/ticket wording are that project's examples. Don't copy them into a new project. */
@import 'tailwindcss';
/* Manual dark mode: Tailwind v4 defaults `dark:` to the OS preference, but this
app needs a user-controlled toggle independent of the OS, so `dark:` is
redefined to key off a `.dark` class on <html> instead. */
@custom-variant dark (&:where(.dark, .dark *));
/* ══ Tokens ══════════════════════════════════════════════════════════════════
One palette for the whole product, declared once here and nowhere else. Every
component reads these through the Tailwind utilities registered in the
`@theme inline` block below (`bg-surface`, `text-ink`, `border-line`, …), so
a colour decision is made in this file or not at all.
LIGHT is the default set; `.dark` below overrides only what changes. Those
are the only two appearances. An `eye` mode used to sit beside them — the
light palette under a warm sepia filter — and it is gone: it was a third
appearance to keep in sync for a warmth effect the OS and the monitor both
already offer.
The ink ramp is three steps on purpose. `--ink` is for anything being read,
`--ink-2` for text that supports it (group headers, a document name in a
toolbar), `--muted` for labels and captions that should be found rather than
read. Anything reaching for a fourth is usually reaching for the wrong one. */
:root {
--ground: #f4f3f7;
--rail: #ffffff;
--surface: #ffffff;
--surface-2: #f7f6fa;
--line: #e2dfe9;
--line-soft: #eceaf1;
--ink: #18151f;
--ink-2: #3d3850;
--muted: #66607a;
--shadow: 0 20px 50px -26px rgba(30, 20, 60, 0.3);
}
.dark {
--ground: #0a0912;
--rail: #100e1a;
--surface: #151220;
--surface-2: #1b1727;
--line: #2a2438;
--line-soft: #221d31;
--ink: #f3f1f8;
--ink-2: #c9c4d8;
--muted: #8d87a0;
--shadow: 0 24px 60px -28px rgba(0, 0, 0, 0.75);
}
/* The brand ramp — identical in both appearances, and the same two hues the
sign-in screen uses, so signing in is a continuation rather than a change of
product. This is the ONLY saturated thing on a page: the header bar, primary
buttons, the active nav row, the super-admin badge, the progress fill.
Nothing else gets the gradient.
`--brand-*` are the sign-in screen's own names for the same two hues, kept
because `.sb-login` and the agents layer reference them and that screen is
deliberately untouched. --accent-1/--accent-2 are the names the rest of the
product uses. Same colours, two vocabularies, no second palette. */
:root {
--accent-1: #ff3049;
--accent-2: #f26b21;
--brand-red-hot: var(--accent-1);
--brand-orange: var(--accent-2);
/* The header bar and the primary button. 100deg for the wide header band so
the two stops are read left-to-right across it; 115deg on buttons so a
small pill still shows both hues rather than reading as one flat colour. */
--brand-bar: linear-gradient(100deg, var(--accent-1), var(--accent-2));
--brand-grad: linear-gradient(115deg, var(--accent-1), var(--accent-2));
/* The one accent, for focus rings and confirm affordances. Repointed from the
old #c81e3a to the ramp's own hot end so a focus ring matches the button it
lands on. */
--accent: var(--accent-1);
--accent-fg: #ffffff;
}
/* ── Grading colours ────────────────────────────────────────────────────────
Semantic, never decorative. These say how legibly a value was PRINTED, not
whether it is correct — a cleanly printed misread still scores clean.
They appear on status pills, the 3px grade border down a field input, legend
swatches, donut segments and quality meters. Never on a button, never on a
heading: the moment green means "primary action" as well as "clean print",
neither reading survives. */
:root {
--clean: #51a68f;
--verify: #e39b34;
--review: #de5243;
/* "Not on the page" — the fourth donut segment and the blank band. Grey on
purpose: nothing was read, so there is no clarity to grade. */
--absent: #6b6580;
/* The logo's shield body. Fixed in both appearances — it is artwork. */
--shield: #21395a;
}
/* Register the tokens as Tailwind colours so components use `bg-surface`,
`text-muted`, `border-line` and never a hex.
`@theme inline` rather than plain `@theme`: inline emits the utilities as
`var(--surface)` at the use site instead of snapshotting the value into a
generated variable, which is what lets `.dark` reassign the token and repaint
every utility built on it. Plain `@theme` would freeze the light value. */
@theme inline {
--color-ground: var(--ground);
--color-rail: var(--rail);
--color-surface: var(--surface);
--color-surface-2: var(--surface-2);
--color-line: var(--line);
--color-line-soft: var(--line-soft);
--color-ink: var(--ink);
--color-ink-2: var(--ink-2);
--color-muted: var(--muted);
--color-accent-1: var(--accent-1);
--color-accent-2: var(--accent-2);
--color-clean: var(--clean);
--color-verify: var(--verify);
--color-review: var(--review);
--color-absent: var(--absent);
--color-shield: var(--shield);
--radius-card: 14px;
--font-sora: 'Sora', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
--font-plex: 'IBM Plex Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
}
body {
background: var(--ground);
color: var(--ink);
/* IBM Plex Sans for body, Sora for display — the sign-in screen's pairing,
now the product's. The interface previously ran on the system stack as a
density choice for 11-13px lists; the two screens reading as different
products was the larger cost. Both carry the full system fallback, so a
blocked CDN costs voice and nothing else. */
font-family: var(--font-plex);
/* Never transition the body. The reduced-motion block at the end of this file
sets `transition-property: all` on every element to collapse durations,
which would make the body's own background transitionable — and switching
appearance is a state change, not an animation. */
transition: none !important;
}
/* Money, counts, timestamps and confidence figures line up in a column. */
.tnum,
.sb-num {
font-variant-numeric: tabular-nums;
}
/* Display type. One class rather than a utility per call site, because every
use is the same decision: this is a heading, a figure, or a control label. */
.sb-display {
font-family: var(--font-sora);
}
/* ══ Signed-in shell and components ══════════════════════════════════════════
Ported from the approved `inner-pages.html` mockup. Values are the mockup's,
not re-derived, so the two stay comparable.
One card radius (14px), one border colour (--line), and hierarchy from
spacing rather than shadows: a page of cards at three elevations reads as a
pile, and this product's screens are lists of peers. */
/* ── header ─────────────────────────────────────────────────────────────── */
.sb-appbar {
display: flex;
align-items: center;
gap: 14px;
padding: 12px 18px;
background: var(--brand-bar);
color: #fff;
}
.sb-appbar-mark {
width: 34px;
height: 34px;
flex: none;
filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.35));
}
.sb-appbar-mark svg {
display: block;
width: 34px;
height: 34px;
}
.sb-appbar-title {
font-family: var(--font-sora);
font-weight: 700;
font-size: 17px;
letter-spacing: 0.005em;
line-height: 1.15;
}
.sb-appbar-sub {
font-size: 11.5px;
opacity: 0.86;
margin-top: 2px;
}
.sb-who {
text-align: right;
line-height: 1.2;
}
.sb-who b {
font-family: var(--font-sora);
font-size: 13px;
display: block;
}
.sb-who span {
font-size: 10px;
letter-spacing: 0.08em;
opacity: 0.85;
}
/* A button ON the gradient. It cannot use --surface tokens: the bar is the one
saturated surface in the product and a token-coloured control on it would
read as a hole punched through the band. */
.sb-ghost-btn {
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.3);
color: #fff;
border-radius: 8px;
padding: 6px 12px;
font: 600 12.5px var(--font-plex);
cursor: pointer;
}
.sb-ghost-btn:hover {
background: rgba(255, 255, 255, 0.26);
}
.sb-themes {
display: flex;
gap: 2px;
background: rgba(0, 0, 0, 0.22);
border-radius: 999px;
padding: 3px;
}
.sb-themes button {
border: none;
background: transparent;
color: rgba(255, 255, 255, 0.85);
font: 600 11.5px var(--font-plex);
padding: 5px 11px;
border-radius: 999px;
cursor: pointer;
}
.sb-themes button[aria-pressed='true'] {
background: rgba(0, 0, 0, 0.45);
color: #fff;
}
/* ── sidebar ────────────────────────────────────────────────────────────── */
.sb-rail {
background: var(--rail);
border-right: 1px solid var(--line);
padding: 10px 0;
}
.sb-rail-group {
display: flex;
align-items: center;
gap: 9px;
padding: 12px 16px 8px;
color: var(--ink-2);
font-family: var(--font-sora);
font-weight: 600;
font-size: 11.5px;
line-height: 1.25;
}
.sb-rail-group svg {
width: 15px;
height: 15px;
flex: none;
color: var(--accent-2);
}
.sb-rail-divider {
border: none;
border-top: 1px solid var(--line-soft);
margin: 8px 14px;
}
.sb-nav-item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
text-align: left;
padding: 9px 16px;
border: none;
background: none;
cursor: pointer;
color: var(--muted);
font: 500 13px var(--font-plex);
/* Transparent rather than absent, so the row does not shift 3px when it
becomes current. */
border-left: 3px solid transparent;
}
.sb-nav-item svg {
width: 16px;
height: 16px;
flex: none;
}
.sb-nav-item:hover {
color: var(--ink-2);
background: var(--surface-2);
}
.sb-nav-item[aria-current='page'] {
color: var(--ink);
font-weight: 600;
border-left-color: var(--accent-1);
background: linear-gradient(90deg, color-mix(in srgb, var(--accent-1) 12%, transparent), transparent);
}
.sb-count {
margin-left: auto;
min-width: 20px;
text-align: center;
font: 600 10.5px var(--font-plex);
font-variant-numeric: tabular-nums;
padding: 2px 6px;
border-radius: 999px;
background: var(--surface-2);
color: var(--muted);
border: 1px solid var(--line);
}
/* Red only when the number is something a person has to act on. A count that
is red at rest teaches people to ignore red. */
.sb-count.sb-count-hot {
background: var(--accent-1);
color: #fff;
border-color: transparent;
}
/* ── page furniture ─────────────────────────────────────────────────────── */
.sb-page-title {
font-family: var(--font-sora);
font-size: 22px;
font-weight: 700;
margin: 0 0 4px;
letter-spacing: -0.01em;
color: var(--ink);
}
.sb-page-sub {
margin: 0 0 18px;
color: var(--muted);
font-size: 13px;
}
.sb-eyebrow {
font: 600 10.5px var(--font-plex);
letter-spacing: 0.12em;
color: var(--muted);
text-transform: uppercase;
margin: 0 0 10px;
}
/* ── run strip ──────────────────────────────────────────────────────────── */
.sb-runbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 18px;
padding: 10px 2px 16px;
border-bottom: 1px solid var(--line-soft);
margin-bottom: 18px;
font-size: 12.5px;
color: var(--muted);
}
.sb-runbar b {
font-family: var(--font-sora);
font-size: 17px;
color: var(--ink);
margin-right: 4px;
font-variant-numeric: tabular-nums;
}
.sb-runbar b.sb-red {
color: var(--accent-1);
}
.sb-runbar b.sb-amber {
color: var(--verify);
}
.sb-keys {
display: flex;
flex-wrap: wrap;
gap: 14px;
margin-left: auto;
align-items: center;
font-size: 11.5px;
}
.sb-swatch {
width: 16px;
height: 9px;
border-radius: 3px;
display: inline-block;
margin-right: 5px;
vertical-align: middle;
}
/* ── stat tiles ─────────────────────────────────────────────────────────── */
.sb-tiles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 12px;
margin-bottom: 18px;
}
.sb-tile {
display: flex;
align-items: center;
gap: 14px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-card, 14px);
padding: 16px;
}
/* The icon chip carries 18% of its own semantic colour — enough to identify
the tile at a glance, not enough to compete with the figure beside it. */
.sb-chip {
width: 38px;
height: 38px;
border-radius: 10px;
display: grid;
place-items: center;
flex: none;
}
.sb-chip svg {
width: 18px;
height: 18px;
}
.sb-tile-num {
font-family: var(--font-sora);
font-weight: 700;
font-size: 21px;
line-height: 1.1;
font-variant-numeric: tabular-nums;
color: var(--ink);
}
.sb-tile-num.sb-spend {
color: var(--accent-1);
font-size: 24px;
}
.sb-tile-cap {
font-size: 11.5px;
color: var(--muted);
margin-top: 2px;
}
/* ── panels and cards ───────────────────────────────────────────────────── */
.sb-panel,
.sb-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-card, 14px);
}
.sb-panel {
padding: 18px;
}
.sb-panel h2,
.sb-panel-title {
font-family: var(--font-sora);
font-size: 14px;
margin: 0 0 14px;
font-weight: 600;
color: var(--ink);
}
.sb-card {
padding: 14px 16px;
}
.sb-card-top {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
font-size: 12.5px;
color: var(--muted);
}
.sb-batch-id {
font-family: var(--font-sora);
font-weight: 700;
color: var(--accent-1);
letter-spacing: 0.02em;
}
.sb-stamp {
margin-left: auto;
font-variant-numeric: tabular-nums;
}
.sb-doc-name {
font-weight: 600;
font-size: 13.5px;
color: var(--ink);
}
.sb-doc-state {
font-size: 12px;
color: var(--muted);
}
.sb-hint {
font-size: 11.5px;
color: var(--muted);
margin: 9px 0 0;
}
/* The quality meter: the document's own grade split, as one bar. Its stops are
set inline per document because they are data, not style. */
.sb-quality {
flex: 1;
min-width: 140px;
height: 7px;
border-radius: 999px;
background: var(--surface-2);
}
.sb-meter {
height: 7px;
border-radius: 999px;
background: var(--surface-2);
overflow: hidden;
}
/* ── pills ──────────────────────────────────────────────────────────────── */
.sb-pill {
font: 600 11px var(--font-plex);
padding: 3px 9px;
border-radius: 999px;
display: inline-block;
}
.sb-pill-ok {
background: color-mix(in srgb, var(--clean) 22%, transparent);
color: var(--clean);
}
.sb-pill-warn {
background: color-mix(in srgb, var(--verify) 20%, transparent);
color: var(--verify);
}
.sb-pill-bad {
background: color-mix(in srgb, var(--review) 20%, transparent);
color: var(--review);
}
.sb-pill-mute {
background: var(--surface-2);
color: var(--muted);
border: 1px solid var(--line);
}
.sb-badge-super {
display: inline-block;
padding: 5px 12px;
border-radius: 999px;
font: 600 11.5px var(--font-plex);
color: #fff;
background: var(--brand-grad);
}
/* ── buttons ────────────────────────────────────────────────────────────── */
.sb-btn {
border: none;
border-radius: 9px;
cursor: pointer;
font: 600 13px var(--font-sora);
padding: 9px 16px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: filter 0.15s ease, background-color 0.15s ease, opacity 0.15s ease;
}
.sb-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.sb-btn-primary {
background: var(--brand-grad);
color: #fff;
}
.sb-btn-primary:hover:not(:disabled) {
filter: brightness(1.07);
}
.sb-btn-quiet {
background: var(--surface-2);
color: var(--ink);
border: 1px solid var(--line);
}
.sb-btn-quiet:hover:not(:disabled) {
border-color: var(--muted);
}
/* Run is deliberately between quiet and primary: it is the action the board is
for, but it costs money, so it does not get the full gradient. */
.sb-btn-run {
background: color-mix(in srgb, var(--accent-1) 30%, var(--surface-2));
color: var(--ink-2);
border: 1px solid color-mix(in srgb, var(--accent-1) 45%, transparent);
}
.sb-btn-run:hover:not(:disabled) {
background: color-mix(in srgb, var(--accent-1) 42%, var(--surface-2));
}
.sb-btn-next {
background: var(--verify);
color: #221704;
}
.sb-btn-danger {
background: none;
border: none;
color: var(--review);
font: 600 12.5px var(--font-plex);
cursor: pointer;
}
.sb-btn-sm {
font-size: 12px;
padding: 7px 12px;
}
.sb-link {
background: none;
border: none;
color: var(--ink-2);
font: 500 12.5px var(--font-plex);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
}
.sb-link:hover {
color: var(--accent-1);
}
.sb-icon-btn {
width: 26px;
height: 26px;
border-radius: 7px;
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink-2);
cursor: pointer;
font-size: 12px;
line-height: 1;
display: inline-grid;
place-items: center;
}
.sb-icon-btn:hover:not(:disabled) {
border-color: var(--muted);
}
.sb-icon-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* ── form controls ──────────────────────────────────────────────────────── */
.sb-input,
.sb-select {
background: var(--surface-2);
color: var(--ink);
border: 1px solid var(--line);
border-radius: 9px;
padding: 9px 12px;
font: 400 13px var(--font-plex);
}
.sb-select {
border-radius: 8px;
padding: 7px 10px;
font-weight: 500;
font-size: 12.5px;
}
.sb-input::placeholder {
color: var(--muted);
}
.sb-input:focus,
.sb-select:focus {
outline: none;
border-color: var(--accent-1);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-1) 20%, transparent);
}
.sb-inline-label {
font-size: 12.5px;
color: var(--muted);
}
/* ── dropzone ───────────────────────────────────────────────────────────── */
.sb-drop {
border: 1.5px dashed var(--line);
border-radius: var(--radius-card, 14px);
background: var(--surface);
padding: 26px 20px;
text-align: center;
}
.sb-drop h2,
.sb-drop-title {
font-family: var(--font-sora);
font-size: 16px;
font-weight: 700;
margin: 0 0 6px;
color: var(--ink);
}
.sb-drop p {
margin: 0 auto 14px;
color: var(--muted);
font-size: 12.5px;
max-width: 52ch;
line-height: 1.5;
}
.sb-drop-active {
border-color: var(--accent-1);
background: color-mix(in srgb, var(--accent-1) 6%, var(--surface));
}
/* ── file row ───────────────────────────────────────────────────────────── */
.sb-file-ico {
width: 30px;
height: 30px;
border-radius: 8px;
background: var(--surface-2);
border: 1px solid var(--line);
display: grid;
place-items: center;
flex: none;
}
.sb-file-ico svg {
width: 15px;
height: 15px;
color: var(--muted);
}
/* ── empty state ────────────────────────────────────────────────────────── */
.sb-empty {
border: 1px solid var(--line-soft);
border-radius: var(--radius-card, 14px);
padding: 64px 20px;
text-align: center;
color: var(--muted);
font-size: 13px;
background: var(--surface);
}
/* ── tables ─────────────────────────────────────────────────────────────── */
.sb-table-wrap {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-card, 14px);
overflow-x: auto;
}
.sb-table {
border-collapse: collapse;
width: 100%;
min-width: 620px;
}
.sb-table th {
text-align: left;
font: 600 10.5px var(--font-plex);
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
padding: 13px 16px;
border-bottom: 1px solid var(--line);
white-space: nowrap;
}
.sb-table td {
padding: 14px 16px;
border-bottom: 1px solid var(--line-soft);
font-size: 13px;
vertical-align: middle;
color: var(--ink);
}
.sb-table tr:last-child td {
border-bottom: none;
}
.sb-table td b {
display: block;
font-size: 13.5px;
}
.sb-table .sb-code {
font-size: 11.5px;
color: var(--muted);
}
/* ── donut and legend ───────────────────────────────────────────────────── */
.sb-legend {
display: grid;
gap: 9px;
min-width: 180px;
flex: 1;
}
.sb-legend > div {
display: flex;
align-items: center;
gap: 8px;
font-size: 12.5px;
color: var(--ink);
}
.sb-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex: none;
}
.sb-legend .sb-n {
margin-left: auto;
font-variant-numeric: tabular-nums;
font-weight: 600;
}
.sb-legend .sb-p {
color: var(--muted);
width: 34px;
text-align: right;
font-variant-numeric: tabular-nums;
}
/* ── review drill-down ──────────────────────────────────────────────────── */
.sb-rv-bar {
display: flex;
align-items: center;
gap: 14px;
padding: 10px 16px;
background: var(--brand-bar);
color: #fff;
}
.sb-rv-bar .sb-appbar-mark,
.sb-rv-bar .sb-appbar-mark svg {
width: 26px;
height: 26px;
}
.sb-rv-back {
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.32);
color: #fff;
border-radius: 8px;
padding: 6px 12px;
font: 600 12.5px var(--font-plex);
cursor: pointer;
white-space: nowrap;
}
.sb-rv-back:hover {
background: rgba(255, 255, 255, 0.3);
}
.sb-rv-file b {
font-family: var(--font-sora);
font-size: 14px;
display: block;
line-height: 1.2;
}
.sb-rv-file span {
font-size: 11px;
opacity: 0.85;
}
.sb-rv-keys {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
padding: 9px 16px;
border-bottom: 1px solid var(--line-soft);
font-size: 11.5px;
color: var(--muted);
background: var(--rail);
}
.sb-rv-status {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-bottom: 1px solid var(--line-soft);
background: var(--rail);
}
.sb-chip-count {
font: 600 12px var(--font-plex);
padding: 5px 11px;
border-radius: 8px;
background: var(--surface-2);
border: 1px solid var(--line);
color: var(--ink-2);
font-variant-numeric: tabular-nums;
}
.sb-chip-count.sb-clean {
color: var(--clean);
border-color: color-mix(in srgb, var(--clean) 45%, transparent);
}
.sb-chip-count.sb-sample {
color: var(--verify);
border-color: color-mix(in srgb, var(--verify) 45%, transparent);
}
.sb-chip-count.sb-review {
color: var(--review);
border-color: color-mix(in srgb, var(--review) 45%, transparent);
}
.sb-chip-done {
background: color-mix(in srgb, var(--clean) 20%, transparent);
color: var(--clean);
border-color: transparent;
}
.sb-rv-progress {
width: 190px;
height: 7px;
border-radius: 999px;
background: var(--surface-2);
overflow: hidden;
}
/* The fill is sized by `width` from the verified/total ratio. The mockup eased
that width over 250ms; this does not, deliberately — animating `width`
dirties layout on every frame, and the ease bought nothing on a bar that
steps forward once per field a reviewer clears. The value change is still
perfectly legible without it. */
.sb-rv-progress i {
display: block;
height: 100%;
background: var(--brand-grad);
}
.sb-rv-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid var(--line);
font-size: 12px;
color: var(--muted);
flex-wrap: wrap;
}
/* The field row: label / input / confidence / OK-Undo. The grade is a 3px
border down the input's left edge rather than a fill, so the value stays on
the same surface as every other value and only its margin is coloured. */
.sb-field-row {
display: grid;
grid-template-columns: 78px minmax(0, 1fr) 42px 70px;
align-items: center;
gap: 9px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 11px;
padding: 9px 11px;
}
.sb-field-row.sb-done {
border-color: color-mix(in srgb, var(--clean) 40%, transparent);
}
.sb-field-row .sb-lbl {
font: 600 12px var(--font-plex);
color: var(--muted);
}
.sb-field-row.sb-done .sb-lbl {
color: var(--ink-2);
}
.sb-field-input {
width: 100%;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 8px;
padding: 7px 9px;
color: var(--ink);
font: 500 12.5px var(--font-plex);
min-width: 0;
}
.sb-field-input:focus {
outline: none;
border-color: var(--accent-1);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-1) 20%, transparent);
}
.sb-field-input.sb-grade-clean {
border-left: 3px solid var(--clean);
}
.sb-field-input.sb-grade-sample {
border-left: 3px solid var(--verify);
}
.sb-field-input.sb-grade-missing {
border-left: 3px solid var(--review);
}
.sb-field-input.sb-grade-absent {
border-left: 3px solid var(--absent);
}
.sb-conf {
font-size: 11.5px;
color: var(--muted);
text-align: right;
font-variant-numeric: tabular-nums;
}
/* ── focus ──────────────────────────────────────────────────────────────── */
/* `:focus-visible`, never `:focus` — a mouse click should never paint a ring,
and keyboard navigation should always get one. No border-radius here on
purpose: `outline` already follows each element's own radius. */
:focus-visible {
outline: 2px solid var(--accent-1);
outline-offset: 2px;
}
/* ── Motion ─────────────────────────────────────────────────────────────────
Every animation below is transform/opacity only — never width, height, top
or left — so each one runs on the compositor and cannot re-trigger layout on
a pane that is already re-rendering a 1275x1650 page image beside it.
None of them is the ONLY way a state change is communicated, which is what
makes the prefers-reduced-motion collapse at the bottom of this file safe:
removing the motion removes polish, never information. */
/* Rows arriving when a patient opens. Staggered by index in ReviewPane so a
record resolves top-to-bottom instead of snapping in as one block. */
@keyframes sb-rise {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: none; }
}
.sb-rise {
animation: sb-rise 280ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
}
/* The unsaved-changes bar sliding up from the bottom edge of the pane. */
@keyframes sb-slide-up {
from { opacity: 0; transform: translateY(100%); }
to { opacity: 1; transform: none; }
}
.sb-slide-up {
animation: sb-slide-up 220ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
}
/* A save landing. Deliberately a scale pulse rather than a colour flash: the
band colour underneath is load-bearing and must not be repainted, even for
200ms, to signal something unrelated to legibility. */
@keyframes sb-pulse {
0% { transform: scale(1); }
40% { transform: scale(1.015); }
100% { transform: scale(1); }
}
.sb-pulse {
animation: sb-pulse 320ms ease-out both;
}
/* Indeterminate sweep for work in flight (extraction, saving). */
@keyframes sb-sweep {
from { transform: translateX(-100%); }
to { transform: translateX(300%); }
}
.sb-sweep::after {
content: '';
position: absolute;
inset: 0;
width: 33%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.55), transparent);
animation: sb-sweep 1.4s linear infinite;
}
/* ── Extraction fire ────────────────────────────────────────────────────────
A document being read is the one place in this app where something is
genuinely burning through work, and the brand palette already IS a fire —
deep red through orange into gold. So `running` renders as embers over a
breathing heat band instead of one more grey spinner, using colours the
shell already owns rather than introducing a hue for the occasion.
Animated properties are opacity and transform only, so each layer
composites and none of it re-triggers layout on a board that is polling
every two seconds. The blur is static — one paint, not one per frame.
None of it carries information. "page 4 of 9" sits beside the fire and is
the actual statement of progress, which is what keeps the reduced-motion
collapse below honest: removing the motion removes the drama, never the
fact that a document is running. */
/* The extraction beam has NO pulse. There was a `sb-beam-breathe` here cycling
the bloom's opacity every 3.4s; even shallow, a glow that throbs reclaims
attention on its own schedule, and this element sits beside a filename someone
is trying to read. The light is steady and the only motion is the sweep below.
Deleted rather than commented out — a disabled animation left in the sheet is
the one that gets re-enabled by accident. */
/* The hotspot scanning the beam. Travels a full row width plus its own width at
each end, so it enters and leaves through the tapered edges instead of
appearing and vanishing mid-beam. `alternate` rather than a wrap-around: a
scan that returns is a reader working back over the page, and it removes the
jump a looping translate would show at the seam. */
@keyframes sb-beam-sweep {
from { transform: translate3d(-100%, -50%, 0); }
to { transform: translate3d(400%, -50%, 0); }
}
.sb-beam-sweep {
animation-name: sb-beam-sweep;
animation-duration: 2600ms;
animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
animation-iteration-count: infinite;
animation-direction: alternate;
}
/* One ember. Drift, duration and delay come in per-instance so a bed of these
reads as scattered sparks rather than one pulse repeated in a row. */
@keyframes sb-ember {
0% { opacity: 0; transform: translate3d(0, 3px, 0) scale(0.35); }
18% { opacity: 0.95; }
100% { opacity: 0; transform: translate3d(var(--sb-drift, 0px), -20px, 0) scale(1.45); }
}
.sb-ember {
animation-name: sb-ember;
animation-duration: var(--sb-dur, 1800ms);
animation-delay: var(--sb-delay, 0ms);
animation-timing-function: ease-out;
animation-iteration-count: infinite;
/* A spark has no edge. Without this they read as tidy dots. */
filter: blur(0.5px);
}
/* The heat under the embers. Scales from its bottom edge so it grows upward
out of the surface it sits on, the way a flame does. */
@keyframes sb-heat {
0%, 100% { opacity: 0.45; transform: scaleY(1); }
50% { opacity: 0.95; transform: scaleY(1.6); }
}
.sb-heat {
animation-name: sb-heat;
animation-duration: 1400ms;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
transform-origin: bottom;
filter: blur(3px);
}
/* The burning head of a progress bar — brightest right where it is eating in. */
@keyframes sb-flicker {
0%, 100% { opacity: 0.7; }
35% { opacity: 1; }
62% { opacity: 0.82; }
}
.sb-flicker {
animation-name: sb-flicker;
animation-duration: 900ms;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
/* ── Browser surfaces ───────────────────────────────────────────────────────
The parts nobody draws still carry the design. A default blue selection
highlight and a default grey scrollbar belong to no design system, and inside
a red-branded app they are the tell that a page was assembled rather than
built.
Selection is slate, not brand red, deliberately. Brand colour is quarantined
to the shell (DESIGN.md, "The One Loud Color Rule") and selecting a value
inside a red confidence chip would drag brand red straight into the one
surface that must never see it. Slate is already this app's neutral, so the
highlight is themed without breaking the quarantine.
The caret inherits `currentColor` for the same reason: inside a band chip it
picks up that band's own text colour instead of stamping an accent on it. */
::selection {
background: rgba(100, 116, 139, 0.30);
}
.dark ::selection {
background: rgba(148, 163, 184, 0.34);
}
input,
textarea,
[contenteditable] {
caret-color: currentColor;
}
:root {
--sb-scroll-thumb: #cbd5e1;
--sb-scroll-thumb-hover: #94a3b8;
}
.dark {
--sb-scroll-thumb: #334155;
--sb-scroll-thumb-hover: #475569;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--sb-scroll-thumb) transparent;
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--sb-scroll-thumb);
border-radius: 9999px;
/* Inset via a transparent border so the thumb reads as a floating pill rather
than a bar welded to the edge of the pane. */
border: 2px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
background: var(--sb-scroll-thumb-hover);
background-clip: content-box;
}
::-webkit-scrollbar-corner {
background: transparent;
}
/* ── the sign-in screen ────────────────────────────────────────────────────
Every value here is carried over from the approved `login-animated.html`
mockup rather than re-derived, so the two render identically: card 400px at
36/32/32 padding, 20px radius, 14.5px fields, 12.5px uppercase labels.
Two things the earlier pass had that the mockup does not, and which are
deliberately gone: the three blurred radials and the raked brand rule. The
rule crossed the viewport straight through the middle of the card, which is
what the diagonal cutting the screen in half was.
This screen is no longer committed to a dark ground. It follows the app's
appearance setting — `.dark` from theme.tsx paints it dark, and light and
`eye` both take the light tokens below (`eye` is the light surface plus a
warm filter, applied globally, so it needs nothing of its own here). */
.sb-login {
--lg-bg: #f4f3f7;
--lg-ink: #18151f;
--lg-muted: #5f5b6c;
--lg-card-bg: #ffffff;
--lg-card-border: #e2dfe9;
--lg-field-bg: #f7f6fa;
--lg-field-border: #dcd8e6;
--lg-shadow: 0 20px 60px -20px rgba(30, 20, 60, 0.25);
/* The mockup's --accent-1/--accent-2 are already the brand's own two hues,
so these alias the product tokens instead of restating the hex. */
--lg-accent-1: var(--brand-red-hot);
--lg-accent-2: var(--brand-orange);
background: var(--lg-bg);
color: var(--lg-ink);
font-family: 'IBM Plex Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
}
.dark .sb-login {
--lg-bg: #0a0912;
--lg-ink: #f3f1f8;
--lg-muted: #9791a8;
--lg-card-bg: #151220;
--lg-card-border: #2a2438;
--lg-field-bg: #1b1727;
--lg-field-border: #332c46;
--lg-shadow: 0 30px 80px -20px rgba(0, 0, 0, 0.6);
}
/* The card's column. `pointer-events: none` so hovers fall through to the
agents behind it; the card itself puts them back on. */
.sb-login-stage {
position: relative;
z-index: 2;
pointer-events: none;
width: 100%;
max-width: 1100px;
min-height: 640px;
display: flex;
align-items: center;
justify-content: center;
padding: 40px clamp(16px, 6vw, 96px);
}
.sb-login-card {
pointer-events: auto;
width: 100%;
max-width: 400px;
background: var(--lg-card-bg);
border: 1px solid var(--lg-card-border);
border-radius: 20px;
padding: 36px 32px 32px;
box-shadow: var(--lg-shadow);
}
/* Each block fades up a beat after the one above it, so the card assembles
top-down instead of appearing all at once. `forwards` with `opacity: 0` at
rest is what holds each one hidden until its own delay comes round. */
@keyframes sb-login-rise {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.sb-login-brand,
.sb-login-title,
.sb-login-lede,
.sb-login-form {
opacity: 0;
animation: sb-login-rise 0.7s cubic-bezier(0.2, 0.7, 0.3, 1) forwards;
}
.sb-login-brand { animation-delay: 0.1s; }
.sb-login-title { animation-delay: 0.22s; }
.sb-login-lede { animation-delay: 0.3s; }
.sb-login-form { animation-delay: 0.38s; }
.sb-login-brand {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 28px;
}
/* The shield, lifting on a slow bob under a warm glow.
Two drop-shadows, not one: the tight warm pass is the light coming off the
shield's own bars, the wide red one is what that light throws onto the card
behind it. A single large blur only ever reads as a smudge.
The bob is a lift with a slight lean, not a bounce — the rotation is what
keeps it from reading as a loading indicator. */
.sb-mark-bob {
position: relative;
width: 38px;
height: 38px;
display: grid;
place-items: center;
flex: none;
filter: drop-shadow(0 0 9px rgba(227, 155, 52, 0.55))
drop-shadow(0 0 18px rgba(222, 82, 67, 0.28));
animation: sb-mark-bob 4.5s ease-in-out 0.9s infinite;
}
.sb-mark-bob > svg {
display: block;
width: 38px;
height: 38px;
}
@keyframes sb-mark-bob {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-3px) rotate(-4deg); }
}
/* The product name with a sheen travelling through it.
The gradient is 260% of the text's own width and its POSITION is what
animates, so a bright band sweeps across the glyphs while the ink either
side of it stays the card's own ink colour. Animating `background-position`
on a clipped background is what does this without a second overlay element
and a mask to keep aligned with it.
Stops 45% and 100% are the same position on purpose: the sheen passes once
and the name then sits still for the rest of the cycle. Ending on a
different stop would make it a shimmer that never settles, which is
movement in the corner of the eye while somebody is typing. */
.sb-brand-name {
font-family: 'Sora', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
font-weight: 700;
font-size: 16px;
letter-spacing: 0.01em;
background-image: linear-gradient(
100deg,
var(--lg-ink) 30%,
var(--lg-accent-1) 46%,
var(--lg-accent-2) 52%,
var(--lg-ink) 68%
);
background-size: 260% 100%;
background-position: 100% 0;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: sb-brand-sheen 5.5s ease-in-out 1.4s infinite;
}
@keyframes sb-brand-sheen {
0% { background-position: 130% 0; }
45% { background-position: -10% 0; }
100% { background-position: -10% 0; }
}
.sb-login-title {
font-family: 'Sora', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
font-size: clamp(22px, 3.2vw, 26px);
font-weight: 700;
line-height: 1.2;
letter-spacing: -0.01em;
margin: 0 0 8px;
text-wrap: balance;
}
.sb-login-title .sb-login-accent {
background-image: linear-gradient(100deg, var(--lg-accent-1), var(--lg-accent-2));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.sb-login-lede {
margin: 0 0 26px;
color: var(--lg-muted);
font-size: 14.5px;
line-height: 1.55;
max-width: 34ch;
}
.sb-login-field {
margin-bottom: 16px;
}
.sb-login-field label {
display: block;
font-size: 12.5px;
font-weight: 600;
color: var(--lg-muted);
margin: 0 0 6px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.sb-login-field input {
width: 100%;
padding: 11px 13px;
border-radius: 10px;
border: 1px solid var(--lg-field-border);
background: var(--lg-field-bg);
color: var(--lg-ink);
font-family: inherit;
font-size: 14.5px;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.sb-login-field input:focus {
outline: none;
border-color: var(--lg-accent-1);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--lg-accent-1) 22%, transparent);
}
.sb-login-field input::placeholder {
color: var(--lg-muted);
opacity: 0.7;
}
/* The refusal notice. Not in the mockup — it has no auth to fail — so it is
built from the same tokens rather than invented: accent-1 at low alpha, on
the card's own field background. */
.sb-login-error {
margin: 0 0 16px;
border-radius: 10px;
border: 1px solid color-mix(in srgb, var(--lg-accent-1) 35%, transparent);
background: color-mix(in srgb, var(--lg-accent-1) 10%, transparent);
color: color-mix(in srgb, var(--lg-accent-1) 75%, var(--lg-ink));
padding: 10px 13px;
font-size: 13px;
line-height: 1.5;
}
.sb-login-submit {
width: 100%;
margin-top: 6px;
padding: 12px;
border: none;
border-radius: 10px;
background: linear-gradient(120deg, var(--lg-accent-1), var(--lg-accent-2));
color: #ffffff;
font-family: 'Sora', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
font-weight: 700;
font-size: 14.5px;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
box-shadow: 0 10px 24px -8px color-mix(in srgb, var(--lg-accent-1) 60%, transparent);
}
.sb-login-submit:hover:not(:disabled) {
transform: translateY(-1px);
}
.sb-login-submit:disabled {
cursor: not-allowed;
opacity: 0.4;
box-shadow: none;
}
/* ── the crew ──────────────────────────────────────────────────────────────
Five line-art agents crossing the background behind the card, at five sizes
and five speeds.
`fixed` so the walk is measured against the viewport rather than the card's
own column, and `overflow: hidden` so a figure part-way past either edge is
clipped instead of making the page scrollable.
`pointer-events: none` on the layer with `auto` on each agent, and that
split is the whole reason hover works here. The layer spans the screen; left
hit-testable it would sit between the pointer and everything under it. Each
agent is only as big as itself, so it can take a hover without shadowing the
card. The page wrapper is `pointer-events-none` for the same reason — it is
a full-screen flex container and would otherwise swallow every hover before
it reached this layer. */
.sb-agents {
position: fixed;
inset: 0;
z-index: 1;
overflow: hidden;
pointer-events: none;
}
.sb-agent {
position: absolute;
left: 0;
color: var(--brand-red-hot);
/* Low enough to stay behind the reading, high enough to be seen as a figure
rather than a stain. Hover lifts it to near-full — see below. */
opacity: 0.42;
pointer-events: auto;
cursor: default;
/* Five simultaneous drifts; promoting the transform keeps them off the main
thread while somebody is typing into the form in front of them. */
will-change: transform;
transition: opacity 0.35s ease;
}
/* The light-line each one trails behind it. Explicitly not hit-testable: it is
90px wide and would otherwise triple the bot's hover target, so the figure
would stop while the pointer was nowhere near it. */
.sb-agent::before {
content: '';
position: absolute;
right: 100%;
top: 50%;
width: 90px;
height: 1px;
margin-right: 6px;
background: linear-gradient(90deg, transparent, currentColor);
opacity: 0.55;
pointer-events: none;
}
/* `content-box` padding, so the 10px grows the hit area without shrinking the
figure inside the width each `.sb-agent--n` sets. */
.sb-ai-bot {
display: block;
padding: 10px;
box-sizing: content-box;
cursor: pointer;
}
/* Line art is mostly holes, and SVG only hit-tests pixels it painted — with no
filled shape behind it the pointer falls between the strokes and the hover
strobes on and off as the cursor crosses a limb. A transparent fill still
hit-tests; `fill: none` does not, which is the distinction this rule exists
for. */
.sb-ai-hit {
fill: transparent;
stroke: none;
}
/* The dance is on the inner group, the drift on the outer span. Two elements
on purpose: one `transform` property cannot hold both animations, and
nesting is what composes them instead of letting one overwrite the other. */
.sb-ai-core {
transform-origin: 12px 27px;
animation: sb-bot-dance 2.8s ease-in-out infinite;
}
@keyframes sb-bot-dance {
0%, 100% { transform: translateY(0) rotate(-7deg); }
25% { transform: translateY(-5px) rotate(0deg); }
50% { transform: translateY(0) rotate(7deg); }
75% { transform: translateY(-5px) rotate(0deg); }
}
/* Off the left edge, across, and off the right. The 160px overshoot at each
end is wider than the largest figure, so none of them pops into existence
at the edge of the screen. */
@keyframes sb-bot-drift {
from { transform: translateX(-160px); }
to { transform: translateX(calc(100vw + 160px)); }
}
/* Five sizes, five durations, five negative delays.
No two durations match: equal speeds would send pairs across in lockstep and
give away that this is five copies of one drawing. The negative delays start
each one part-way through its own crossing, so the screen is already
populated on the first frame rather than filling up over the first minute. */
.sb-agent--1 { top: 12%; animation: sb-bot-drift 48s linear infinite; }
.sb-agent--1 .sb-ai-bot { width: 64px; height: 80px; }
.sb-agent--2 { top: 40%; animation: sb-bot-drift 62s linear -18s infinite; }
.sb-agent--2 .sb-ai-bot { width: 34px; height: 42px; }
.sb-agent--2 .sb-ai-core { animation-duration: 3.6s; animation-delay: -0.6s; }
.sb-agent--3 { top: 70%; animation: sb-bot-drift 54s linear -34s infinite; }
.sb-agent--3 .sb-ai-bot { width: 52px; height: 65px; }
.sb-agent--3 .sb-ai-core { animation-duration: 2.3s; animation-delay: -1.2s; }
.sb-agent--4 { top: 26%; animation: sb-bot-drift 70s linear -50s infinite; }
.sb-agent--4 .sb-ai-bot { width: 28px; height: 35px; }
.sb-agent--4 .sb-ai-core { animation-duration: 4.1s; animation-delay: -2s; }
.sb-agent--5 { top: 58%; animation: sb-bot-drift 44s linear -8s infinite; }
.sb-agent--5 .sb-ai-bot { width: 76px; height: 95px; }
.sb-agent--5 .sb-ai-core { animation-duration: 3.1s; animation-delay: -1.7s; }
/* Hover: that one agent halts where it stands and grins. The other four keep
walking, which is what the per-agent drift animations above are for.
These rules MUST stay below the `.sb-agent--n` blocks, and they carry
`!important`, and both of those are for the same reason: those blocks set
the `animation` SHORTHAND, which expands to include
`animation-play-state: running`. A pause declared earlier loses on source
order; a pause at equal specificity loses too. Either way the bot keeps
drifting out from under a cursor that was meant to have stopped it. */
.sb-agents .sb-agent:hover,
.sb-agents .sb-agent:hover .sb-ai-core {
animation-play-state: paused !important;
}
.sb-agents .sb-agent:hover {
opacity: 0.95;
}
/* Only one mouth shows at a time: the gentle arc while walking, the wide one
while the pointer is holding it still. Crossfaded with a transition rather
than an animation, deliberately — hovering pauses animations, and a paused
crossfade would freeze the face on whichever expression it happened to be
interpolating. `animation-play-state` does not touch transitions, so the
grin lands and stays for as long as the pointer is there. */
.sb-ai-mouth-idle,
.sb-ai-mouth-laugh {
transition: opacity 260ms ease-out;
}
.sb-ai-mouth-laugh {
opacity: 0;
}
.sb-agents .sb-agent:hover .sb-ai-mouth-idle {
opacity: 0;
}
.sb-agents .sb-agent:hover .sb-ai-mouth-laugh {
opacity: 1;
}
/* The antenna bulb's click response — separate from the hover dip/smile,
which are ambient and never triggered by the visitor directly. This is the
one thing on the row that answers a click.
`transform-box: fill-box` is required for an SVG shape: without it a CSS
`scale` transform pivots on the SVG viewport's origin (0,0), not the
circle's own centre, and the bulb would swing out on an arc instead of
growing in place. */
.sb-ai-bulb {
transform-box: fill-box;
transform-origin: center;
}
.sb-ai-bulb--pulsing {
animation: sb-bulb-flash 650ms cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes sb-bulb-flash {
0%, 100% { transform: scale(1); }
45% { transform: scale(2.2); }
}
/* The halo: a second, larger circle at the same centre, invisible at rest.
Expanding radius AND fading opacity together is what reads as light
blooming outward rather than a shape simply growing — a scale-only halo
would look like a second solid dot. */
.sb-ai-bulb-halo {
transform-box: fill-box;
transform-origin: center;
filter: blur(1.5px);
}
.sb-ai-bulb-halo--pulsing {
animation: sb-bulb-halo 650ms ease-out;
}
@keyframes sb-bulb-halo {
0% { transform: scale(1); opacity: 0.9; }
100% { transform: scale(5); opacity: 0; }
}
/* A near-zero duration cut is safe here specifically because nothing in this
app uses motion to CARRY information — every transition here (hover color,
progress-bar fill, a focus ring) has both its start and end state fully
legible on their own; the animation is polish, not the only way a state
change is communicated. That is the case where collapsing the duration
preserves every state change and loses only the easing, rather than the
riskier version of this pattern that hides a spinner or skips a toast's
only appearance. */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
/* The blanket collapse above is wrong for anything `infinite`: a 0.01ms
duration doesn't finish and settle, it restarts forever, so an ambient
loop reads as a rapid strobe instead of holding still — worse for a
motion-sensitive visitor than the motion it was meant to remove. The
sign-in screen's drifting agents, their dance, the brand mark's bob and
the name's sheen are all `infinite`, so they need to actually stop. */
.sb-agent,
.sb-agent .sb-ai-core,
.sb-mark-bob,
.sb-brand-name {
animation: none !important;
}
/* Stopping the drift leaves every agent stacked at the left edge, since 0%
of `sb-bot-drift` is where they all start. Parked across the width
instead, so the still frame is the composition the motion was making. */
.sb-agent--1 { transform: translateX(14vw); }
.sb-agent--2 { transform: translateX(58vw); }
.sb-agent--3 { transform: translateX(80vw); }
.sb-agent--4 { transform: translateX(34vw); }
.sb-agent--5 { transform: translateX(66vw); }
/* And the sheen settles on its resting stop rather than the off-screen one
it starts from, which would leave the name in flat white. */
.sb-brand-name { background-position: 0 0; }
}
/* ── Office Location (a Example Client ticket's yellow note) ──────────────────────
The note is amber because it IS amber on the page; everything else stays on
the neutral surfaces. No brand ramp here -- it is reserved for its six
things (DESIGN.md, "The One Loud Colour Rule"). */
.sb-ol-sect {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
font: 600 11px var(--font-plex);
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.sb-ol-sect::after {
content: '';
flex: 1;
height: 1px;
background: var(--line-soft);
}
.sb-ol-tag {
font: 600 10.5px var(--font-plex);
color: var(--muted);
border: 1px solid var(--line);
border-radius: 999px;
padding: 1px 7px;
text-transform: none;
letter-spacing: 0;
}
.sb-ol-tabs {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.sb-ol-tab {
border: 1px solid var(--line);
background: var(--surface-2);
border-radius: 9px;
padding: 6px 11px;
font: 600 12px var(--font-plex);
color: var(--muted);
cursor: pointer;
}
.sb-ol-tab[aria-selected='true'] {
color: var(--ink);
background: var(--surface);
border-color: var(--ink-2);
}
.sb-ol-tab small {
margin-left: 6px;
font-weight: 500;
color: var(--muted);
}
.sb-ol-info {
font: 500 11.5px var(--font-plex);
color: var(--muted);
}
.sb-ol-line {
display: flex;
flex-direction: column;
gap: 7px;
border: 1px solid var(--line);
border-radius: 11px;
padding: 8px;
background: var(--surface-2);
}
/* A separate encounter billed under its own provider. */
.sb-ol-line-own {
border: 2px solid var(--accent-1);
}
.sb-ol-line-own .sb-ol-tag {
margin-left: 6px;
color: var(--accent-1);
border-color: var(--accent-1);
}
.sb-ol-line-h {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 4px 8px;
padding: 0 3px;
font: 500 11.5px var(--font-plex);
color: var(--muted);
}
.sb-ol-line-h b {
font-weight: 600;
color: var(--ink);
}
.sb-ol-warn {
font: 500 11.5px var(--font-plex);
color: var(--verify);
background: color-mix(in srgb, var(--verify) 12%, transparent);
border-radius: 8px;
padding: 6px 9px;
}
/* The three note-rule names are long ("Referring Provider Update"); the
standard 78px label column would clip them to nothing. */
.sb-field-row.sb-wide-lbl {
grid-template-columns: 172px minmax(0, 1fr) 42px 70px;
}