src/index.css
Rama main · réplica de lectura del repositorio original.
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600&display=swap');
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&display=swap');
/* Cinzel for engraved-Roman titles in the Sanctum (LLM Sun
panel header, Archive Warden label, page title). Used sparingly so the rest
of the HUD stays scannable in monospace. */
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&display=swap');
@import "tailwindcss";
:root,
:root[data-theme="dark"] {
--bg-page: oklch(0.145 0 0);
--bg-card: oklch(0.205 0 0);
--bg-hover: oklch(0.269 0 0);
--bg-accent: oklch(0.371 0 0);
--border: oklch(1 0 0 / 10%);
--border-input: oklch(1 0 0 / 15%);
--border-hover: oklch(1 0 0 / 20%);
--text-primary: oklch(0.985 0 0);
--text-secondary: oklch(0.708 0 0);
--text-muted: oklch(0.556 0 0);
--accent: oklch(0.488 0.243 264.376);
--accent-hover: oklch(0.55 0.243 264.376);
--green: oklch(0.72 0.17 142);
--amber: oklch(0.78 0.15 85);
--red: oklch(0.63 0.2 25);
--purple: oklch(0.65 0.2 295);
--cyan: oklch(0.75 0.15 195);
--ease: cubic-bezier(0.4, 0, 0.2, 1);
--sidebar-w: 220px;
}
/* ─── Scale tokens ───────────────────────────────────────────────────────────
Type, spacing and radius were ad-hoc before this: 26 distinct font sizes and
47 size/weight pairs across the app, including half-point values (10.5,
11.5, 12.5). These seven steps cover every real use. Anything that does not
fit a step is a design bug, not a missing step. */
:root {
--fs-label: 10px; /* uppercase eyebrows, axis ticks */
--fs-meta: 11px; /* captions, sub-values, chart legends */
--fs-ui: 12px; /* dense controls, table cells */
--fs-body: 13px; /* default UI text, nav, buttons */
--fs-title: 15px; /* card and section titles */
--fs-page: 20px; /* page titles */
--fs-metric: 28px; /* KPI numbers */
--sp-1: 4px;
--sp-2: 8px;
--sp-3: 12px;
--sp-4: 16px;
--sp-5: 24px;
--sp-6: 32px;
--sp-7: 48px;
--r-sm: 6px;
--r-md: 8px;
--r-lg: 12px;
--r-full: 999px;
--shadow-card:
0 0 0 0.5px oklch(1 0 0 / 5%) inset,
0 1px 2px rgba(0, 0, 0, 0.04),
0 4px 8px rgba(0, 0, 0, 0.06);
--shadow-pop: 0 16px 48px rgba(0, 0, 0, 0.36);
--page-max: 1180px;
}
/* Semantic role aliases — separate from raw hue tokens so future themes can
re-paint each role without renaming every consumer. They resolve to the
raw hue tokens above today; once a role needs to diverge (e.g. success
shouldn't be the same green as currency), only the alias changes. */
:root {
--success: var(--green);
--currency: var(--green);
--positive: var(--green);
--danger: var(--red);
--over-budget: var(--red);
--warning: var(--amber);
--info: var(--cyan);
/* Chart series — use these for Recharts/D3 stroke/fill so theme-flips
keep series legible. Avoid hardcoding raw hue tokens in chart code. */
--data-1: var(--accent);
--data-2: var(--cyan);
--data-3: var(--green);
--data-4: var(--amber);
--data-5: var(--purple);
--data-6: var(--red);
/* Loop-Ops entity statuses — operational, not decorative (spec §8).
--orange is a base hue token (like --amber above) so the light overlay
can re-darken it. */
--orange: oklch(0.72 0.15 60);
--loop-covered: var(--cyan);
--loop-wired: var(--accent);
--loop-running: var(--purple);
--loop-blocked: var(--amber);
--loop-failed: var(--red);
--loop-passed: var(--green);
--loop-needs-review: var(--orange);
}
/* Sanctum A9: one surface recipe and a 9px minimum for secondary HUD copy. */
.sanctum-hud-panel {
background: rgba(8, 18, 23, .72) !important;
border: 1px solid rgba(100, 229, 194, .14) !important;
border-radius: 10px !important;
backdrop-filter: blur(14px) !important;
box-shadow: 0 18px 44px rgba(0, 0, 0, .32), inset 0 1px rgba(255, 255, 255, .025) !important;
font-family: 'JetBrains Mono', monospace !important;
font-size: 10px;
}
@media (max-width: 720px) {
.sanctum-toolbar {
min-height: 0 !important;
padding: 9px 12px !important;
gap: 7px !important;
}
.sanctum-toolbar-brand {
min-width: 0 !important;
flex: 1 1 100%;
}
.sanctum-toolbar-metrics {
display: none !important;
}
.sanctum-toolbar-controls {
min-width: 0 !important;
flex: 1 1 100% !important;
gap: 6px !important;
}
.sanctum-toolbar-run-group {
flex: 1 1 100% !important;
width: 100% !important;
}
}
.sanctum-hud-panel [style*="font-size: 7"],
.sanctum-hud-panel [style*="font-size: 8"] { font-size: 9px !important; }
.sanctum-hud-panel.sanctum-hud-round { border-radius: 50% !important; }
.sanctum-hud-title {
font-family: 'Cinzel', serif !important;
font-size: 14px !important;
letter-spacing: 3px;
}
/* Loop-Ops node reveal — staggered via per-node animation-delay. Disabled
entirely under reduced motion; no other ambient movement exists (spec §8). */
.loop-reveal {
opacity: 0;
animation: loop-reveal-in 0.35s var(--ease) forwards;
}
@keyframes loop-reveal-in {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
.loop-reveal { animation: none; opacity: 1; }
}
/* Refresh-spec button while the importer runs — activity feedback, not ambient. */
.loop-spin {
animation: loop-spin 1s linear infinite;
}
@keyframes loop-spin {
to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.loop-spin { animation: none; }
}
/* ─── Light theme overlay ────────────────────────────────────────────────────
Activated by setting `<html data-theme="light">`. The toggle in the sidebar
footer flips this attribute and persists to localStorage. The Sanctum 3D
scene and Companion 2D scene render to their own canvases with internally
baked palettes — they're not theme-aware (yet) and stay dark in either
theme; the dashboard chrome around them flips. */
:root[data-theme="light"] {
--bg-page: oklch(0.985 0 0);
--bg-card: oklch(0.97 0 0);
--bg-hover: oklch(0.94 0 0);
--bg-accent: oklch(0.88 0 0);
--border: oklch(0 0 0 / 10%);
--border-input: oklch(0 0 0 / 15%);
--border-hover: oklch(0 0 0 / 22%);
--text-primary: oklch(0.205 0 0);
--text-secondary: oklch(0.40 0 0);
--text-muted: oklch(0.55 0 0);
--accent: oklch(0.50 0.243 264.376);
--accent-hover: oklch(0.44 0.243 264.376);
--green: oklch(0.55 0.17 142);
--amber: oklch(0.65 0.15 85);
--red: oklch(0.55 0.2 25);
--purple: oklch(0.55 0.2 295);
--cyan: oklch(0.55 0.15 195);
--orange: oklch(0.58 0.15 60);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
font-weight: 400;
letter-spacing: 0.16px;
background: var(--bg-page);
color: var(--text-primary);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4 {
font-weight: 300;
}
code, .font-mono {
font-family: 'JetBrains Mono', monospace;
}
@keyframes emerge {
from { opacity: 0; transform: scale(0.95) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.emerge {
animation: emerge 0.5s var(--ease) both;
}
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--r-lg);
box-shadow: var(--shadow-card);
transition: border-color 0.2s var(--ease);
}
.card:hover {
border-color: var(--border-hover);
}
/* ─── Primitives ─────────────────────────────────────────────────────────────
Real CSS classes rather than inline style objects, so :hover and
:focus-visible are handled by the cascade. The previous pattern attached
onMouseEnter/onMouseLeave handlers to ~35 files to fake hover, which also
meant keyboard users never saw a focus ring. */
:where(button, a, input, select, textarea, [tabindex]):focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: var(--r-sm);
}
.mo-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--r-lg);
box-shadow: var(--shadow-card);
transition: border-color 0.2s var(--ease);
}
.mo-card--flat { box-shadow: none; }
.mo-card--inset { background: var(--bg-page); box-shadow: none; }
.mo-card--pad { padding: var(--sp-4); }
.mo-card--interactive { cursor: pointer; }
.mo-card--interactive:hover { border-color: var(--border-hover); }
.mo-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--sp-2);
padding: 7px var(--sp-3);
border: 1px solid var(--border);
border-radius: var(--r-md);
background: var(--bg-card);
color: var(--text-secondary);
font-family: inherit;
font-size: var(--fs-body);
font-weight: 500;
line-height: 1.2;
white-space: nowrap;
cursor: pointer;
transition: background 0.15s var(--ease), border-color 0.15s var(--ease), color 0.15s var(--ease);
}
.mo-btn:hover:not(:disabled) { background: var(--bg-hover); border-color: var(--border-hover); color: var(--text-primary); }
.mo-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.mo-btn--sm { padding: 4px var(--sp-2); font-size: var(--fs-ui); }
.mo-btn--lg { padding: 10px var(--sp-4); }
.mo-btn--block { width: 100%; }
.mo-btn--primary { background: var(--accent); border-color: var(--accent); color: oklch(0.99 0 0); }
.mo-btn--primary:hover:not(:disabled) { background: var(--accent-hover); border-color: var(--accent-hover); color: oklch(0.99 0 0); }
.mo-btn--ghost { background: transparent; border-color: transparent; }
.mo-btn--ghost:hover:not(:disabled) { background: var(--bg-hover); border-color: transparent; }
.mo-btn--danger { color: var(--danger); border-color: color-mix(in oklch, var(--danger) 40%, transparent); }
.mo-btn--danger:hover:not(:disabled) { background: color-mix(in oklch, var(--danger) 12%, transparent); color: var(--danger); }
.mo-btn--icon { padding: 6px; }
.mo-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 2px var(--sp-2);
border-radius: var(--r-full);
border: 1px solid transparent;
font-size: var(--fs-meta);
font-weight: 500;
line-height: 1.5;
white-space: nowrap;
}
.mo-badge--neutral { background: var(--bg-hover); color: var(--text-secondary); }
.mo-badge--accent { background: color-mix(in oklch, var(--accent) 16%, transparent); color: var(--accent); border-color: color-mix(in oklch, var(--accent) 32%, transparent); }
.mo-badge--positive { background: color-mix(in oklch, var(--green) 14%, transparent); color: var(--green); border-color: color-mix(in oklch, var(--green) 30%, transparent); }
.mo-badge--warning { background: color-mix(in oklch, var(--amber) 14%, transparent); color: var(--amber); border-color: color-mix(in oklch, var(--amber) 30%, transparent); }
.mo-badge--danger { background: color-mix(in oklch, var(--red) 14%, transparent); color: var(--red); border-color: color-mix(in oklch, var(--red) 30%, transparent); }
.mo-badge--info { background: color-mix(in oklch, var(--cyan) 14%, transparent); color: var(--cyan); border-color: color-mix(in oklch, var(--cyan) 30%, transparent); }
.mo-eyebrow {
font-size: var(--fs-label);
font-weight: 500;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--text-muted);
}
/* Scope chip — Product Law 2: every number states its range, its source, and
whether it came from the complete archive or the bounded preview. */
.mo-scope {
display: inline-flex;
flex-wrap: wrap;
min-width: 0;
align-items: center;
gap: var(--sp-1);
font-size: var(--fs-meta);
color: var(--text-muted);
white-space: normal;
}
.mo-scope__dot { opacity: 0.4; }
.mo-scope--warn { color: var(--amber); }
/* Help affordance for terms that need one. A term needing a permanent tooltip
is a naming smell; this exists for the metrics where the short name really
is the industry term (TPM, p95) and renaming would be worse. */
.mo-help {
display: inline-flex;
align-items: center;
justify-content: center;
width: 13px;
height: 13px;
border-radius: var(--r-full);
border: 1px solid var(--border-input);
background: transparent;
color: var(--text-muted);
font-size: 9px;
font-family: inherit;
line-height: 1;
cursor: help;
padding: 0;
flex-shrink: 0;
}
.mo-help:hover { color: var(--text-primary); border-color: var(--border-hover); }
.mo-tabs {
display: flex;
align-items: center;
gap: var(--sp-1);
border-bottom: 1px solid var(--border);
overflow-x: auto;
scrollbar-width: none;
}
.mo-tabs::-webkit-scrollbar { display: none; }
.mo-tab {
position: relative;
padding: var(--sp-2) var(--sp-3);
border: none;
background: transparent;
color: var(--text-muted);
font-family: inherit;
font-size: var(--fs-body);
font-weight: 500;
white-space: nowrap;
cursor: pointer;
transition: color 0.15s var(--ease);
}
.mo-tab:hover { color: var(--text-secondary); }
.mo-tab[aria-selected='true'] { color: var(--text-primary); }
.mo-tab[aria-selected='true']::after {
content: '';
position: absolute;
left: var(--sp-2);
right: var(--sp-2);
bottom: -1px;
height: 2px;
background: var(--accent);
border-radius: 2px 2px 0 0;
}
.mo-tab__count {
margin-left: 6px;
font-size: var(--fs-meta);
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
.mo-kbd {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 17px;
padding: 1px 4px;
border: 1px solid var(--border-input);
border-radius: 4px;
background: var(--bg-page);
color: var(--text-muted);
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
line-height: 1.4;
}
.mo-num { font-variant-numeric: tabular-nums; }
/* Focus chip — lives in the page header on every screen. Not a page. */
.mo-focus { position: relative; }
.mo-focus__chip {
display: inline-flex;
align-items: stretch;
border: 1px solid var(--border);
border-radius: var(--r-full);
background: var(--bg-card);
overflow: hidden;
}
.mo-focus__chip[data-running='true'] { border-color: color-mix(in oklch, var(--accent) 45%, var(--border)); }
.mo-focus__toggle,
.mo-focus__time {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
color: var(--text-secondary);
font-family: inherit;
cursor: pointer;
padding: 0;
}
.mo-focus__toggle { width: 26px; color: var(--text-primary); }
.mo-focus__time {
padding: 4px 10px 4px 2px;
font-size: var(--fs-ui);
color: var(--text-primary);
}
.mo-focus__chip[data-running='true'] .mo-focus__time { color: var(--accent); }
.mo-focus__pop {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 80;
width: 220px;
padding: var(--sp-3);
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--r-lg);
box-shadow: var(--shadow-pop);
display: grid;
gap: var(--sp-3);
}
.mo-focus__pophead {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--sp-2);
}
.mo-focus__bar {
height: 3px;
border-radius: var(--r-full);
background: var(--bg-hover);
overflow: hidden;
}
.mo-focus__bar > span {
display: block;
height: 100%;
background: var(--accent);
}
.mo-focus__actions { display: flex; gap: var(--sp-2); }
.mo-focus__presets { display: flex; gap: var(--sp-1); }
.mo-focus__preset {
flex: 1;
padding: 4px 0;
border: 1px solid var(--border);
border-radius: var(--r-sm);
background: transparent;
color: var(--text-muted);
font-family: inherit;
font-size: var(--fs-meta);
cursor: pointer;
}
.mo-focus__preset.is-on,
.mo-focus__preset:hover { color: var(--text-primary); border-color: var(--border-hover); }
/* ─── Command palette ────────────────────────────────────────────────────── */
.mo-palette__backdrop {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 12vh var(--sp-4) var(--sp-4);
background: oklch(0 0 0 / 45%);
backdrop-filter: blur(3px);
}
.mo-palette {
width: min(560px, 100%);
max-height: 60vh;
display: flex;
flex-direction: column;
background: var(--bg-card);
border: 1px solid var(--border-hover);
border-radius: var(--r-lg);
box-shadow: var(--shadow-pop);
overflow: hidden;
}
.mo-palette__input {
width: 100%;
padding: var(--sp-4);
border: none;
border-bottom: 1px solid var(--border);
background: transparent;
color: var(--text-primary);
font-family: inherit;
font-size: var(--fs-title);
outline: none;
}
.mo-palette__input::placeholder { color: var(--text-muted); }
.mo-palette__list {
flex: 1;
overflow-y: auto;
padding: var(--sp-2);
}
.mo-palette__group {
padding: var(--sp-2) var(--sp-3) var(--sp-1);
}
.mo-palette__item {
width: 100%;
display: flex;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-2) var(--sp-3);
border: none;
border-radius: var(--r-md);
background: transparent;
color: var(--text-secondary);
font-family: inherit;
font-size: var(--fs-body);
text-align: left;
cursor: pointer;
}
.mo-palette__item[data-active='true'] { background: var(--bg-hover); color: var(--text-primary); }
.mo-palette__item:focus-visible { outline: none; background: var(--bg-hover); }
.mo-palette__hint { margin-left: auto; display: flex; gap: var(--sp-1); align-items: center; }
.mo-palette__empty {
padding: var(--sp-6) var(--sp-4);
text-align: center;
color: var(--text-muted);
font-size: var(--fs-body);
}
.mo-palette__footer {
display: flex;
align-items: center;
gap: var(--sp-4);
padding: var(--sp-2) var(--sp-4);
border-top: 1px solid var(--border);
color: var(--text-muted);
font-size: var(--fs-meta);
}
@media (prefers-reduced-motion: no-preference) {
.mo-palette { animation: mo-pop 0.14s var(--ease) both; }
}
@keyframes mo-pop {
from { opacity: 0; transform: translateY(-6px) scale(0.985); }
to { opacity: 1; transform: none; }
}
/* ─── App shell ──────────────────────────────────────────────────────────── */
.mo-shell { display: flex; min-height: 100vh; }
.mo-main {
margin-left: var(--sidebar-w);
flex: 1;
min-width: 0;
width: calc(100vw - var(--sidebar-w));
}
.mo-main--standard { padding: var(--sp-6); max-width: calc(var(--page-max) + var(--sidebar-w)); }
.mo-main--bleed { padding: 0; display: flex; flex-direction: column; height: 100vh; }
/* Full-bleed surfaces keep a padded header rail so the 3D scene and the loop
canvas still have visible tabs and a title. Previously the Sanctum swallowed
the whole viewport and the only way out was the sidebar. */
.mo-bleedhead {
flex-shrink: 0;
padding: var(--sp-4) var(--sp-5) 0;
background: var(--bg-page);
border-bottom: 1px solid var(--border);
z-index: 2;
}
.mo-bleedhead .mo-pageheader { margin-bottom: var(--sp-3); }
.mo-bleedbody { flex: 1; min-height: 0; display: flex; flex-direction: column; position: relative; }
.mo-pageheader {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--sp-4);
flex-wrap: wrap;
margin-bottom: var(--sp-4);
}
.mo-pageheader__title { font-size: var(--fs-page); font-weight: 300; margin: 0; }
.mo-pageheader__desc { margin-top: 3px; font-size: var(--fs-ui); color: var(--text-muted); max-width: 62ch; }
.mo-pageheader__actions { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.mo-section { margin-bottom: var(--sp-5); }
.mo-section__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-3);
margin-bottom: var(--sp-3);
}
.mo-grid { display: grid; gap: var(--sp-3); }
.mo-grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.mo-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.mo-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 1100px) { .mo-grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 860px) {
.mo-grid--3, .mo-grid--2 { grid-template-columns: minmax(0, 1fr); }
.mo-main--standard { padding: var(--sp-4); }
}
/* Helper-offline banner. Product Law 4: state it once, at the top, with the
exact command — rather than five surfaces each discovering it separately. */
.mo-notice {
display: flex;
align-items: center;
gap: var(--sp-3);
flex-wrap: wrap;
padding: var(--sp-3) var(--sp-4);
margin-bottom: var(--sp-4);
border: 1px solid color-mix(in oklch, var(--amber) 30%, transparent);
background: color-mix(in oklch, var(--amber) 8%, transparent);
border-radius: var(--r-md);
font-size: var(--fs-ui);
color: var(--text-secondary);
}
.mo-notice code {
padding: 2px 6px;
border-radius: 4px;
background: var(--bg-page);
color: var(--amber);
font-size: var(--fs-meta);
}
.mo-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--sp-3);
padding: var(--sp-7) var(--sp-5);
text-align: center;
}
.mo-empty__title { font-size: var(--fs-title); font-weight: 400; color: var(--text-primary); }
.mo-empty__body { font-size: var(--fs-body); color: var(--text-muted); max-width: 46ch; line-height: 1.65; }
.mo-empty__cmd {
padding: var(--sp-2) var(--sp-4);
border: 1px solid var(--border);
border-radius: var(--r-md);
background: var(--bg-card);
color: var(--accent);
font-family: 'JetBrains Mono', monospace;
font-size: var(--fs-ui);
user-select: all;
}
@media (max-width: 720px) {
.mo-tab { padding-inline: var(--sp-2); }
:root,
:root[data-theme="dark"],
:root[data-theme="light"] {
--sidebar-w: 72px;
}
.sidebar-logo-wrap {
padding: 0 16px !important;
justify-content: center;
}
.sidebar-logo-text,
.sidebar-section-label,
.sidebar-nav-label,
.sidebar-sync-text,
.sidebar-sync-time,
.sidebar-powered-text {
display: none !important;
}
.sidebar-nav-button {
justify-content: center;
padding: 10px 0 !important;
gap: 0 !important;
}
.sidebar-sync-button {
justify-content: center;
padding: 9px 0 !important;
gap: 0 !important;
}
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--bg-accent);
border-radius: 3px;
}