mekosotto Claude Opus 4.7 (1M context) commited on
Commit
84572d9
·
1 Parent(s): 0079804

feat(frontend): premium motion layer — Apple HIG / Netflix transitions

Browse files

Adds 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>

Files changed (1) hide show
  1. src/frontend/app.py +205 -19
src/frontend/app.py CHANGED
@@ -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 {{ background: var(--ng-success); box-shadow: 0 0 8px var(--ng-success); }}
 
 
 
 
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 180ms ease, transform 120ms ease, box-shadow 180ms ease !important;
373
- box-shadow: 0 0 0 0 var(--ng-accent-ring);
 
 
 
374
  }}
375
  .stButton > button[kind="primary"]:hover {{
376
  background: var(--ng-accent-strong) !important;
377
- transform: translateY(-1px);
 
378
  }}
379
- .stButton > button[kind="primary"]:focus {{
380
- box-shadow: 0 0 0 3px var(--ng-accent-ring) !important;
 
 
 
 
 
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 180ms ease, background 180ms ease !important;
 
 
 
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 180ms ease, border-color 180ms ease !important;
 
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 150ms ease, box-shadow 150ms ease !important;
 
 
 
 
 
 
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: var(--ng-accent) !important;
 
 
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.01ms !important;
647
- transition-duration: 0.01ms !important;
 
 
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