/* ============================================================================ Technical-Light design system — portable theme layer ---------------------------------------------------------------------------- A refined technical-light aesthetic: warm-neutral paper, hairline rules, IBM Plex type, monospace tabular numerics, status carried by colour. Built to layer over Bootstrap 5 via --bs-* overrides, but every rule below works standalone — Bootstrap is optional. HOW TO ADOPT 1. Serve the three IBM Plex woff2 files (shipped in fonts/) and fix the @font-face url() paths below to wherever you serve them. 2. Include this file once, globally. Add view-specific rules in a separate stylesheet — never edit the token block per-view. 3. Status is colour, not iconography. Use the .s-* / .chip-* / .kv .v.* helpers; do not hand-pick hex values in feature CSS. ========================================================================= */ /* ── Vendored fonts (embedded woff2, no network/CDN fetch) ─────────────────── Adjust these url()s to your asset route. If you cannot vendor the fonts the --sans / --mono fallback stacks below degrade gracefully to system fonts. */ @font-face { font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 400; font-display: swap; src: url('../fonts/ibm-plex-sans-400.woff2') format('woff2'); } @font-face { font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 600; font-display: swap; src: url('../fonts/ibm-plex-sans-600.woff2') format('woff2'); } @font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 500; font-display: swap; src: url('../fonts/ibm-plex-mono-500.woff2') format('woff2'); } /* ── Design tokens ─────────────────────────────────────────────────────────── The single source of truth. Re-theme by editing only this block. */ :root { /* Surfaces & ink */ --paper: #f4f4f1; /* page background — warm off-white, never pure */ --card: #ffffff; /* raised surfaces: cards, bars, table heads */ --ink: #1b1d21; /* primary text */ --ink-soft: #5a6066; /* secondary text, labels */ --ink-faint: #8b9097; /* tertiary text, captions, units */ --rule: #e4e4df; /* hairline borders / row dividers */ --rule-strong: #d2d2cb; /* emphasised hairlines: bar underline, pills */ /* Accent */ --accent: #2f5fd0; /* links, sort arrows, primary actions */ --accent-deep: #1e3f99; /* hover / pressed accent, raw-value emphasis */ /* Status — foreground */ --ok: #2f9e44; --warn: #e8920c; --bad: #e03131; --idle: #868e96; /* Status — tinted backgrounds (pair with the matching foreground) */ --ok-bg: #e9f6ec; --warn-bg: #fdf1dd; --bad-bg: #fceaea; --idle-bg: #eef0f2; /* Type stacks — Plex first, graceful system fallback */ --mono: 'IBM Plex Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace; --sans: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', sans-serif; /* Bootstrap 5 overrides — harmless if Bootstrap is absent */ --bs-body-bg: var(--paper); --bs-body-color: var(--ink); --bs-body-font-family: var(--sans); --bs-body-font-size: 0.9rem; --bs-primary: var(--accent); --bs-border-color: var(--rule); --bs-emphasis-color: var(--ink); } /* ── Base ──────────────────────────────────────────────────────────────────── The faint top-right radial is the one deliberate flourish — a soft sheen, not a gradient wash. Keep it subtle. */ body { background: radial-gradient(1200px 480px at 88% -8%, #ffffff 0%, rgba(255,255,255,0) 70%), var(--paper); color: var(--ink); font-family: var(--sans); font-size: 0.9rem; -webkit-font-smoothing: antialiased; } /* Any numeric / fixed-width text. Tabular figures so columns of digits align. */ .numeric, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; } a { color: var(--accent); text-decoration: none; } a:hover { color: var(--accent-deep); text-decoration: underline; } /* ── App chrome: top bar ───────────────────────────────────────────────────── One bar across the top: brand, breadcrumb crumbs, a flex spacer, then meta text and any status pill pushed hard right. */ .app-bar { display: flex; align-items: baseline; gap: 1rem; padding: 0.85rem 1.25rem; background: var(--card); border-bottom: 1px solid var(--rule-strong); } .app-bar .brand { font-weight: 600; font-size: 1.05rem; letter-spacing: 0.02em; } .app-bar .brand .mark { color: var(--accent); } /* the one accent glyph */ .app-bar .crumb { color: var(--ink-faint); font-size: 0.85rem; } .app-bar .spacer { flex: 1; } /* pushes meta/pill right */ .app-bar .meta { font-family: var(--mono); font-size: 0.78rem; color: var(--ink-soft); } /* ── Connection / liveness pill ────────────────────────────────────────────── A rounded pill with a dot, driven entirely by data-state. Use for any live-link health indicator (websocket, SSE, polling). */ .conn-pill { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.74rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; padding: 0.2rem 0.6rem; border-radius: 999px; border: 1px solid var(--rule-strong); color: var(--ink-soft); background: var(--card); } .conn-pill .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--idle); } .conn-pill[data-state="connected"] { color: var(--ok); border-color: #bfe3c6; background: var(--ok-bg); } .conn-pill[data-state="connected"] .dot { background: var(--ok); } .conn-pill[data-state="connecting"] { color: var(--warn); border-color: #f0d9ab; background: var(--warn-bg); } .conn-pill[data-state="connecting"] .dot { background: var(--warn); animation: pulse 1.1s ease-in-out infinite; } .conn-pill[data-state="disconnected"] { color: var(--bad); border-color: #f0c0c0; background: var(--bad-bg); } .conn-pill[data-state="disconnected"] .dot { background: var(--bad); } @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.25; } } /* ── Status text helpers ───────────────────────────────────────────────────── Recolour a value in place — counts, ratios, error totals. */ .s-ok { color: var(--ok); } .s-warn { color: var(--warn); } .s-bad { color: var(--bad); } .s-idle { color: var(--idle); } /* ── State chip ────────────────────────────────────────────────────────────── Compact rectangular badge for an enumerated state (bound/recovering/…). Squarer than the pill; use the pill for liveness, the chip for state. */ .chip { display: inline-block; font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.15rem 0.5rem; border-radius: 4px; border: 1px solid transparent; } .chip-ok { color: var(--ok); background: var(--ok-bg); border-color: #c6e6cd; } .chip-warn { color: #b56a00; background: var(--warn-bg); border-color: #efd6a6; } .chip-bad { color: var(--bad); background: var(--bad-bg); border-color: #eec3c3; } .chip-idle { color: var(--ink-soft); background: var(--idle-bg); border-color: var(--rule-strong); } /* ── Panel — the base raised surface ───────────────────────────────────────── A white card with a hairline border and 8px radius. .panel-head is the uppercase eyebrow label that sits on top. */ .panel { background: var(--card); border: 1px solid var(--rule); border-radius: 8px; } .panel-head { font-size: 0.74rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-faint); padding: 0.6rem 0.9rem; border-bottom: 1px solid var(--rule); } /* ── Page wrapper ──────────────────────────────────────────────────────────── Centred, capped width, even gutter. */ .page { padding: 1.25rem; max-width: 1680px; margin: 0 auto; } /* ── Reveal-on-paint ───────────────────────────────────────────────────────── Add .rise to top-level sections; stagger with inline animation-delay (.02s, .08s, .14s …) so panels settle in sequence, not all at once. */ @keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } } .rise { animation: rise 0.4s ease both; } /* ════════════════════════════════════════════════════════════════════════════ COMPONENT LIBRARY Generic, reusable pieces. View-specific layout belongs in a separate sheet. ════════════════════════════════════════════════════════════════════════════ */ /* ── KPI / aggregate cards ─────────────────────────────────────────────────── A responsive strip of headline numbers. .agg-card.alert / .caution tint the whole card when a watched metric goes non-zero. */ .agg-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 0.75rem; margin-bottom: 1rem; } @media (max-width: 1100px) { .agg-grid { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 620px) { .agg-grid { grid-template-columns: repeat(2, 1fr); } } .agg-card { background: var(--card); border: 1px solid var(--rule); border-radius: 8px; padding: 0.7rem 0.9rem; } .agg-label { font-size: 0.68rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-faint); } .agg-value { margin-top: 0.25rem; font-size: 1.5rem; font-weight: 600; line-height: 1.1; display: flex; align-items: baseline; gap: 0.35rem; } .agg-sub { /* trailing "/ 54", "ms" etc. — quieter */ font-size: 0.85rem; font-weight: 400; color: var(--ink-faint); } .agg-card.alert { border-color: #eec3c3; background: var(--bad-bg); } .agg-card.alert .agg-value { color: var(--bad); } .agg-card.caution { border-color: #efd6a6; background: var(--warn-bg); } .agg-card.caution .agg-value { color: #b56a00; } /* ── Metric card + key/value rows ──────────────────────────────────────────── A .panel-head over a stack of .kv rows: label left, monospace value right. Zebra striping on even rows. .v.warn / .v.bad / .v.ok recolour a value. */ .card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 0.85rem; margin-bottom: 1rem; } .metric-card { background: var(--card); border: 1px solid var(--rule); border-radius: 8px; overflow: hidden; } .metric-card .panel-head { margin: 0; } .kv { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; padding: 0.32rem 0.9rem; font-size: 0.85rem; } .kv:nth-child(even) { background: #fbfbf9; } .kv .k { color: var(--ink-soft); } .kv .v { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right; } .kv .v.warn { color: var(--warn); } .kv .v.bad { color: var(--bad); } .kv .v.ok { color: var(--ok); } /* ── Toolbar ───────────────────────────────────────────────────────────────── Filter/search row that sits inside a .panel above a table. */ .toolbar { display: flex; align-items: center; gap: 0.6rem; padding: 0.6rem 0.9rem; border-bottom: 1px solid var(--rule); } .toolbar .spacer { flex: 1; } .tb-search { max-width: 280px; } .tb-state { max-width: 150px; } .tb-check { display: flex; align-items: center; gap: 0.35rem; font-size: 0.82rem; color: var(--ink-soft); white-space: nowrap; user-select: none; } .tb-count { font-family: var(--mono); font-size: 0.78rem; color: var(--ink-faint); } /* ── Data table ────────────────────────────────────────────────────────────── Dense, hairline-ruled table. Uppercase sticky head on a faint fill; numeric columns get .num (right-aligned, monospace). Rows are clickable by default — drop the cursor/hover rules if yours are not. */ .table-wrap { overflow-x: auto; } .data-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; } .data-table th, .data-table td { padding: 0.45rem 0.8rem; text-align: left; white-space: nowrap; border-bottom: 1px solid var(--rule); } .data-table th { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-faint); background: #fbfbf9; position: sticky; top: 0; } .data-table th.num, .data-table td.num { text-align: right; font-family: var(--mono); } .data-table th.sortable { cursor: pointer; user-select: none; } .data-table th.sortable:hover { color: var(--ink); } .data-table th.sorted-asc::after { content: ' \2191'; color: var(--accent); } .data-table th.sorted-desc::after { content: ' \2193'; color: var(--accent); } .data-table tbody tr { cursor: pointer; transition: background 0.08s; } .data-table tbody tr:hover { background: #f3f6fd; } .data-table tbody tr:last-child td { border-bottom: none; } .empty-row { text-align: center !important; color: var(--ink-faint); padding: 1.6rem !important; font-style: italic; } /* ── Direction / category tag ──────────────────────────────────────────────── Tiny inline tag for a per-row category (e.g. read vs write). */ .dir-tag { font-size: 0.68rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.1rem 0.4rem; border-radius: 3px; } .dir-read { color: var(--accent-deep); background: #e7ecfb; } .dir-write { color: #8a5a00; background: var(--warn-bg); } /* ── Inline notice ─────────────────────────────────────────────────────────── A .panel with a warning tint — for "this thing is gone / degraded" banners. */ .notice { padding: 0.85rem 1.1rem; margin-bottom: 1rem; color: #b56a00; background: var(--warn-bg); border-color: #efd6a6; }