feat(frontend): premium motion layer — Apple HIG / Netflix transitions
Browse filesAdds a unified motion system on top of the editorial color/typography
work. Goal: make every state change FEEL high-quality without becoming
distracting.
Motion tokens (CSS variables on :root):
- Easing: ease-out-expo (hero entrances), spring (verdict reveal),
MD-standard (micro-interactions), decel (enter), accel (exit)
- Durations: 80 / 180 / 240 / 360 / 640 ms — single rhythm everywhere
Keyframes:
- ng-fade-up (translate3d 14px → 0 + opacity 0 → 1)
- ng-fade-in (opacity)
- ng-scale-in (0.94 → 1 + opacity, used for the verdict drop)
- ng-pulse-dot (status dot "alive" pulse, healthy state only)
- ng-rise (translate 6px, used for sub-elements)
- ng-bar-grow (scaleX from 0, axis-left, for SHAP bars)
Component application:
- Hero strip: 640ms expo-out fade-up + radial accent glow ::before;
eyebrow / title / tagline / status-row stagger 80→300ms
- Decision card: 640ms drop-in + hover lift (-2px translate, shadow-lg
expansion + border-strong)
- Verdict-value: 640ms scale-in with spring overshoot (the "moment of
truth" beat)
- Signal rows: 240ms ng-rise with 50ms stagger (Apple Settings vibe)
- Section headers: ng-rise 240ms backwards
- Buttons (primary + secondary):
* Hover: translateY(-1px) + larger shadow
* Active: scale(0.97) press feedback in 80ms (no layout shift)
* Focus-visible: 3px accent ring (replaces generic :focus)
- Tabs: 240ms color/border transition; hover ghost underline
fade-in; tab-panel content cross-fades on switch
- Inputs: hover deepens border, focus expands accent ring smoothly
- Progress bar: width transition 640ms expo-out + accent gradient + halo
- Metrics: ng-fade-up 360ms + horizontal stagger (80/140/200ms) +
hover lift (-2px)
- Alerts: ng-fade-up 360ms (notification feel)
- Expanders: chevron rotates 90° on open, body ng-rise on reveal,
header hover deepens background
- Toast: ng-fade-up 360ms with spring easing
- Status dot (.is-ok): infinite 2.4s pulse-dot — system is "breathing"
- Page first paint: stApp children fade-in 240ms
Reduced-motion safety: media query hardened — sets duration to 0.001ms
for ALL animations + iterations capped at 1 + scroll-behavior auto.
Pulse infinite loop is properly disabled.
Will-change set on transformed elements (hero, card, button, metric)
to promote to GPU layer; no layout-thrashing properties animated.
184 tests still green; UserWarning gate clean; Streamlit boot HTTP 200.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/frontend/app.py +205 -19
|
@@ -110,6 +110,21 @@ def _build_css(theme: str) -> str:
|
|
| 110 |
--ng-radius-xl: 24px;
|
| 111 |
--ng-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 112 |
--ng-font-mono: 'JetBrains Mono', 'SF Mono', Menlo, monospace;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
}}
|
| 114 |
|
| 115 |
/* --- Global typography + canvas ----------------------------------------- */
|
|
@@ -131,6 +146,43 @@ main .block-container {{
|
|
| 131 |
max-width: 1200px;
|
| 132 |
}}
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
/* --- Hero / brand strip ------------------------------------------------- */
|
| 135 |
|
| 136 |
.hero {{
|
|
@@ -144,8 +196,36 @@ main .block-container {{
|
|
| 144 |
border: 1px solid var(--ng-border);
|
| 145 |
box-shadow: var(--ng-shadow-md);
|
| 146 |
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
| 147 |
}}
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
.hero::after {{
|
| 150 |
content: "";
|
| 151 |
position: absolute;
|
|
@@ -213,16 +293,24 @@ main .block-container {{
|
|
| 213 |
background: var(--ng-bg-elevated-3);
|
| 214 |
color: var(--ng-text-secondary);
|
| 215 |
border: 1px solid var(--ng-border);
|
|
|
|
|
|
|
| 216 |
}}
|
|
|
|
| 217 |
|
| 218 |
.dot::before {{
|
| 219 |
content: "";
|
| 220 |
width: 6px; height: 6px;
|
| 221 |
border-radius: 50%;
|
| 222 |
background: var(--ng-text-tertiary);
|
|
|
|
| 223 |
}}
|
| 224 |
|
| 225 |
-
.dot.is-ok::before {{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
.dot.is-warn::before {{ background: var(--ng-warning); }}
|
| 227 |
.dot.is-down::before {{ background: var(--ng-danger); }}
|
| 228 |
.dot.is-mute::before {{ background: var(--ng-text-tertiary); }}
|
|
@@ -233,6 +321,7 @@ main .block-container {{
|
|
| 233 |
margin: 2rem 0 1.5rem 0;
|
| 234 |
padding-bottom: 1.25rem;
|
| 235 |
border-bottom: 1px solid var(--ng-border);
|
|
|
|
| 236 |
}}
|
| 237 |
.section-eyebrow {{
|
| 238 |
font-family: var(--ng-font-mono);
|
|
@@ -268,6 +357,17 @@ main .block-container {{
|
|
| 268 |
padding: 1.6rem 1.75rem;
|
| 269 |
margin: 1.25rem 0;
|
| 270 |
box-shadow: var(--ng-shadow-md);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
}}
|
| 272 |
|
| 273 |
.provenance-strip {{
|
|
@@ -310,6 +410,8 @@ main .block-container {{
|
|
| 310 |
line-height: 1;
|
| 311 |
margin: 0;
|
| 312 |
font-feature-settings: "tnum" on, "lnum" on;
|
|
|
|
|
|
|
| 313 |
}}
|
| 314 |
.verdict-confidence {{
|
| 315 |
font-size: 1.1rem;
|
|
@@ -338,7 +440,13 @@ main .block-container {{
|
|
| 338 |
align-items: baseline;
|
| 339 |
font-size: 0.92rem;
|
| 340 |
line-height: 1.55;
|
|
|
|
|
|
|
| 341 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
.signal-key {{
|
| 343 |
font-family: var(--ng-font-mono);
|
| 344 |
font-size: 0.72rem;
|
|
@@ -369,15 +477,24 @@ main .block-container {{
|
|
| 369 |
padding: 0.6rem 1.4rem !important;
|
| 370 |
letter-spacing: 0.01em !important;
|
| 371 |
font-size: 0.92rem !important;
|
| 372 |
-
transition: background
|
| 373 |
-
|
|
|
|
|
|
|
|
|
|
| 374 |
}}
|
| 375 |
.stButton > button[kind="primary"]:hover {{
|
| 376 |
background: var(--ng-accent-strong) !important;
|
| 377 |
-
transform:
|
|
|
|
| 378 |
}}
|
| 379 |
-
.stButton > button[kind="primary"]:
|
| 380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
outline: none !important;
|
| 382 |
}}
|
| 383 |
|
|
@@ -389,11 +506,19 @@ main .block-container {{
|
|
| 389 |
border-radius: var(--ng-radius-sm) !important;
|
| 390 |
font-weight: 500 !important;
|
| 391 |
padding: 0.55rem 1.2rem !important;
|
| 392 |
-
transition: border-color
|
|
|
|
|
|
|
|
|
|
| 393 |
}}
|
| 394 |
.stButton > button:not([kind="primary"]):not([kind="primaryFormSubmit"]):hover {{
|
| 395 |
background: var(--ng-bg-elevated-3) !important;
|
| 396 |
border-color: var(--ng-accent) !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
}}
|
| 398 |
|
| 399 |
/* Tabs — left-aligned underline indicator (Apple/Netflix tab strip) */
|
|
@@ -409,19 +534,35 @@ main .block-container {{
|
|
| 409 |
padding: 0.85rem 1.4rem !important;
|
| 410 |
border-bottom: 2px solid transparent !important;
|
| 411 |
background: transparent !important;
|
| 412 |
-
transition: color
|
|
|
|
| 413 |
letter-spacing: -0.005em;
|
|
|
|
| 414 |
}}
|
| 415 |
.stTabs [data-baseweb="tab"]:hover {{
|
| 416 |
color: var(--ng-text-secondary) !important;
|
| 417 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
.stTabs [aria-selected="true"] {{
|
| 419 |
color: var(--ng-accent) !important;
|
| 420 |
border-bottom-color: var(--ng-accent) !important;
|
| 421 |
font-weight: 600 !important;
|
| 422 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
|
| 424 |
-
/* Inputs — flat with accent-on-focus border */
|
| 425 |
.stTextInput > div > div > input,
|
| 426 |
.stTextArea > div > div > textarea {{
|
| 427 |
background: var(--ng-bg-elevated-2) !important;
|
|
@@ -431,7 +572,13 @@ main .block-container {{
|
|
| 431 |
padding: 0.7rem 0.85rem !important;
|
| 432 |
font-family: var(--ng-font-sans) !important;
|
| 433 |
font-size: 0.95rem !important;
|
| 434 |
-
transition: border-color
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
}}
|
| 436 |
.stTextInput > div > div > input:focus,
|
| 437 |
.stTextArea > div > div > textarea:focus {{
|
|
@@ -457,24 +604,42 @@ main .block-container {{
|
|
| 457 |
background: var(--ng-accent) !important;
|
| 458 |
}}
|
| 459 |
|
| 460 |
-
/* Progress bar */
|
| 461 |
.stProgress > div > div > div > div {{
|
| 462 |
-
background:
|
|
|
|
|
|
|
| 463 |
border-radius: 999px !important;
|
|
|
|
|
|
|
| 464 |
}}
|
| 465 |
.stProgress > div > div > div {{
|
| 466 |
background: var(--ng-bg-elevated-3) !important;
|
| 467 |
border-radius: 999px !important;
|
| 468 |
}}
|
| 469 |
|
| 470 |
-
/* Metric cards (KPI strip) */
|
| 471 |
[data-testid="stMetric"] {{
|
| 472 |
background: var(--ng-bg-elevated) !important;
|
| 473 |
border: 1px solid var(--ng-border) !important;
|
| 474 |
border-radius: var(--ng-radius-md) !important;
|
| 475 |
padding: 1.4rem 1.5rem !important;
|
| 476 |
box-shadow: var(--ng-shadow-sm);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
[data-testid="stMetricLabel"] > div {{
|
| 479 |
color: var(--ng-text-tertiary) !important;
|
| 480 |
font-family: var(--ng-font-mono) !important;
|
|
@@ -502,13 +667,29 @@ main .block-container {{
|
|
| 502 |
line-height: 1.55 !important;
|
| 503 |
}}
|
| 504 |
|
| 505 |
-
/* Expander */
|
| 506 |
.streamlit-expanderHeader, [data-testid="stExpander"] details summary {{
|
| 507 |
background: var(--ng-bg-elevated-2) !important;
|
| 508 |
color: var(--ng-text-primary) !important;
|
| 509 |
border: 1px solid var(--ng-border) !important;
|
| 510 |
border-radius: var(--ng-radius-sm) !important;
|
| 511 |
font-weight: 500 !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
}}
|
| 513 |
[data-testid="stExpander"] {{
|
| 514 |
border: 1px solid var(--ng-border) !important;
|
|
@@ -534,6 +715,8 @@ code, pre {{
|
|
| 534 |
border-radius: var(--ng-radius-sm) !important;
|
| 535 |
color: var(--ng-text-primary) !important;
|
| 536 |
box-shadow: var(--ng-shadow-sm);
|
|
|
|
|
|
|
| 537 |
}}
|
| 538 |
[data-testid="stAlert"][data-baseweb="notification"][kind="info"] {{ border-left-color: var(--ng-accent); }}
|
| 539 |
[data-testid="stAlert"][data-baseweb="notification"][kind="warning"] {{ border-left-color: var(--ng-warning); }}
|
|
@@ -616,12 +799,13 @@ hr, [data-testid="stDivider"] {{
|
|
| 616 |
margin: 1.5rem 0 !important;
|
| 617 |
}}
|
| 618 |
|
| 619 |
-
/* Toast (st.toast) */
|
| 620 |
.stToast {{
|
| 621 |
background: var(--ng-bg-elevated) !important;
|
| 622 |
color: var(--ng-text-primary) !important;
|
| 623 |
border: 1px solid var(--ng-border) !important;
|
| 624 |
box-shadow: var(--ng-shadow-lg) !important;
|
|
|
|
| 625 |
}}
|
| 626 |
|
| 627 |
/* Chart container — quiet frame */
|
|
@@ -640,11 +824,13 @@ hr, [data-testid="stDivider"] {{
|
|
| 640 |
padding: 1rem;
|
| 641 |
}}
|
| 642 |
|
| 643 |
-
/* Reduced motion */
|
| 644 |
@media (prefers-reduced-motion: reduce) {{
|
| 645 |
-
* {{
|
| 646 |
-
animation-duration: 0.
|
| 647 |
-
|
|
|
|
|
|
|
| 648 |
}}
|
| 649 |
}}
|
| 650 |
|
|
|
|
| 110 |
--ng-radius-xl: 24px;
|
| 111 |
--ng-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 112 |
--ng-font-mono: 'JetBrains Mono', 'SF Mono', Menlo, monospace;
|
| 113 |
+
/* Motion tokens — Apple HIG fluid-physics + Material standard mix */
|
| 114 |
+
--ng-ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* expo-out, hero entrances */
|
| 115 |
+
--ng-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* gentle overshoot, verdict reveal */
|
| 116 |
+
--ng-ease-standard: cubic-bezier(0.4, 0, 0.2, 1); /* MD standard, micro-interactions */
|
| 117 |
+
--ng-ease-decel: cubic-bezier(0, 0, 0.2, 1); /* enter from off-screen */
|
| 118 |
+
--ng-ease-accel: cubic-bezier(0.4, 0, 1, 1); /* exit to off-screen */
|
| 119 |
+
--ng-dur-instant: 80ms;
|
| 120 |
+
--ng-dur-fast: 180ms;
|
| 121 |
+
--ng-dur-base: 240ms;
|
| 122 |
+
--ng-dur-slow: 360ms;
|
| 123 |
+
--ng-dur-hero: 640ms;
|
| 124 |
+
}}
|
| 125 |
+
|
| 126 |
+
html {{
|
| 127 |
+
scroll-behavior: smooth;
|
| 128 |
}}
|
| 129 |
|
| 130 |
/* --- Global typography + canvas ----------------------------------------- */
|
|
|
|
| 146 |
max-width: 1200px;
|
| 147 |
}}
|
| 148 |
|
| 149 |
+
/* --- Premium motion keyframes ------------------------------------------ */
|
| 150 |
+
|
| 151 |
+
@keyframes ng-fade-up {{
|
| 152 |
+
from {{ opacity: 0; transform: translate3d(0, 14px, 0); }}
|
| 153 |
+
to {{ opacity: 1; transform: translate3d(0, 0, 0); }}
|
| 154 |
+
}}
|
| 155 |
+
@keyframes ng-fade-in {{
|
| 156 |
+
from {{ opacity: 0; }}
|
| 157 |
+
to {{ opacity: 1; }}
|
| 158 |
+
}}
|
| 159 |
+
@keyframes ng-scale-in {{
|
| 160 |
+
from {{ opacity: 0; transform: scale(0.94); }}
|
| 161 |
+
to {{ opacity: 1; transform: scale(1); }}
|
| 162 |
+
}}
|
| 163 |
+
@keyframes ng-pulse-dot {{
|
| 164 |
+
0% {{ box-shadow: 0 0 0 0 var(--ng-success), 0 0 8px var(--ng-success); }}
|
| 165 |
+
70% {{ box-shadow: 0 0 0 6px transparent, 0 0 8px var(--ng-success); }}
|
| 166 |
+
100% {{ box-shadow: 0 0 0 0 transparent, 0 0 8px var(--ng-success); }}
|
| 167 |
+
}}
|
| 168 |
+
@keyframes ng-shimmer {{
|
| 169 |
+
0% {{ background-position: -200% 0; }}
|
| 170 |
+
100% {{ background-position: 200% 0; }}
|
| 171 |
+
}}
|
| 172 |
+
@keyframes ng-bar-grow {{
|
| 173 |
+
from {{ transform: scaleX(0); }}
|
| 174 |
+
to {{ transform: scaleX(1); }}
|
| 175 |
+
}}
|
| 176 |
+
@keyframes ng-rise {{
|
| 177 |
+
from {{ opacity: 0; transform: translate3d(0, 6px, 0); }}
|
| 178 |
+
to {{ opacity: 1; transform: translate3d(0, 0, 0); }}
|
| 179 |
+
}}
|
| 180 |
+
|
| 181 |
+
/* Page first-paint — soft fade-in over the whole canvas */
|
| 182 |
+
.stApp > div {{
|
| 183 |
+
animation: ng-fade-in var(--ng-dur-base) var(--ng-ease-out) backwards;
|
| 184 |
+
}}
|
| 185 |
+
|
| 186 |
/* --- Hero / brand strip ------------------------------------------------- */
|
| 187 |
|
| 188 |
.hero {{
|
|
|
|
| 196 |
border: 1px solid var(--ng-border);
|
| 197 |
box-shadow: var(--ng-shadow-md);
|
| 198 |
overflow: hidden;
|
| 199 |
+
/* Premium entrance: subtle drop with expo-out easing (Apple-style) */
|
| 200 |
+
animation: ng-fade-up var(--ng-dur-hero) var(--ng-ease-out) backwards;
|
| 201 |
+
will-change: transform, opacity;
|
| 202 |
}}
|
| 203 |
|
| 204 |
+
.hero::before {{
|
| 205 |
+
/* Diffuse warm glow that never fully resolves — adds depth without noise */
|
| 206 |
+
content: "";
|
| 207 |
+
position: absolute;
|
| 208 |
+
top: -40%; right: -20%;
|
| 209 |
+
width: 60%; height: 200%;
|
| 210 |
+
background: radial-gradient(ellipse at center,
|
| 211 |
+
var(--ng-accent-soft) 0%,
|
| 212 |
+
transparent 60%);
|
| 213 |
+
pointer-events: none;
|
| 214 |
+
opacity: 0.7;
|
| 215 |
+
}}
|
| 216 |
+
|
| 217 |
+
.hero-eyebrow,
|
| 218 |
+
.hero-title,
|
| 219 |
+
.hero-tagline,
|
| 220 |
+
.hero-status-row {{
|
| 221 |
+
position: relative; /* sit above ::before */
|
| 222 |
+
}}
|
| 223 |
+
|
| 224 |
+
.hero-eyebrow {{ animation: ng-rise var(--ng-dur-slow) var(--ng-ease-out) 80ms backwards; }}
|
| 225 |
+
.hero-title {{ animation: ng-rise var(--ng-dur-slow) var(--ng-ease-out) 140ms backwards; }}
|
| 226 |
+
.hero-tagline {{ animation: ng-rise var(--ng-dur-slow) var(--ng-ease-out) 220ms backwards; }}
|
| 227 |
+
.hero-status-row {{ animation: ng-rise var(--ng-dur-slow) var(--ng-ease-out) 300ms backwards; }}
|
| 228 |
+
|
| 229 |
.hero::after {{
|
| 230 |
content: "";
|
| 231 |
position: absolute;
|
|
|
|
| 293 |
background: var(--ng-bg-elevated-3);
|
| 294 |
color: var(--ng-text-secondary);
|
| 295 |
border: 1px solid var(--ng-border);
|
| 296 |
+
transition: border-color var(--ng-dur-fast) var(--ng-ease-standard),
|
| 297 |
+
background var(--ng-dur-fast) var(--ng-ease-standard);
|
| 298 |
}}
|
| 299 |
+
.dot:hover {{ border-color: var(--ng-border-strong); }}
|
| 300 |
|
| 301 |
.dot::before {{
|
| 302 |
content: "";
|
| 303 |
width: 6px; height: 6px;
|
| 304 |
border-radius: 50%;
|
| 305 |
background: var(--ng-text-tertiary);
|
| 306 |
+
transition: background var(--ng-dur-base) var(--ng-ease-standard);
|
| 307 |
}}
|
| 308 |
|
| 309 |
+
.dot.is-ok::before {{
|
| 310 |
+
background: var(--ng-success);
|
| 311 |
+
/* Subtle "alive" pulse — only on healthy state, says system is breathing */
|
| 312 |
+
animation: ng-pulse-dot 2.4s var(--ng-ease-standard) infinite;
|
| 313 |
+
}}
|
| 314 |
.dot.is-warn::before {{ background: var(--ng-warning); }}
|
| 315 |
.dot.is-down::before {{ background: var(--ng-danger); }}
|
| 316 |
.dot.is-mute::before {{ background: var(--ng-text-tertiary); }}
|
|
|
|
| 321 |
margin: 2rem 0 1.5rem 0;
|
| 322 |
padding-bottom: 1.25rem;
|
| 323 |
border-bottom: 1px solid var(--ng-border);
|
| 324 |
+
animation: ng-rise var(--ng-dur-base) var(--ng-ease-out) backwards;
|
| 325 |
}}
|
| 326 |
.section-eyebrow {{
|
| 327 |
font-family: var(--ng-font-mono);
|
|
|
|
| 357 |
padding: 1.6rem 1.75rem;
|
| 358 |
margin: 1.25rem 0;
|
| 359 |
box-shadow: var(--ng-shadow-md);
|
| 360 |
+
/* Drop-in reveal — starts slightly below + faded, settles with expo-out */
|
| 361 |
+
animation: ng-fade-up var(--ng-dur-hero) var(--ng-ease-out) backwards;
|
| 362 |
+
transition: border-color var(--ng-dur-base) var(--ng-ease-standard),
|
| 363 |
+
box-shadow var(--ng-dur-base) var(--ng-ease-standard),
|
| 364 |
+
transform var(--ng-dur-base) var(--ng-ease-standard);
|
| 365 |
+
will-change: transform;
|
| 366 |
+
}}
|
| 367 |
+
.card:hover {{
|
| 368 |
+
border-color: var(--ng-border-strong);
|
| 369 |
+
box-shadow: var(--ng-shadow-lg);
|
| 370 |
+
transform: translate3d(0, -2px, 0);
|
| 371 |
}}
|
| 372 |
|
| 373 |
.provenance-strip {{
|
|
|
|
| 410 |
line-height: 1;
|
| 411 |
margin: 0;
|
| 412 |
font-feature-settings: "tnum" on, "lnum" on;
|
| 413 |
+
/* Verdict reveal — gentle spring overshoot, the "moment of truth" */
|
| 414 |
+
animation: ng-scale-in var(--ng-dur-hero) var(--ng-ease-spring) 120ms backwards;
|
| 415 |
}}
|
| 416 |
.verdict-confidence {{
|
| 417 |
font-size: 1.1rem;
|
|
|
|
| 440 |
align-items: baseline;
|
| 441 |
font-size: 0.92rem;
|
| 442 |
line-height: 1.55;
|
| 443 |
+
/* Stagger entry — 50ms between rows, very Apple Settings */
|
| 444 |
+
animation: ng-rise var(--ng-dur-base) var(--ng-ease-out) backwards;
|
| 445 |
}}
|
| 446 |
+
.signal-row:nth-child(1) {{ animation-delay: 280ms; }}
|
| 447 |
+
.signal-row:nth-child(2) {{ animation-delay: 330ms; }}
|
| 448 |
+
.signal-row:nth-child(3) {{ animation-delay: 380ms; }}
|
| 449 |
+
.signal-row:nth-child(4) {{ animation-delay: 430ms; }}
|
| 450 |
.signal-key {{
|
| 451 |
font-family: var(--ng-font-mono);
|
| 452 |
font-size: 0.72rem;
|
|
|
|
| 477 |
padding: 0.6rem 1.4rem !important;
|
| 478 |
letter-spacing: 0.01em !important;
|
| 479 |
font-size: 0.92rem !important;
|
| 480 |
+
transition: background var(--ng-dur-fast) var(--ng-ease-standard),
|
| 481 |
+
transform var(--ng-dur-fast) var(--ng-ease-out),
|
| 482 |
+
box-shadow var(--ng-dur-fast) var(--ng-ease-standard) !important;
|
| 483 |
+
box-shadow: 0 0 0 0 var(--ng-accent-ring), var(--ng-shadow-sm);
|
| 484 |
+
will-change: transform;
|
| 485 |
}}
|
| 486 |
.stButton > button[kind="primary"]:hover {{
|
| 487 |
background: var(--ng-accent-strong) !important;
|
| 488 |
+
transform: translate3d(0, -1px, 0);
|
| 489 |
+
box-shadow: 0 0 0 0 var(--ng-accent-ring), var(--ng-shadow-md) !important;
|
| 490 |
}}
|
| 491 |
+
.stButton > button[kind="primary"]:active {{
|
| 492 |
+
/* Apple-style press: brief downward scale, no layout shift */
|
| 493 |
+
transform: translate3d(0, 0, 0) scale(0.97) !important;
|
| 494 |
+
transition-duration: var(--ng-dur-instant) !important;
|
| 495 |
+
}}
|
| 496 |
+
.stButton > button[kind="primary"]:focus-visible {{
|
| 497 |
+
box-shadow: 0 0 0 3px var(--ng-accent-ring), var(--ng-shadow-sm) !important;
|
| 498 |
outline: none !important;
|
| 499 |
}}
|
| 500 |
|
|
|
|
| 506 |
border-radius: var(--ng-radius-sm) !important;
|
| 507 |
font-weight: 500 !important;
|
| 508 |
padding: 0.55rem 1.2rem !important;
|
| 509 |
+
transition: border-color var(--ng-dur-fast) var(--ng-ease-standard),
|
| 510 |
+
background var(--ng-dur-fast) var(--ng-ease-standard),
|
| 511 |
+
transform var(--ng-dur-fast) var(--ng-ease-out) !important;
|
| 512 |
+
will-change: transform;
|
| 513 |
}}
|
| 514 |
.stButton > button:not([kind="primary"]):not([kind="primaryFormSubmit"]):hover {{
|
| 515 |
background: var(--ng-bg-elevated-3) !important;
|
| 516 |
border-color: var(--ng-accent) !important;
|
| 517 |
+
transform: translate3d(0, -1px, 0);
|
| 518 |
+
}}
|
| 519 |
+
.stButton > button:not([kind="primary"]):not([kind="primaryFormSubmit"]):active {{
|
| 520 |
+
transform: translate3d(0, 0, 0) scale(0.97) !important;
|
| 521 |
+
transition-duration: var(--ng-dur-instant) !important;
|
| 522 |
}}
|
| 523 |
|
| 524 |
/* Tabs — left-aligned underline indicator (Apple/Netflix tab strip) */
|
|
|
|
| 534 |
padding: 0.85rem 1.4rem !important;
|
| 535 |
border-bottom: 2px solid transparent !important;
|
| 536 |
background: transparent !important;
|
| 537 |
+
transition: color var(--ng-dur-base) var(--ng-ease-standard),
|
| 538 |
+
border-color var(--ng-dur-base) var(--ng-ease-out) !important;
|
| 539 |
letter-spacing: -0.005em;
|
| 540 |
+
position: relative;
|
| 541 |
}}
|
| 542 |
.stTabs [data-baseweb="tab"]:hover {{
|
| 543 |
color: var(--ng-text-secondary) !important;
|
| 544 |
}}
|
| 545 |
+
/* The hover "ghost" underline — only visible while hovering an inactive tab */
|
| 546 |
+
.stTabs [data-baseweb="tab"]:not([aria-selected="true"]):hover::after {{
|
| 547 |
+
content: "";
|
| 548 |
+
position: absolute;
|
| 549 |
+
left: 1.4rem; right: 1.4rem; bottom: -1px;
|
| 550 |
+
height: 2px;
|
| 551 |
+
background: var(--ng-text-tertiary);
|
| 552 |
+
opacity: 0.4;
|
| 553 |
+
animation: ng-fade-in var(--ng-dur-fast) var(--ng-ease-out);
|
| 554 |
+
}}
|
| 555 |
.stTabs [aria-selected="true"] {{
|
| 556 |
color: var(--ng-accent) !important;
|
| 557 |
border-bottom-color: var(--ng-accent) !important;
|
| 558 |
font-weight: 600 !important;
|
| 559 |
}}
|
| 560 |
+
/* Tab content cross-fades on switch */
|
| 561 |
+
.stTabs [data-baseweb="tab-panel"] {{
|
| 562 |
+
animation: ng-fade-in var(--ng-dur-base) var(--ng-ease-out) backwards;
|
| 563 |
+
}}
|
| 564 |
|
| 565 |
+
/* Inputs — flat with accent-on-focus border + smooth ring expansion */
|
| 566 |
.stTextInput > div > div > input,
|
| 567 |
.stTextArea > div > div > textarea {{
|
| 568 |
background: var(--ng-bg-elevated-2) !important;
|
|
|
|
| 572 |
padding: 0.7rem 0.85rem !important;
|
| 573 |
font-family: var(--ng-font-sans) !important;
|
| 574 |
font-size: 0.95rem !important;
|
| 575 |
+
transition: border-color var(--ng-dur-fast) var(--ng-ease-standard),
|
| 576 |
+
box-shadow var(--ng-dur-base) var(--ng-ease-out),
|
| 577 |
+
background var(--ng-dur-fast) var(--ng-ease-standard) !important;
|
| 578 |
+
}}
|
| 579 |
+
.stTextInput > div > div > input:hover,
|
| 580 |
+
.stTextArea > div > div > textarea:hover {{
|
| 581 |
+
border-color: var(--ng-border-strong) !important;
|
| 582 |
}}
|
| 583 |
.stTextInput > div > div > input:focus,
|
| 584 |
.stTextArea > div > div > textarea:focus {{
|
|
|
|
| 604 |
background: var(--ng-accent) !important;
|
| 605 |
}}
|
| 606 |
|
| 607 |
+
/* Progress bar — fill animates from 0 with expo-out (the "filling up" beat) */
|
| 608 |
.stProgress > div > div > div > div {{
|
| 609 |
+
background: linear-gradient(90deg,
|
| 610 |
+
var(--ng-accent) 0%,
|
| 611 |
+
var(--ng-accent-strong) 100%) !important;
|
| 612 |
border-radius: 999px !important;
|
| 613 |
+
transition: width var(--ng-dur-hero) var(--ng-ease-out) !important;
|
| 614 |
+
box-shadow: 0 0 16px var(--ng-accent-ring);
|
| 615 |
}}
|
| 616 |
.stProgress > div > div > div {{
|
| 617 |
background: var(--ng-bg-elevated-3) !important;
|
| 618 |
border-radius: 999px !important;
|
| 619 |
}}
|
| 620 |
|
| 621 |
+
/* Metric cards (KPI strip) — drop in with subtle stagger */
|
| 622 |
[data-testid="stMetric"] {{
|
| 623 |
background: var(--ng-bg-elevated) !important;
|
| 624 |
border: 1px solid var(--ng-border) !important;
|
| 625 |
border-radius: var(--ng-radius-md) !important;
|
| 626 |
padding: 1.4rem 1.5rem !important;
|
| 627 |
box-shadow: var(--ng-shadow-sm);
|
| 628 |
+
animation: ng-fade-up var(--ng-dur-slow) var(--ng-ease-out) backwards;
|
| 629 |
+
transition: border-color var(--ng-dur-base) var(--ng-ease-standard),
|
| 630 |
+
box-shadow var(--ng-dur-base) var(--ng-ease-standard),
|
| 631 |
+
transform var(--ng-dur-base) var(--ng-ease-standard);
|
| 632 |
+
will-change: transform;
|
| 633 |
+
}}
|
| 634 |
+
[data-testid="stMetric"]:hover {{
|
| 635 |
+
border-color: var(--ng-border-strong) !important;
|
| 636 |
+
box-shadow: var(--ng-shadow-md);
|
| 637 |
+
transform: translate3d(0, -2px, 0);
|
| 638 |
}}
|
| 639 |
+
/* Stagger when 3 metrics sit side-by-side */
|
| 640 |
+
[data-testid="stHorizontalBlock"] [data-testid="stMetric"]:nth-child(1) {{ animation-delay: 80ms; }}
|
| 641 |
+
[data-testid="stHorizontalBlock"] [data-testid="stMetric"]:nth-child(2) {{ animation-delay: 140ms; }}
|
| 642 |
+
[data-testid="stHorizontalBlock"] [data-testid="stMetric"]:nth-child(3) {{ animation-delay: 200ms; }}
|
| 643 |
[data-testid="stMetricLabel"] > div {{
|
| 644 |
color: var(--ng-text-tertiary) !important;
|
| 645 |
font-family: var(--ng-font-mono) !important;
|
|
|
|
| 667 |
line-height: 1.55 !important;
|
| 668 |
}}
|
| 669 |
|
| 670 |
+
/* Expander — chevron rotates smoothly + body cross-fades */
|
| 671 |
.streamlit-expanderHeader, [data-testid="stExpander"] details summary {{
|
| 672 |
background: var(--ng-bg-elevated-2) !important;
|
| 673 |
color: var(--ng-text-primary) !important;
|
| 674 |
border: 1px solid var(--ng-border) !important;
|
| 675 |
border-radius: var(--ng-radius-sm) !important;
|
| 676 |
font-weight: 500 !important;
|
| 677 |
+
transition: background var(--ng-dur-fast) var(--ng-ease-standard),
|
| 678 |
+
border-color var(--ng-dur-fast) var(--ng-ease-standard) !important;
|
| 679 |
+
cursor: pointer;
|
| 680 |
+
}}
|
| 681 |
+
[data-testid="stExpander"] details summary:hover {{
|
| 682 |
+
background: var(--ng-bg-elevated-3) !important;
|
| 683 |
+
border-color: var(--ng-border-strong) !important;
|
| 684 |
+
}}
|
| 685 |
+
[data-testid="stExpander"] details summary svg {{
|
| 686 |
+
transition: transform var(--ng-dur-base) var(--ng-ease-out) !important;
|
| 687 |
+
}}
|
| 688 |
+
[data-testid="stExpander"] details[open] summary svg {{
|
| 689 |
+
transform: rotate(90deg);
|
| 690 |
+
}}
|
| 691 |
+
[data-testid="stExpander"] details[open] > div {{
|
| 692 |
+
animation: ng-rise var(--ng-dur-base) var(--ng-ease-out);
|
| 693 |
}}
|
| 694 |
[data-testid="stExpander"] {{
|
| 695 |
border: 1px solid var(--ng-border) !important;
|
|
|
|
| 715 |
border-radius: var(--ng-radius-sm) !important;
|
| 716 |
color: var(--ng-text-primary) !important;
|
| 717 |
box-shadow: var(--ng-shadow-sm);
|
| 718 |
+
/* Slide-down + fade — alerts feel like notifications dropping in */
|
| 719 |
+
animation: ng-fade-up var(--ng-dur-slow) var(--ng-ease-out) backwards;
|
| 720 |
}}
|
| 721 |
[data-testid="stAlert"][data-baseweb="notification"][kind="info"] {{ border-left-color: var(--ng-accent); }}
|
| 722 |
[data-testid="stAlert"][data-baseweb="notification"][kind="warning"] {{ border-left-color: var(--ng-warning); }}
|
|
|
|
| 799 |
margin: 1.5rem 0 !important;
|
| 800 |
}}
|
| 801 |
|
| 802 |
+
/* Toast (st.toast) — slide in from bottom-right, expo-out */
|
| 803 |
.stToast {{
|
| 804 |
background: var(--ng-bg-elevated) !important;
|
| 805 |
color: var(--ng-text-primary) !important;
|
| 806 |
border: 1px solid var(--ng-border) !important;
|
| 807 |
box-shadow: var(--ng-shadow-lg) !important;
|
| 808 |
+
animation: ng-fade-up var(--ng-dur-slow) var(--ng-ease-spring) backwards;
|
| 809 |
}}
|
| 810 |
|
| 811 |
/* Chart container — quiet frame */
|
|
|
|
| 824 |
padding: 1rem;
|
| 825 |
}}
|
| 826 |
|
| 827 |
+
/* Reduced motion — fully disable animations + cap transitions to a flash */
|
| 828 |
@media (prefers-reduced-motion: reduce) {{
|
| 829 |
+
*, *::before, *::after {{
|
| 830 |
+
animation-duration: 0.001ms !important;
|
| 831 |
+
animation-iteration-count: 1 !important;
|
| 832 |
+
transition-duration: 0.001ms !important;
|
| 833 |
+
scroll-behavior: auto !important;
|
| 834 |
}}
|
| 835 |
}}
|
| 836 |
|