/* Self-hosted fonts (R14) — @fontsource ships the .woff2 locally; Vite bundles them and serves them from our own origin. No Google Fonts / CDN request. */ @import '@fontsource/ibm-plex-sans/400.css'; @import '@fontsource/ibm-plex-sans/500.css'; @import '@fontsource/ibm-plex-sans/600.css'; @import '@fontsource/ibm-plex-sans/700.css'; @import '@fontsource/ibm-plex-mono/400.css'; @import '@fontsource/ibm-plex-mono/500.css'; @import '@fontsource/ibm-plex-mono/600.css'; /* No serif: a serif headline made every page read as a document, which is exactly the impression the redesign removes. One family, used assertively. */ /* Design tokens, then Tailwind v3 layers. */ @import './portal-tokens.css'; @import './admin-tokens.css'; @tailwind base; @tailwind components; @tailwind utilities; @layer base { html { font-family: var(--font-sans); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; /* Stops iOS inflating body text when the phone is turned to landscape. */ -webkit-text-size-adjust: 100%; } body { background: var(--bg); color: var(--text); font-size: var(--text-base); line-height: var(--lh-normal); } /* Uniform, visible focus for every interactive element (a11y §6.4/§9). */ :focus-visible { outline: none; box-shadow: var(--focus-ring); border-color: var(--accent); } /* Data/IDs/IPs use tabular figures. */ .font-mono { font-variant-numeric: tabular-nums; } /* Alpine: hide elements until initialised. */ [x-cloak] { display: none !important; } } /* iOS zooms the whole page when a field smaller than 16px takes focus, and * then leaves the page zoomed — every tap into a form throws the layout about. * * There is no way to decline it: the meta-viewport switch that used to suppress * it also kills pinch-zoom, which takes a genuine accessibility feature away * from everyone to fix a cosmetic problem for some. Making the fields 16px on * touch devices removes the reason instead. * * !important, and deliberately: this has to beat every text-size utility on * every field in the application, and a rule that loses to `text-sm` on one * form is worth nothing. Checkboxes and radios are excluded — they have no text * to inflate and sizing them here would break their box. */ @media (pointer: coarse) { input:not([type='checkbox']):not([type='radio']), select, textarea { font-size: 16px !important; } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; } } /* ── The public website ─────────────────────────────────────────────────── * The marketing page draws from the same tokens as the portal and the console * and differs only in rhythm. These four rules are what Tailwind cannot say: * a state class toggled by an observer, and the disclosure marker. */ .rv { opacity: 0; transform: translateY(16px); transition: opacity 0.55s var(--ease), transform 0.55s var(--ease); } .rv.in { opacity: 1; transform: none; } /* Staggered siblings — the delay belongs to the element, not to the observer, so a card that scrolls in alone still arrives on its own schedule. Kept short: a row of three cards that takes a second to finish assembling reads as a slow page, not as a considered one. */ .rv.d1 { transition-delay: 0.07s; } .rv.d2 { transition-delay: 0.14s; } .logline { opacity: 0; transform: translateX(-10px); transition: opacity 0.45s var(--ease), transform 0.45s var(--ease); } .logline.in { opacity: 1; transform: none; } /* The FAQ disclosure. Safari needs the -webkit- selector as well, and without `list-style: none` Firefox keeps drawing its own triangle beside ours. */ .faq summary { list-style: none; cursor: pointer; } .faq summary::-webkit-details-marker { display: none; } @media (prefers-reduced-motion: reduce) { .rv, .logline { opacity: 1 !important; transform: none !important; } } /* ── Metric visuals ─────────────────────────────────────────────────────── * The ring and the sparkline from the approved template. They live here * rather than as utilities because both need keyframes and a per-element * custom property, and Tailwind expresses neither. * * Every var() carries a fallback: an undefined custom property does not make * a browser skip the declaration, it computes to `unset` — which for a * stroke-dashoffset silently draws a full ring at every value. */ .metric-ring { transform: rotate(-90deg); } .metric-ring .track { fill: none; stroke: var(--surface-hover); stroke-width: 7; } .metric-ring .value { fill: none; stroke: var(--accent); stroke-width: 7; stroke-linecap: round; stroke-dasharray: var(--c, 157); stroke-dashoffset: var(--c, 157); animation: ring-sweep 1.1s cubic-bezier(.2, .7, .2, 1) .2s forwards; } @keyframes ring-sweep { to { stroke-dashoffset: var(--o, 0); } } .spark path { fill: none; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; } .spark .line { stroke: var(--accent); stroke-dasharray: 400; stroke-dashoffset: 400; animation: spark-draw 1.3s cubic-bezier(.4, .1, .2, 1) .2s forwards; } .spark.muted .line { stroke: var(--text-muted); } .spark .area { fill: var(--accent); stroke: none; opacity: 0; animation: spark-fade .8s ease .9s forwards; } @keyframes spark-draw { to { stroke-dashoffset: 0; } } @keyframes spark-fade { to { opacity: .09; } } @media (prefers-reduced-motion: reduce) { .metric-ring .value { stroke-dashoffset: var(--o, 0) !important; } .spark .line { stroke-dashoffset: 0 !important; } .spark .area { opacity: .09 !important; } }