/* Global visual polish that used to come from MUI's / in themes/index.js (ThemeCustomization). Kept as plain CSS now that every page runs on Astryx — see root CLAUDE.md §6. Astryx's own reset (@astryxdesign/core/reset.css, imported in index.js) covers the base element resets; this file carries the handful of things a design token cannot express: font rendering, scrollbar pseudo-elements, keyframes, the focus treatment, and the global reduced-motion rule. NOTE ON COLOUR: nothing here introduces a colour. The console keeps its own palette (themes/dt/tokens.js) — the KROW work adopted KROW's geometry and type treatment only. Where a rule below needs an accent it reads `var(--color-accent)` rather than naming one. */ body { /* Astryx sets --font-family-body on :root (from themes/astryx.js's typography config) but nothing was applying it to the document, and MUI's — which used to do this — is no longer mounted. The result was a body computing to "Times New Roman": Astryx components rendered correctly because they set their own font, but every plain element and any text outside a inherited the browser serif default. Verified in the browser on /login before and after. */ font-family: var(--font-family-body); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; } /* -------------------------------------------------------------------------- Motion vocabulary. One set of durations and one easing curve for the whole app, so a hover, a row highlight and a card entrance all feel like the same product. Three steps only: `fast` for colour-only changes (anything slower feels laggy under a moving cursor), `base` for the default, `slow` for layout and entrances. Prefixed --dt-* deliberately. These live on :root, and Dispatch.css ALSO writes :root from a lazy-loaded chunk — an unprefixed name there would win or lose purely on bundle order rather than on intent. -------------------------------------------------------------------------- */ :root { --dt-ease-out: cubic-bezier(0.16, 1, 0.3, 1); --dt-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); --dt-duration-fast: 120ms; --dt-duration-base: 200ms; --dt-duration-slow: 320ms; } /* -------------------------------------------------------------------------- Focus. Focus is never the browser default. One ring, one width, everywhere. The colour is the app's own accent token, not a value chosen here, so the ring follows the brand automatically. :focus-visible, not :focus — a mouse click on a button should not leave a ring behind it. -------------------------------------------------------------------------- */ .dt-focus-ring:focus-visible { outline: none; box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px var(--color-accent); } /* The inset variant, for a control flush inside a row or table cell where an offset ring would be clipped by the overflow container. */ .dt-focus-ring-inset:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--color-accent); } /* -------------------------------------------------------------------------- Scrollbars. Chrome, not content — they should recede until used. The thumb was rgba(0,0,0,0.26) at 10px, which drew a solid dark bar down the right edge of every scrollable table and read as a heavy border rather than as a scrollbar. Lightened and narrowed; it still darkens on hover so it stays findable. -------------------------------------------------------------------------- */ * { scrollbar-width: thin; /* Firefox */ scrollbar-color: rgba(0, 0, 0, 0.16) transparent; } *::-webkit-scrollbar { width: 8px; height: 8px; } *::-webkit-scrollbar-track { background-color: transparent; } *::-webkit-scrollbar-thumb { background-color: rgba(0, 0, 0, 0.16); border-radius: 999px; border: 2px solid transparent; background-clip: padding-box; } *::-webkit-scrollbar-thumb:hover { background-color: rgba(0, 0, 0, 0.34); } *::-webkit-scrollbar-corner { background-color: transparent; } /* -------------------------------------------------------------------------- Keyframes. A skeleton SHIMMERS, it does not pulse: a pulsing block reads as a rendering glitch, while a travelling highlight reads as progress. Astryx's own keeps its built-in treatment; this is for the hand-rolled placeholders. -------------------------------------------------------------------------- */ @keyframes dt-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } .dt-shimmer { background-image: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.65) 50%, transparent 100%); background-size: 200% 100%; animation: dt-shimmer 1.6s var(--dt-ease-in-out) infinite; } @keyframes dt-fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes dt-slide-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } .dt-fade-in { animation: dt-fade-in var(--dt-duration-base) var(--dt-ease-out); } .dt-slide-up { animation: dt-slide-up var(--dt-duration-slow) var(--dt-ease-out); } /* -------------------------------------------------------------------------- KPI / StatCard (Compact Enterprise Metric Strip). Static tiles stay quiet and static. Clickable/interactive tiles (e.g. on Tenants page) get a subtle 150ms hover lift and border emphasis. -------------------------------------------------------------------------- */ .dt-stat-card { transition: box-shadow 150ms ease, border-color 150ms ease, transform 150ms ease !important; } /* Interactive hover state ONLY when clickable or wrapped in button role */ .dt-stat-card.dt-stat-card-clickable:hover, .dt-stat-card[data-clickable='true']:hover, [role='button'] .dt-stat-card:hover, [role='button'] > .dt-stat-card:hover, button .dt-stat-card:hover { border-color: var(--color-border-emphasized, #cbd5e1) !important; box-shadow: 0 2px 6px rgba(15, 23, 42, 0.08) !important; transform: translateY(-1px); } .dt-stat-card.dt-stat-card-clickable:active, .dt-stat-card[data-clickable='true']:active, [role='button'] .dt-stat-card:active { transform: translateY(0); } /* Standardized 14px icon size inside the 26px chip across all call sites */ .dt-stat-card .dt-stat-icon svg { width: 14px !important; height: 14px !important; font-size: 14px !important; } /* -------------------------------------------------------------------------- Reduced motion. Handled ONCE, globally, rather than per component — a per-component opt-out is a rule that only covers the components somebody remembered. -------------------------------------------------------------------------- */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } }