@dillingerstaffing/strand-svelte 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css/strand-ui.css +2796 -222
- package/dist/index.js +1507 -1454
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/CodeBlock/CodeBlock.svelte +77 -1
- package/src/components/InstrumentViewport/InstrumentViewport.svelte +5 -0
- package/src/components/InstrumentViewport/InstrumentViewport.test.ts +12 -0
- package/src/components/Link/Link.svelte +5 -1
- package/src/components/Link/Link.test.ts +10 -0
- package/src/components/Nav/Nav.svelte +5 -1
- package/src/components/Nav/Nav.test.ts +5 -0
- package/src/components/Section/Section.svelte +4 -1
- package/src/components/Section/Section.test.ts +10 -0
package/dist/css/strand-ui.css
CHANGED
|
@@ -228,6 +228,115 @@
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
|
|
231
|
+
/* Banner */
|
|
232
|
+
/*! Strand UI | MIT License | dillingerstaffing.com */
|
|
233
|
+
|
|
234
|
+
/*! Strand UI | MIT License | dillingerstaffing.com */
|
|
235
|
+
|
|
236
|
+
/* ── Banner (DL Part XI.6: page-level persistent alert) ──
|
|
237
|
+
Fixed to top of viewport. Pushes page content down.
|
|
238
|
+
System-wide status: maintenance, announcements, warnings. */
|
|
239
|
+
.strand-banner {
|
|
240
|
+
position: fixed;
|
|
241
|
+
top: 0;
|
|
242
|
+
left: 0;
|
|
243
|
+
right: 0;
|
|
244
|
+
z-index: 101;
|
|
245
|
+
padding: var(--strand-space-3) var(--strand-space-4);
|
|
246
|
+
font-family: var(--strand-font-sans);
|
|
247
|
+
font-size: var(--strand-text-sm);
|
|
248
|
+
line-height: var(--strand-leading-normal);
|
|
249
|
+
text-align: center;
|
|
250
|
+
border-bottom: 1px solid transparent;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* Push nav down when banner is present.
|
|
254
|
+
Two cases handled here:
|
|
255
|
+
- .strand-nav--glass is position:fixed, so it must be offset via top.
|
|
256
|
+
- All other nav variants (default, --instrument) are in document flow,
|
|
257
|
+
so they shift via margin-top instead. The fixed-positioning rule above
|
|
258
|
+
wins for --glass because top is meaningful only when position:fixed,
|
|
259
|
+
and margin-top has no effect on a fixed element. */
|
|
260
|
+
.strand-banner ~ .strand-nav--glass {
|
|
261
|
+
top: var(--strand-banner-height, 0px);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.strand-banner ~ .strand-nav:not(.strand-nav--glass) {
|
|
265
|
+
margin-top: var(--strand-banner-height, 0px);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* When the page hosts a full-bleed instrument viewport AND a banner is
|
|
269
|
+
present, the viewport height calculation must subtract the banner
|
|
270
|
+
height as well as the nav height. The viewport is a descendant of body,
|
|
271
|
+
not a sibling of .strand-banner, so :has() is required to scope this
|
|
272
|
+
rule to "any document containing a banner". */
|
|
273
|
+
body:has(.strand-banner) .strand-instrument-viewport--full-bleed {
|
|
274
|
+
height: calc(100vh - var(--strand-nav-height) - var(--strand-banner-height, 0px));
|
|
275
|
+
height: calc(100dvh - var(--strand-nav-height) - var(--strand-banner-height, 0px));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* ── Variants ── */
|
|
279
|
+
.strand-banner--info {
|
|
280
|
+
background: var(--strand-blue-glow);
|
|
281
|
+
color: var(--strand-blue-deep);
|
|
282
|
+
border-bottom-color: var(--strand-blue-indicator);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.strand-banner--warning {
|
|
286
|
+
background: var(--strand-amber-tint);
|
|
287
|
+
color: var(--strand-on-amber-tint);
|
|
288
|
+
border-bottom-color: var(--strand-amber-caution);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.strand-banner--critical {
|
|
292
|
+
background: var(--strand-red-tint);
|
|
293
|
+
color: var(--strand-on-red-tint);
|
|
294
|
+
border-bottom-color: var(--strand-red-alert);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* ── Content ── */
|
|
298
|
+
.strand-banner__text {
|
|
299
|
+
margin: 0;
|
|
300
|
+
font-weight: var(--strand-weight-medium);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* ── Dismiss button ── */
|
|
304
|
+
.strand-banner__dismiss {
|
|
305
|
+
position: absolute;
|
|
306
|
+
right: var(--strand-space-4);
|
|
307
|
+
top: 50%;
|
|
308
|
+
transform: translateY(-50%);
|
|
309
|
+
background: none;
|
|
310
|
+
border: none;
|
|
311
|
+
cursor: pointer;
|
|
312
|
+
color: inherit;
|
|
313
|
+
opacity: 0.6;
|
|
314
|
+
padding: var(--strand-space-2);
|
|
315
|
+
min-width: 44px;
|
|
316
|
+
min-height: 44px;
|
|
317
|
+
display: inline-flex;
|
|
318
|
+
align-items: center;
|
|
319
|
+
justify-content: center;
|
|
320
|
+
border-radius: var(--strand-radius-md);
|
|
321
|
+
transition: opacity var(--strand-duration-fast) ease;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.strand-banner__dismiss:hover {
|
|
325
|
+
opacity: 1;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.strand-banner__dismiss:focus-visible {
|
|
329
|
+
outline: 2px solid currentColor;
|
|
330
|
+
outline-offset: 2px;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
@media (prefers-reduced-motion: reduce) {
|
|
334
|
+
.strand-banner__dismiss {
|
|
335
|
+
transition: none;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
|
|
231
340
|
/* Breadcrumb */
|
|
232
341
|
/*! Strand UI | MIT License | dillingerstaffing.com */
|
|
233
342
|
|
|
@@ -356,6 +465,16 @@
|
|
|
356
465
|
min-width: 48px;
|
|
357
466
|
}
|
|
358
467
|
|
|
468
|
+
/* ── Circular modifier (icon-only only) ──
|
|
469
|
+
Combine with strand-btn--icon-only + a size modifier for a
|
|
470
|
+
pill/circle button. Use for inline rating stars, avatar
|
|
471
|
+
actions, or reaction chips where a square outline would
|
|
472
|
+
feel harsh. */
|
|
473
|
+
.strand-btn--circular {
|
|
474
|
+
border-radius: var(--strand-radius-full);
|
|
475
|
+
aspect-ratio: 1;
|
|
476
|
+
}
|
|
477
|
+
|
|
359
478
|
/* ── Full width ── */
|
|
360
479
|
.strand-btn--full-width {
|
|
361
480
|
width: 100%;
|
|
@@ -431,6 +550,13 @@
|
|
|
431
550
|
pointer-events: none;
|
|
432
551
|
}
|
|
433
552
|
|
|
553
|
+
/* When the loading spinner is active, hide the label so only
|
|
554
|
+
the spinner is visible. We use visibility (not display) to
|
|
555
|
+
keep the button width stable so the spinner does not jump. */
|
|
556
|
+
.strand-btn--loading .strand-btn__content {
|
|
557
|
+
visibility: hidden;
|
|
558
|
+
}
|
|
559
|
+
|
|
434
560
|
.strand-btn__spinner {
|
|
435
561
|
position: absolute;
|
|
436
562
|
width: 16px;
|
|
@@ -488,10 +614,17 @@
|
|
|
488
614
|
|
|
489
615
|
/*! Strand UI | MIT License | dillingerstaffing.com */
|
|
490
616
|
|
|
491
|
-
/*
|
|
617
|
+
/* CF-REF: craft-test.md [17]
|
|
618
|
+
DL Principle 8 (Default Philosophy): A card at default settings must look
|
|
619
|
+
like a glass instrument panel, not a blank rectangle. The base includes
|
|
620
|
+
elevation, border, and padding so zero-customization output is premium.
|
|
621
|
+
Craft test: place next to a Bootstrap/Material card. Must be visibly more refined. */
|
|
492
622
|
.strand-card {
|
|
493
|
-
border-radius: var(--strand-radius-lg);
|
|
623
|
+
border-radius: var(--strand-radius-lg); /* CF-REF: radius-range.md [1] */
|
|
494
624
|
background: var(--strand-surface-elevated);
|
|
625
|
+
border: 1px solid var(--strand-border-subtle, rgba(148, 163, 184, 0.12));
|
|
626
|
+
box-shadow: var(--strand-elevation-1); /* CF-REF: earned-elevation.md [1] */
|
|
627
|
+
padding: var(--strand-space-6); /* CF-REF: padding-tiers.md [1] */
|
|
495
628
|
font-family: var(--strand-font-sans);
|
|
496
629
|
overflow: hidden;
|
|
497
630
|
box-sizing: border-box;
|
|
@@ -499,31 +632,69 @@
|
|
|
499
632
|
}
|
|
500
633
|
|
|
501
634
|
/* ── Variants ── */
|
|
502
|
-
.strand-card--elevated {
|
|
503
|
-
border: 1px solid var(--strand-border-subtle);
|
|
504
|
-
box-shadow: var(--strand-elevation-1);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
635
|
.strand-card--outlined {
|
|
508
636
|
box-shadow: none;
|
|
509
637
|
border: 1px solid var(--strand-gray-200);
|
|
510
638
|
}
|
|
511
639
|
|
|
640
|
+
.strand-card--flat {
|
|
641
|
+
box-shadow: none;
|
|
642
|
+
border: none;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/* ── Warm variant (DL Part 7.1b) ──
|
|
646
|
+
Showcase contexts where the warm wood / controlled daylight
|
|
647
|
+
spatial signature should be felt. Adds the warm shadow tint
|
|
648
|
+
alongside the existing cool layers. Default cool variant
|
|
649
|
+
stays the default. Use on hero/landing/gallery cards, never
|
|
650
|
+
on dashboard or readout cards. */
|
|
651
|
+
.strand-card--warm {
|
|
652
|
+
box-shadow: var(--strand-elevation-1-warm);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.strand-card--warm.strand-card--interactive:hover {
|
|
656
|
+
box-shadow: var(--strand-elevation-2-warm);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/* CF-REF: motion-quality-signal.md [14]
|
|
660
|
+
DL Part VI.5: Card hover uses duration-normal (250ms), not fast (150ms).
|
|
661
|
+
Cards are content containers -- smoother transition than buttons.
|
|
662
|
+
Framerate test: play at 30fps, must feel broken. */
|
|
512
663
|
.strand-card--interactive {
|
|
513
|
-
border: 1px solid var(--strand-border-subtle);
|
|
514
|
-
box-shadow: var(--strand-elevation-1);
|
|
515
664
|
cursor: pointer;
|
|
665
|
+
text-decoration: none;
|
|
666
|
+
color: inherit;
|
|
667
|
+
display: block;
|
|
668
|
+
position: relative;
|
|
516
669
|
transition:
|
|
517
|
-
transform var(--strand-duration-
|
|
518
|
-
box-shadow var(--strand-duration-
|
|
670
|
+
transform var(--strand-duration-normal) var(--strand-ease-out-expo),
|
|
671
|
+
box-shadow var(--strand-duration-normal) ease;
|
|
519
672
|
}
|
|
520
673
|
|
|
521
674
|
.strand-card--interactive:hover {
|
|
522
675
|
transform: translateY(-2px);
|
|
523
|
-
box-shadow: var(--strand-elevation-2);
|
|
676
|
+
box-shadow: var(--strand-elevation-2); /* CF-REF: five-level-shadow.md [1] */
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/* The "alive" signal for live/active cards has been through many
|
|
680
|
+
iterations and each one was wrong: conic-gradient radar sweep
|
|
681
|
+
(too animated), blue-indicator halo plus top edge accent (too
|
|
682
|
+
loud, read as AI-workspace chrome), left-edge accent bar
|
|
683
|
+
(banned, copied by every agentic coding tool as lowest-effort
|
|
684
|
+
default), and finally a blue-glow tinted background (still
|
|
685
|
+
too heavy, reads as "blue borders" on lab/channel grids).
|
|
686
|
+
|
|
687
|
+
The active state has NO card-chrome treatment at all. Inline
|
|
688
|
+
affordances (a "Joined" pill, a status-chip--live, a lifecycle
|
|
689
|
+
overline like "Live" or "Early Access", a StatusIndicator) carry
|
|
690
|
+
the entire signal. The class is kept as a no-op semantic hook
|
|
691
|
+
so consumers can still mark active cards for data-attr selectors
|
|
692
|
+
or future treatments, but it paints nothing by default. */
|
|
693
|
+
.strand-card--active {
|
|
694
|
+
/* intentionally empty */
|
|
524
695
|
}
|
|
525
696
|
|
|
526
|
-
/* ── Padding ── */
|
|
697
|
+
/* ── Padding modifiers ── */
|
|
527
698
|
.strand-card--pad-none {
|
|
528
699
|
padding: 0;
|
|
529
700
|
}
|
|
@@ -537,16 +708,73 @@
|
|
|
537
708
|
}
|
|
538
709
|
|
|
539
710
|
.strand-card--pad-lg {
|
|
711
|
+
padding: var(--strand-space-8);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.strand-card--pad-xl {
|
|
540
715
|
padding: var(--strand-space-10);
|
|
541
716
|
}
|
|
542
717
|
|
|
543
|
-
/*
|
|
718
|
+
/* CF-REF: focus-visible.md [4] */
|
|
544
719
|
.strand-card--interactive:focus-visible {
|
|
545
720
|
outline: 2px solid var(--strand-blue-primary);
|
|
546
721
|
outline-offset: 2px;
|
|
547
722
|
}
|
|
548
723
|
|
|
549
|
-
/* ──
|
|
724
|
+
/* ── Channel grid helpers ──
|
|
725
|
+
Layout helpers for a content-sized card grid with an inline
|
|
726
|
+
next-event preview row. Useful for membership grids, channel
|
|
727
|
+
listings, and similar layouts where cards must size to their
|
|
728
|
+
content rather than fill a fixed cell. */
|
|
729
|
+
.strand-channel-grid {
|
|
730
|
+
align-items: start;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.strand-channel-title {
|
|
734
|
+
margin: 0;
|
|
735
|
+
line-height: var(--strand-leading-snug);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.strand-channel-description {
|
|
739
|
+
margin: 0;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.strand-channel-next {
|
|
743
|
+
display: flex;
|
|
744
|
+
align-items: baseline;
|
|
745
|
+
flex-wrap: wrap;
|
|
746
|
+
gap: var(--strand-space-2);
|
|
747
|
+
margin-top: var(--strand-space-1);
|
|
748
|
+
padding-top: var(--strand-space-2);
|
|
749
|
+
border-top: 1px dashed var(--strand-gray-200);
|
|
750
|
+
font-size: var(--strand-text-xs);
|
|
751
|
+
line-height: var(--strand-leading-snug);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.strand-channel-next__label {
|
|
755
|
+
font-family: var(--strand-font-mono);
|
|
756
|
+
font-size: var(--strand-text-xs);
|
|
757
|
+
font-weight: var(--strand-weight-medium);
|
|
758
|
+
letter-spacing: var(--strand-tracking-widest);
|
|
759
|
+
text-transform: uppercase;
|
|
760
|
+
color: var(--strand-gray-500);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.strand-channel-next__title {
|
|
764
|
+
color: var(--strand-gray-900);
|
|
765
|
+
font-weight: var(--strand-weight-medium);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.strand-channel-next__when {
|
|
769
|
+
color: var(--strand-gray-600);
|
|
770
|
+
font-variant-numeric: tabular-nums;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.strand-channel-signin-hint {
|
|
774
|
+
grid-column: 1 / -1;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/* CF-REF: reduced-motion.md [9] */
|
|
550
778
|
@media (prefers-reduced-motion: reduce) {
|
|
551
779
|
.strand-card--interactive {
|
|
552
780
|
transition: none;
|
|
@@ -683,20 +911,162 @@
|
|
|
683
911
|
margin-bottom: var(--strand-space-2);
|
|
684
912
|
}
|
|
685
913
|
|
|
686
|
-
/* ── Code area ──
|
|
687
|
-
|
|
914
|
+
/* ── Code area ──
|
|
915
|
+
Both <pre> and the nested <code> get explicit color AND background
|
|
916
|
+
so axe-core never has to walk inheritance or composite the
|
|
917
|
+
inset box-shadow. Without explicit color/background on the code
|
|
918
|
+
element, axe-core can compute a false-positive contrast violation
|
|
919
|
+
by blending the inset shadow against the surface. */
|
|
920
|
+
.strand-code-block__pre,
|
|
921
|
+
.strand-code-block__pre code {
|
|
688
922
|
font-family: var(--strand-font-mono);
|
|
689
923
|
font-size: var(--strand-text-sm);
|
|
690
924
|
line-height: var(--strand-leading-relaxed);
|
|
691
|
-
color: var(--strand-blue-
|
|
692
|
-
background: var(--strand-surface-recessed);
|
|
693
|
-
|
|
925
|
+
color: var(--strand-blue-abyss);
|
|
926
|
+
background-color: var(--strand-surface-recessed);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.strand-code-block__pre {
|
|
694
930
|
border-radius: var(--strand-radius-lg);
|
|
695
931
|
padding: var(--strand-space-3) var(--strand-space-4);
|
|
696
932
|
overflow-x: auto;
|
|
697
933
|
white-space: pre;
|
|
698
934
|
tab-size: 2;
|
|
699
935
|
margin: 0;
|
|
936
|
+
border: 1px solid var(--strand-gray-200);
|
|
937
|
+
/* Floor the pre at touch-target so that even a theoretical empty
|
|
938
|
+
code block leaves room for the copy affordance. Single-line
|
|
939
|
+
snippets naturally exceed this (padding + line-height + border
|
|
940
|
+
≈ 47.66px) so this is defensive rather than active. */
|
|
941
|
+
min-height: var(--strand-touch-target);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/* ── Copy button ──
|
|
945
|
+
Injected by consumer JS (strand-copy-buttons.js in dillinger) onto any
|
|
946
|
+
.strand-code-block wrapper. The button sits at the top-right of the
|
|
947
|
+
wrapper. The host wrapper carries [data-strand-copy] so the inner
|
|
948
|
+
<pre> can reserve right-side padding for the icon without shifting
|
|
949
|
+
code blocks that have no copy button.
|
|
950
|
+
Default state is low-ink so it does not compete with the code. On
|
|
951
|
+
hover, focus, or parent hover it gains full contrast. Success state
|
|
952
|
+
swaps the icon to a check and flips the label to "Copied" for 1.5s. */
|
|
953
|
+
.strand-code-block[data-strand-copy] > .strand-code-block__pre {
|
|
954
|
+
padding-right: var(--strand-space-10);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/* Geometry derivation (why these exact values):
|
|
958
|
+
- <pre> for a 1-line snippet: 12 + 21.66 + 12 + 2 = ~47.66px tall
|
|
959
|
+
(padding-top + line-height + padding-bottom + 2×border).
|
|
960
|
+
- Button MUST fit inside the pre or its white surface bleeds out
|
|
961
|
+
the bottom. With top=8 and a 44x44 button, the bottom lands at
|
|
962
|
+
54px which overflows the 47.66 pre by ~6px (the bleed the
|
|
963
|
+
previous fix tried to mask).
|
|
964
|
+
- Visible button is therefore 32×32 (+ 2×1 border = 34×34). With
|
|
965
|
+
top=6 the box occupies y ∈ [6, 40], inside the 47.66 pre with
|
|
966
|
+
~7.66px of breathing room.
|
|
967
|
+
- First code line center = padding-top (12) + ½ line-height
|
|
968
|
+
(~10.83) = ~22.83px from pre top. Button center = top (6) + 17
|
|
969
|
+
= 23px. Delta = 0.17px, well under the 2px test slop.
|
|
970
|
+
- A `::before` pseudo-element expands the hit area to 44×44
|
|
971
|
+
(WCAG 2.5.8 touch target) without affecting the visible box. */
|
|
972
|
+
.strand-code-block__copy {
|
|
973
|
+
position: absolute;
|
|
974
|
+
top: calc(var(--strand-space-2) - 2px);
|
|
975
|
+
right: calc(var(--strand-space-2) - 2px);
|
|
976
|
+
display: inline-flex;
|
|
977
|
+
align-items: center;
|
|
978
|
+
justify-content: center;
|
|
979
|
+
width: var(--strand-space-8);
|
|
980
|
+
height: var(--strand-space-8);
|
|
981
|
+
padding: 0;
|
|
982
|
+
border: 1px solid var(--strand-gray-200);
|
|
983
|
+
border-radius: var(--strand-radius-md);
|
|
984
|
+
background-color: var(--strand-surface-elevated);
|
|
985
|
+
color: var(--strand-gray-500);
|
|
986
|
+
cursor: pointer;
|
|
987
|
+
opacity: 0.55;
|
|
988
|
+
transition:
|
|
989
|
+
opacity var(--strand-duration-fast) var(--strand-ease-out-expo),
|
|
990
|
+
color var(--strand-duration-fast) ease,
|
|
991
|
+
border-color var(--strand-duration-fast) ease,
|
|
992
|
+
background-color var(--strand-duration-fast) ease,
|
|
993
|
+
transform var(--strand-duration-fast) var(--strand-ease-out-expo);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/* Expanded hit area: the visible button is 32x32 so it fits in a
|
|
997
|
+
1-line code block, but WCAG 2.5.8 (Target Size Minimum) prefers a
|
|
998
|
+
larger touch target. This invisible pseudo-element extends the
|
|
999
|
+
click area to 44x44 without shifting any pixels. The halo is
|
|
1000
|
+
contained within the reserved right-padding on the pre so it never
|
|
1001
|
+
overlaps code text. */
|
|
1002
|
+
.strand-code-block__copy::before {
|
|
1003
|
+
content: "";
|
|
1004
|
+
position: absolute;
|
|
1005
|
+
inset: calc((var(--strand-touch-target) - var(--strand-space-8)) / -2);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.strand-code-block:hover .strand-code-block__copy,
|
|
1009
|
+
.strand-code-block__copy:hover,
|
|
1010
|
+
.strand-code-block__copy:focus-visible {
|
|
1011
|
+
opacity: 1;
|
|
1012
|
+
color: var(--strand-blue-primary);
|
|
1013
|
+
border-color: var(--strand-blue-primary);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
.strand-code-block__copy:hover {
|
|
1017
|
+
transform: translateY(-1px);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
.strand-code-block__copy:active {
|
|
1021
|
+
transform: translateY(0);
|
|
1022
|
+
transition-duration: 75ms;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
.strand-code-block__copy:focus-visible {
|
|
1026
|
+
outline: 2px solid var(--strand-blue-primary);
|
|
1027
|
+
outline-offset: 2px;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/* Success state: check icon, green accent, pinned opacity. */
|
|
1031
|
+
.strand-code-block__copy--copied,
|
|
1032
|
+
.strand-code-block__copy--copied:hover {
|
|
1033
|
+
opacity: 1;
|
|
1034
|
+
color: var(--strand-green-positive);
|
|
1035
|
+
border-color: var(--strand-green-positive);
|
|
1036
|
+
transform: translateY(0);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/* Icon swap. Both icons stack in the same cell; the active one has
|
|
1040
|
+
display:inline, the inactive one has display:none. No opacity or
|
|
1041
|
+
transform animation on the swap so reduced-motion users get the
|
|
1042
|
+
same instant visual feedback. */
|
|
1043
|
+
.strand-code-block__copy-icon {
|
|
1044
|
+
width: 16px;
|
|
1045
|
+
height: 16px;
|
|
1046
|
+
pointer-events: none;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
.strand-code-block__copy-icon--check {
|
|
1050
|
+
display: none;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.strand-code-block__copy--copied .strand-code-block__copy-icon--clipboard {
|
|
1054
|
+
display: none;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
.strand-code-block__copy--copied .strand-code-block__copy-icon--check {
|
|
1058
|
+
display: inline;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1062
|
+
.strand-code-block__copy {
|
|
1063
|
+
transition: none;
|
|
1064
|
+
}
|
|
1065
|
+
.strand-code-block__copy:hover,
|
|
1066
|
+
.strand-code-block__copy:active,
|
|
1067
|
+
.strand-code-block__copy--copied {
|
|
1068
|
+
transform: none;
|
|
1069
|
+
}
|
|
700
1070
|
}
|
|
701
1071
|
|
|
702
1072
|
/* ── Inline code ── */
|
|
@@ -922,6 +1292,13 @@
|
|
|
922
1292
|
white-space: nowrap;
|
|
923
1293
|
}
|
|
924
1294
|
|
|
1295
|
+
/* ── Gradient (biosynthetic separator) ── */
|
|
1296
|
+
.strand-divider--gradient {
|
|
1297
|
+
border-top: none;
|
|
1298
|
+
height: 1px;
|
|
1299
|
+
background: linear-gradient(90deg, transparent 0%, var(--strand-blue-indicator) 50%, transparent 100%);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
925
1302
|
|
|
926
1303
|
/* FormField */
|
|
927
1304
|
/*! Strand UI | MIT License | dillingerstaffing.com */
|
|
@@ -946,9 +1323,12 @@
|
|
|
946
1323
|
line-height: var(--strand-leading-snug);
|
|
947
1324
|
}
|
|
948
1325
|
|
|
949
|
-
/* ── Required indicator ──
|
|
1326
|
+
/* ── Required indicator ──
|
|
1327
|
+
Uses blue-primary, not red, because the asterisk is a "this is required"
|
|
1328
|
+
accent (informational), not an error. Red is reserved for actual errors
|
|
1329
|
+
inside .strand-form-field__error. */
|
|
950
1330
|
.strand-form-field__required {
|
|
951
|
-
color: var(--strand-
|
|
1331
|
+
color: var(--strand-blue-primary);
|
|
952
1332
|
margin-left: var(--strand-space-1);
|
|
953
1333
|
}
|
|
954
1334
|
|
|
@@ -999,9 +1379,21 @@
|
|
|
999
1379
|
.strand-grid--cols-3 { grid-template-columns: repeat(3, 1fr); }
|
|
1000
1380
|
.strand-grid--cols-4 { grid-template-columns: repeat(4, 1fr); }
|
|
1001
1381
|
|
|
1002
|
-
/* ── Responsive auto-fit (columns adjust to available width) ──
|
|
1382
|
+
/* ── Responsive auto-fit (columns adjust to available width) ──
|
|
1383
|
+
Use the size that matches your card content width. The breakpoint
|
|
1384
|
+
behavior is determined entirely by the minmax() value:
|
|
1385
|
+
- auto-sm (200px): 1col <410, 2col 410-640, 3col 640-870, 4col 870+
|
|
1386
|
+
- auto-220 (220px): 1col <450, 2col 450-700, 3col 700-940, 4col 940+
|
|
1387
|
+
- auto-md (280px): 1col <570, 2col 570-880, 3col 880-1190, 4col 1190+
|
|
1388
|
+
- auto-260 (260px): 1col <530, 2col 530-820, 3col 820-1100, 4col 1100+
|
|
1389
|
+
- auto-lg (360px): 1col <730, 2col 730-1120, 3col 1120-1500, 4col 1500+
|
|
1390
|
+
The auto-220 and auto-260 presets exist for card layouts that need
|
|
1391
|
+
4 columns at 1280 desktop, 2 columns at tablet portrait, and 1 column
|
|
1392
|
+
at phone widths. */
|
|
1003
1393
|
.strand-grid--auto-sm { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
|
|
1394
|
+
.strand-grid--auto-220 { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
|
|
1004
1395
|
.strand-grid--auto-md { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
|
|
1396
|
+
.strand-grid--auto-260 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
|
|
1005
1397
|
.strand-grid--auto-lg { grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); }
|
|
1006
1398
|
|
|
1007
1399
|
/* ── Gap utilities ── */
|
|
@@ -1111,6 +1503,23 @@
|
|
|
1111
1503
|
|
|
1112
1504
|
/*! Strand UI | MIT License | dillingerstaffing.com */
|
|
1113
1505
|
|
|
1506
|
+
/* ══════════════════════════════════════════════════
|
|
1507
|
+
INSTRUMENT VIEWPORT (DL Part 9.3: Dark Mode Island)
|
|
1508
|
+
══════════════════════════════════════════════════
|
|
1509
|
+
The viewport is the only place dark backgrounds appear in the
|
|
1510
|
+
lab. The frame around it stays the white lab surface. Inside
|
|
1511
|
+
the viewport, every primitive (search bar, panel, pin, legend,
|
|
1512
|
+
coordinate readout, scanline, vignette) is built from the
|
|
1513
|
+
instrument tokens defined in tokens.css. None of these
|
|
1514
|
+
primitives leak the dark surface to the rest of the page.
|
|
1515
|
+
|
|
1516
|
+
Three modes:
|
|
1517
|
+
- Default (recessed inside a card)
|
|
1518
|
+
- --grid (with subtle blue grid overlay)
|
|
1519
|
+
- --full-bleed (page-filling instrument cabinet, requires
|
|
1520
|
+
body to be marked .strand-body--instrument so the page
|
|
1521
|
+
surface is dark all the way to the edges) */
|
|
1522
|
+
|
|
1114
1523
|
.strand-instrument-viewport {
|
|
1115
1524
|
background: var(--strand-blue-abyss);
|
|
1116
1525
|
color: var(--strand-gray-100);
|
|
@@ -1134,57 +1543,1387 @@
|
|
|
1134
1543
|
font-variant-numeric: tabular-nums;
|
|
1135
1544
|
}
|
|
1136
1545
|
|
|
1137
|
-
/*
|
|
1546
|
+
/* ── Map slot (base layer inside the viewport) ──
|
|
1547
|
+
When the instrument viewport hosts a map (DL 9.3 explicitly
|
|
1548
|
+
lists maps as supported content), the consumer's map library
|
|
1549
|
+
container div must be given a known size. Without explicit
|
|
1550
|
+
dimensions a raw <div> collapses to height: 0 and libraries
|
|
1551
|
+
like maplibre-gl silently initialize into a zero-size canvas
|
|
1552
|
+
and render nothing.
|
|
1553
|
+
|
|
1554
|
+
This slot primitive fills the viewport so the map canvas has
|
|
1555
|
+
somewhere to paint. It deliberately does NOT set z-index: a
|
|
1556
|
+
z-index would create a new stacking context that traps the
|
|
1557
|
+
map library's markers below the viewport's FUI overlays
|
|
1558
|
+
(scanline, vignette). Without a z-index, the markers can be
|
|
1559
|
+
promoted into the viewport's own stacking context via the
|
|
1560
|
+
library-specific marker rules below.
|
|
1561
|
+
|
|
1562
|
+
NOTE on specificity: the selector is scoped via the parent
|
|
1563
|
+
.strand-instrument-viewport to outrank .maplibregl-map, which
|
|
1564
|
+
maplibre-gl.css (appended at runtime, after strand-ui.css)
|
|
1565
|
+
sets to position: relative. Without the descendant selector
|
|
1566
|
+
the single-class rules would tie on specificity, and later
|
|
1567
|
+
source order would hand the win to maplibre, leaving the
|
|
1568
|
+
container as a relatively-positioned in-flow element whose
|
|
1569
|
+
height: 100% can collapse to 0 in some mobile browsers when
|
|
1570
|
+
the parent chain's explicit heights are not yet resolved at
|
|
1571
|
+
paint time. Absolute positioning anchored via inset: 0
|
|
1572
|
+
guarantees the container always fills its containing block,
|
|
1573
|
+
regardless of maplibre's cascade. */
|
|
1574
|
+
.strand-instrument-viewport .strand-instrument-viewport__map {
|
|
1575
|
+
position: absolute;
|
|
1576
|
+
inset: 0;
|
|
1577
|
+
width: 100%;
|
|
1578
|
+
height: 100%;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/* ── Optional grid overlay ── */
|
|
1138
1582
|
.strand-instrument-viewport--grid::before {
|
|
1139
1583
|
content: '';
|
|
1140
1584
|
position: absolute;
|
|
1141
1585
|
inset: 0;
|
|
1142
1586
|
background:
|
|
1143
|
-
linear-gradient(
|
|
1144
|
-
linear-gradient(90deg,
|
|
1587
|
+
linear-gradient(var(--strand-viewport-grid-line) 1px, transparent 1px),
|
|
1588
|
+
linear-gradient(90deg, var(--strand-viewport-grid-line) 1px, transparent 1px);
|
|
1145
1589
|
background-size: var(--strand-viewport-grid-size) var(--strand-viewport-grid-size);
|
|
1146
1590
|
pointer-events: none;
|
|
1147
1591
|
}
|
|
1148
1592
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
font-family: var(--strand-font-sans);
|
|
1159
|
-
background-image: linear-gradient(currentColor, currentColor);
|
|
1160
|
-
background-position: 0% 100%;
|
|
1161
|
-
background-repeat: no-repeat;
|
|
1162
|
-
background-size: 0% 1px;
|
|
1163
|
-
transition: background-size var(--strand-duration-normal) var(--strand-ease-out-expo);
|
|
1164
|
-
cursor: pointer;
|
|
1593
|
+
/* ── Full-bleed variant (page-filling instrument cabinet) ──
|
|
1594
|
+
The viewport occupies all the space below the nav. The body
|
|
1595
|
+
must be marked .strand-body--instrument so the dark surface
|
|
1596
|
+
reaches the edge of the screen, not just the rounded card. */
|
|
1597
|
+
.strand-instrument-viewport--full-bleed {
|
|
1598
|
+
width: 100%;
|
|
1599
|
+
height: calc(100vh - var(--strand-nav-height));
|
|
1600
|
+
height: calc(100dvh - var(--strand-nav-height));
|
|
1601
|
+
border-radius: 0;
|
|
1165
1602
|
}
|
|
1166
1603
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1604
|
+
/* ── Atmospheric grid overlay (always-on for full-bleed) ── */
|
|
1605
|
+
.strand-instrument-viewport--full-bleed::before {
|
|
1606
|
+
content: "";
|
|
1607
|
+
position: absolute;
|
|
1608
|
+
inset: 0;
|
|
1609
|
+
background-image: linear-gradient(var(--strand-viewport-grid-line) 1px, transparent 1px),
|
|
1610
|
+
linear-gradient(90deg, var(--strand-viewport-grid-line) 1px, transparent 1px);
|
|
1611
|
+
background-size: 60px 60px;
|
|
1612
|
+
pointer-events: none;
|
|
1613
|
+
z-index: 3;
|
|
1169
1614
|
}
|
|
1170
1615
|
|
|
1171
|
-
/* ──
|
|
1172
|
-
.strand-
|
|
1173
|
-
|
|
1174
|
-
|
|
1616
|
+
/* ── Ambient breathing glow (DL Part 7: alive signal) ── */
|
|
1617
|
+
.strand-instrument-viewport--full-bleed::after {
|
|
1618
|
+
content: "";
|
|
1619
|
+
position: absolute;
|
|
1620
|
+
inset: 0;
|
|
1621
|
+
background: radial-gradient(
|
|
1622
|
+
ellipse 60% 50% at 50% 40%,
|
|
1623
|
+
var(--strand-instrument-glow-soft) 0%,
|
|
1624
|
+
transparent 70%
|
|
1625
|
+
);
|
|
1626
|
+
pointer-events: none;
|
|
1627
|
+
z-index: 2;
|
|
1628
|
+
animation: strand-instrument-breathe 8s var(--strand-ease-in-out-sine) infinite;
|
|
1175
1629
|
}
|
|
1176
1630
|
|
|
1177
|
-
|
|
1178
|
-
.
|
|
1179
|
-
|
|
1180
|
-
align-items: center;
|
|
1181
|
-
min-height: var(--strand-touch-target);
|
|
1182
|
-
padding-block: var(--strand-space-2);
|
|
1183
|
-
font-size: var(--strand-text-sm);
|
|
1631
|
+
@keyframes strand-instrument-breathe {
|
|
1632
|
+
0%, 100% { opacity: 0.5; }
|
|
1633
|
+
50% { opacity: 1; }
|
|
1184
1634
|
}
|
|
1185
1635
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1636
|
+
/* ──────────────────────────────────────────────
|
|
1637
|
+
BODY MODE: full-bleed dark surface
|
|
1638
|
+
──────────────────────────────────────────────
|
|
1639
|
+
When a page hosts a full-bleed instrument viewport, the body
|
|
1640
|
+
itself must be dark all the way to the edges. The nav also
|
|
1641
|
+
needs to switch to its instrument variant. Add
|
|
1642
|
+
.strand-body--instrument to <body> to enable this mode. */
|
|
1643
|
+
|
|
1644
|
+
.strand-body--instrument {
|
|
1645
|
+
background: var(--strand-instrument-bg);
|
|
1646
|
+
overflow: hidden;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/* ──────────────────────────────────────────────
|
|
1650
|
+
NAV: instrument variant (dark glassmorphic)
|
|
1651
|
+
──────────────────────────────────────────────
|
|
1652
|
+
When the body is in instrument mode, the nav must also be
|
|
1653
|
+
dark + glassmorphic to match the cabinet aesthetic. Apply
|
|
1654
|
+
.strand-nav--instrument alongside .strand-nav for a dark
|
|
1655
|
+
blurred bar with blue-tinted hairline.
|
|
1656
|
+
|
|
1657
|
+
The selectors compose .strand-nav with .strand-nav--instrument
|
|
1658
|
+
so they outweigh the bare .strand-nav rules even though Nav.css
|
|
1659
|
+
is bundled later than InstrumentViewport.css. Without the
|
|
1660
|
+
compound selector, the white surface from Nav.css would win
|
|
1661
|
+
on source order. */
|
|
1662
|
+
|
|
1663
|
+
.strand-nav.strand-nav--instrument {
|
|
1664
|
+
background: var(--strand-instrument-bg-translucent);
|
|
1665
|
+
-webkit-backdrop-filter: blur(16px);
|
|
1666
|
+
backdrop-filter: blur(16px);
|
|
1667
|
+
border-bottom: 1px solid var(--strand-instrument-border);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
.strand-nav.strand-nav--instrument .strand-nav__logo {
|
|
1671
|
+
color: var(--strand-blue-primary);
|
|
1672
|
+
text-shadow: 0 0 12px rgba(59, 142, 246, 0.3);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
.strand-nav.strand-nav--instrument .strand-nav__link {
|
|
1676
|
+
color: var(--strand-instrument-text-secondary);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
.strand-nav.strand-nav--instrument .strand-nav__link:hover {
|
|
1680
|
+
color: var(--strand-instrument-text-primary);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/* ──────────────────────────────────────────────
|
|
1684
|
+
NAV ACCESSORIES (mono titles + ghost CTAs)
|
|
1685
|
+
──────────────────────────────────────────────
|
|
1686
|
+
Lab pages render a small mono title between the logo and the
|
|
1687
|
+
primary CTA. The accessories live in the nav__inner container,
|
|
1688
|
+
between logo and actions. */
|
|
1689
|
+
|
|
1690
|
+
.strand-nav__title {
|
|
1691
|
+
font-family: var(--strand-font-mono);
|
|
1692
|
+
font-size: var(--strand-text-sm);
|
|
1693
|
+
font-weight: var(--strand-weight-semibold);
|
|
1694
|
+
letter-spacing: var(--strand-tracking-widest);
|
|
1695
|
+
text-transform: uppercase;
|
|
1696
|
+
color: var(--strand-instrument-text-primary);
|
|
1697
|
+
text-shadow: 0 0 16px rgba(59, 142, 246, 0.15);
|
|
1698
|
+
margin-right: auto;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
.strand-nav__title-tag {
|
|
1702
|
+
font-weight: var(--strand-weight-regular);
|
|
1703
|
+
color: var(--strand-gray-400);
|
|
1704
|
+
margin-left: var(--strand-space-1);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
/* ──────────────────────────────────────────────
|
|
1708
|
+
FUI: SCANLINE
|
|
1709
|
+
──────────────────────────────────────────────
|
|
1710
|
+
A horizontal pulse that sweeps the viewport from top to bottom
|
|
1711
|
+
on every scan trigger. Uses Principle 7 (alive signal) at the
|
|
1712
|
+
highest visibility tier: an event the user can perceive when
|
|
1713
|
+
the system is actively processing. Combine with .scanline-
|
|
1714
|
+
ambient for the slow continuous baseline pulse. */
|
|
1715
|
+
|
|
1716
|
+
.strand-scanline {
|
|
1717
|
+
position: absolute;
|
|
1718
|
+
left: 0;
|
|
1719
|
+
right: 0;
|
|
1720
|
+
height: 2px;
|
|
1721
|
+
top: -2px;
|
|
1722
|
+
background: linear-gradient(
|
|
1723
|
+
90deg,
|
|
1724
|
+
transparent 0%,
|
|
1725
|
+
rgba(59, 142, 246, 0.2) 15%,
|
|
1726
|
+
var(--strand-instrument-glow-strong) 50%,
|
|
1727
|
+
rgba(59, 142, 246, 0.2) 85%,
|
|
1728
|
+
transparent 100%
|
|
1729
|
+
);
|
|
1730
|
+
box-shadow:
|
|
1731
|
+
0 0 16px rgba(59, 142, 246, 0.3),
|
|
1732
|
+
0 0 48px rgba(59, 142, 246, 0.1),
|
|
1733
|
+
0 1px 0 var(--strand-instrument-border);
|
|
1734
|
+
pointer-events: none;
|
|
1735
|
+
z-index: 5;
|
|
1736
|
+
opacity: 0;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
.strand-scanline--active {
|
|
1740
|
+
animation: strand-scan 2s var(--strand-ease-out-quart) forwards;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
@keyframes strand-scan {
|
|
1744
|
+
0% { top: -2px; opacity: 0; }
|
|
1745
|
+
5% { opacity: 0.5; }
|
|
1746
|
+
90% { opacity: 0.5; }
|
|
1747
|
+
100% { top: 100%; opacity: 0; }
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/* ── Ambient continuous scan (slow breathing baseline) ── */
|
|
1751
|
+
.strand-scanline--ambient {
|
|
1752
|
+
position: absolute;
|
|
1753
|
+
left: 0;
|
|
1754
|
+
right: 0;
|
|
1755
|
+
height: 1px;
|
|
1756
|
+
top: -1px;
|
|
1757
|
+
background: linear-gradient(
|
|
1758
|
+
90deg,
|
|
1759
|
+
transparent 0%,
|
|
1760
|
+
rgba(59, 142, 246, 0.08) 30%,
|
|
1761
|
+
var(--strand-instrument-glow-medium) 50%,
|
|
1762
|
+
rgba(59, 142, 246, 0.08) 70%,
|
|
1763
|
+
transparent 100%
|
|
1764
|
+
);
|
|
1765
|
+
pointer-events: none;
|
|
1766
|
+
z-index: 5;
|
|
1767
|
+
opacity: 1;
|
|
1768
|
+
animation: strand-scan-slow 6s linear infinite;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
@keyframes strand-scan-slow {
|
|
1772
|
+
0% { top: -1px; }
|
|
1773
|
+
100% { top: 100%; }
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
/* ──────────────────────────────────────────────
|
|
1777
|
+
FUI: VIEWPORT VIGNETTE (cursor-following spotlight)
|
|
1778
|
+
──────────────────────────────────────────────
|
|
1779
|
+
A radial gradient that follows the user's cursor (via JS that
|
|
1780
|
+
sets --mouse-x / --mouse-y custom properties on the viewport),
|
|
1781
|
+
darkening the edges so the user's eye is drawn toward whatever
|
|
1782
|
+
they are pointing at. The "instrument focus" effect from
|
|
1783
|
+
cinematic FUI work. */
|
|
1784
|
+
|
|
1785
|
+
.strand-viewport-vignette {
|
|
1786
|
+
position: absolute;
|
|
1787
|
+
inset: 0;
|
|
1788
|
+
pointer-events: none;
|
|
1789
|
+
z-index: 4;
|
|
1790
|
+
background: radial-gradient(
|
|
1791
|
+
ellipse 70% 60% at var(--mouse-x, 50%) var(--mouse-y, 50%),
|
|
1792
|
+
transparent 45%,
|
|
1793
|
+
rgba(15, 25, 42, 0.35) 100%
|
|
1794
|
+
);
|
|
1795
|
+
transition: background-position 0.15s ease;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
/* ──────────────────────────────────────────────
|
|
1799
|
+
FUI: COORDINATE READOUT
|
|
1800
|
+
──────────────────────────────────────────────
|
|
1801
|
+
A small mono panel in the bottom-left corner that displays the
|
|
1802
|
+
instrument's current coordinate (latitude/longitude, or any
|
|
1803
|
+
tabular numeric pair). The blinking cursor before the values
|
|
1804
|
+
is the alive signal -- the system is reading position right
|
|
1805
|
+
now. */
|
|
1806
|
+
|
|
1807
|
+
.strand-coord-readout {
|
|
1808
|
+
position: absolute;
|
|
1809
|
+
bottom: var(--strand-space-4);
|
|
1810
|
+
left: var(--strand-space-4);
|
|
1811
|
+
z-index: 10;
|
|
1812
|
+
font-family: var(--strand-font-mono);
|
|
1813
|
+
font-size: 13px;
|
|
1814
|
+
font-variant-numeric: tabular-nums;
|
|
1815
|
+
letter-spacing: var(--strand-tracking-widest);
|
|
1816
|
+
color: rgba(148, 163, 184, 0.65);
|
|
1817
|
+
display: flex;
|
|
1818
|
+
align-items: center;
|
|
1819
|
+
gap: var(--strand-space-4);
|
|
1820
|
+
background: var(--strand-instrument-bg-overlay);
|
|
1821
|
+
border: 1px solid var(--strand-instrument-border);
|
|
1822
|
+
border-radius: var(--strand-radius-sm);
|
|
1823
|
+
padding: var(--strand-space-2) var(--strand-space-3);
|
|
1824
|
+
box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.3);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
.strand-coord-readout::before {
|
|
1828
|
+
content: "";
|
|
1829
|
+
display: inline-block;
|
|
1830
|
+
width: 2px;
|
|
1831
|
+
height: 12px;
|
|
1832
|
+
background: var(--strand-instrument-glow-strong);
|
|
1833
|
+
border-radius: var(--strand-radius-sm);
|
|
1834
|
+
animation: strand-coord-blink 1.2s steps(2) infinite;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
@keyframes strand-coord-blink {
|
|
1838
|
+
0%, 49% { opacity: 1; }
|
|
1839
|
+
50%, 100% { opacity: 0; }
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
.strand-coord-readout__lat,
|
|
1843
|
+
.strand-coord-readout__lng {
|
|
1844
|
+
display: inline-block;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/* ──────────────────────────────────────────────
|
|
1848
|
+
FUI: MAP LEGEND (sector classification HUD)
|
|
1849
|
+
──────────────────────────────────────────────
|
|
1850
|
+
A small HUD panel listing the sector classifications used by
|
|
1851
|
+
the instrument. The colored dots are the connector between
|
|
1852
|
+
the legend and the map pins. Hovering an item dims unrelated
|
|
1853
|
+
pins so the user can scan one sector at a time. */
|
|
1854
|
+
|
|
1855
|
+
.strand-map-legend {
|
|
1856
|
+
position: absolute;
|
|
1857
|
+
bottom: calc(var(--strand-space-6) + 6rem);
|
|
1858
|
+
right: var(--strand-space-4);
|
|
1859
|
+
z-index: 10;
|
|
1860
|
+
font-family: var(--strand-font-mono);
|
|
1861
|
+
font-size: var(--strand-text-xs);
|
|
1862
|
+
letter-spacing: var(--strand-tracking-wide);
|
|
1863
|
+
color: rgba(148, 163, 184, 0.6);
|
|
1864
|
+
display: flex;
|
|
1865
|
+
flex-direction: column;
|
|
1866
|
+
gap: var(--strand-space-2);
|
|
1867
|
+
background: rgba(15, 25, 42, 0.7);
|
|
1868
|
+
border: 1px solid var(--strand-instrument-border);
|
|
1869
|
+
border-radius: var(--strand-radius-md);
|
|
1870
|
+
padding: var(--strand-space-3) var(--strand-space-4);
|
|
1871
|
+
-webkit-backdrop-filter: blur(8px);
|
|
1872
|
+
backdrop-filter: blur(8px);
|
|
1873
|
+
box-shadow: var(--strand-instrument-shadow-inset), var(--strand-instrument-shadow-soft);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
.strand-map-legend__title {
|
|
1877
|
+
font-weight: var(--strand-weight-semibold);
|
|
1878
|
+
text-transform: uppercase;
|
|
1879
|
+
letter-spacing: var(--strand-tracking-widest);
|
|
1880
|
+
color: rgba(148, 163, 184, 0.4);
|
|
1881
|
+
margin-bottom: var(--strand-space-1);
|
|
1882
|
+
display: flex;
|
|
1883
|
+
align-items: center;
|
|
1884
|
+
gap: var(--strand-space-2);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
.strand-map-legend__title::before {
|
|
1888
|
+
content: "";
|
|
1889
|
+
width: 4px;
|
|
1890
|
+
height: 4px;
|
|
1891
|
+
background: var(--strand-instrument-glow-strong);
|
|
1892
|
+
border-radius: var(--strand-radius-full);
|
|
1893
|
+
animation: strand-instrument-breathe 3s var(--strand-ease-in-out-sine) infinite;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
.strand-map-legend__item {
|
|
1897
|
+
display: flex;
|
|
1898
|
+
align-items: center;
|
|
1899
|
+
gap: var(--strand-space-3);
|
|
1900
|
+
padding: 2px 0;
|
|
1901
|
+
cursor: pointer;
|
|
1902
|
+
transition: color var(--strand-duration-fast) ease;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
.strand-map-legend__item:hover {
|
|
1906
|
+
color: rgba(226, 232, 240, 0.9);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
.strand-map-legend__item:hover .strand-map-legend__dot {
|
|
1910
|
+
transform: scale(1.4);
|
|
1911
|
+
filter: brightness(1.3);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
.strand-map-legend__dot {
|
|
1915
|
+
width: 8px;
|
|
1916
|
+
height: 8px;
|
|
1917
|
+
border-radius: var(--strand-radius-full);
|
|
1918
|
+
transition: transform var(--strand-duration-fast) var(--strand-ease-out-expo),
|
|
1919
|
+
filter var(--strand-duration-fast) ease;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
.strand-map-legend__dot--tech {
|
|
1923
|
+
background: var(--strand-sector-tech);
|
|
1924
|
+
box-shadow: 0 0 8px rgba(59, 142, 246, 0.5);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
.strand-map-legend__dot--health {
|
|
1928
|
+
background: var(--strand-sector-health);
|
|
1929
|
+
box-shadow: 0 0 8px rgba(20, 184, 166, 0.5);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
.strand-map-legend__dot--trades {
|
|
1933
|
+
background: var(--strand-sector-trades);
|
|
1934
|
+
box-shadow: 0 0 8px rgba(34, 211, 238, 0.5);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
.strand-map-legend__dot--finance {
|
|
1938
|
+
background: var(--strand-sector-finance);
|
|
1939
|
+
box-shadow: 0 0 8px rgba(139, 92, 246, 0.5);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
/* ──────────────────────────────────────────────
|
|
1943
|
+
SEARCH BAR (floating glassmorphic, instrument)
|
|
1944
|
+
──────────────────────────────────────────────
|
|
1945
|
+
A floating search bar centered above the viewport. Glass-
|
|
1946
|
+
morphic dark surface, biosynthetic blue focus glow. This is
|
|
1947
|
+
the input device the user reaches for first inside an
|
|
1948
|
+
instrument viewport. */
|
|
1949
|
+
|
|
1950
|
+
.strand-search-bar {
|
|
1951
|
+
position: absolute;
|
|
1952
|
+
top: var(--strand-space-4);
|
|
1953
|
+
left: 50%;
|
|
1954
|
+
transform: translateX(-50%);
|
|
1955
|
+
z-index: 10;
|
|
1956
|
+
width: min(520px, calc(100% - var(--strand-space-8)));
|
|
1957
|
+
transition:
|
|
1958
|
+
left var(--strand-duration-normal) var(--strand-ease-out-expo),
|
|
1959
|
+
transform var(--strand-duration-normal) var(--strand-ease-out-expo),
|
|
1960
|
+
width var(--strand-duration-normal) var(--strand-ease-out-expo);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
.strand-search-bar--shifted {
|
|
1964
|
+
left: calc(340px + var(--strand-space-4) + var(--strand-space-4));
|
|
1965
|
+
transform: none;
|
|
1966
|
+
width: min(480px, calc(100% - 340px - var(--strand-space-4) * 3));
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
.strand-search-bar__inner {
|
|
1970
|
+
display: flex;
|
|
1971
|
+
align-items: center;
|
|
1972
|
+
gap: var(--strand-space-2);
|
|
1973
|
+
background: linear-gradient(
|
|
1974
|
+
135deg,
|
|
1975
|
+
rgba(15, 25, 42, 0.88) 0%,
|
|
1976
|
+
rgba(15, 23, 42, 0.85) 100%
|
|
1977
|
+
);
|
|
1978
|
+
-webkit-backdrop-filter: blur(20px);
|
|
1979
|
+
backdrop-filter: blur(20px);
|
|
1980
|
+
border: 1px solid rgba(59, 142, 246, 0.15);
|
|
1981
|
+
border-radius: var(--strand-radius-lg);
|
|
1982
|
+
padding: var(--strand-space-2) var(--strand-space-4);
|
|
1983
|
+
box-shadow: var(--strand-instrument-shadow-deep);
|
|
1984
|
+
transition:
|
|
1985
|
+
border-color var(--strand-duration-normal) ease,
|
|
1986
|
+
box-shadow var(--strand-duration-normal) ease;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
.strand-search-bar__inner:has(.strand-search-bar__input:focus) {
|
|
1990
|
+
border-color: var(--strand-instrument-border-strong);
|
|
1991
|
+
box-shadow:
|
|
1992
|
+
var(--strand-instrument-shadow-deep),
|
|
1993
|
+
inset 0 0 24px rgba(59, 142, 246, 0.06);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
.strand-search-bar__input {
|
|
1997
|
+
flex: 1;
|
|
1998
|
+
background: transparent;
|
|
1999
|
+
border: none;
|
|
2000
|
+
outline: none;
|
|
2001
|
+
font-family: var(--strand-font-sans);
|
|
2002
|
+
font-size: var(--strand-text-base);
|
|
2003
|
+
color: var(--strand-instrument-text-primary);
|
|
2004
|
+
padding: var(--strand-space-2) 0;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
.strand-search-bar__input::placeholder {
|
|
2008
|
+
color: var(--strand-gray-500);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
.strand-search-bar__action {
|
|
2012
|
+
display: flex;
|
|
2013
|
+
align-items: center;
|
|
2014
|
+
justify-content: center;
|
|
2015
|
+
width: 36px;
|
|
2016
|
+
height: 36px;
|
|
2017
|
+
border: 1px solid rgba(59, 142, 246, 0.2);
|
|
2018
|
+
border-radius: var(--strand-radius-md);
|
|
2019
|
+
background: transparent;
|
|
2020
|
+
color: var(--strand-gray-400);
|
|
2021
|
+
cursor: pointer;
|
|
2022
|
+
transition:
|
|
2023
|
+
color var(--strand-duration-fast) ease,
|
|
2024
|
+
border-color var(--strand-duration-fast) ease,
|
|
2025
|
+
box-shadow var(--strand-duration-fast) ease;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
.strand-search-bar__action:hover {
|
|
2029
|
+
color: var(--strand-blue-primary);
|
|
2030
|
+
border-color: var(--strand-blue-primary);
|
|
2031
|
+
box-shadow: 0 0 8px rgba(59, 142, 246, 0.2);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
.strand-search-bar__action:hover svg {
|
|
2035
|
+
filter: drop-shadow(0 0 3px rgba(59, 142, 246, 0.5));
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
.strand-search-bar__action:active {
|
|
2039
|
+
transform: scale(0.95);
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
.strand-search-bar__action--danger {
|
|
2043
|
+
border-color: rgba(239, 68, 68, 0.2);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
.strand-search-bar__action--danger:hover {
|
|
2047
|
+
color: var(--strand-red-alert);
|
|
2048
|
+
border-color: var(--strand-red-alert);
|
|
2049
|
+
box-shadow: 0 0 8px rgba(239, 68, 68, 0.2);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
.strand-search-bar__action[hidden] {
|
|
2053
|
+
display: none;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
.strand-search-bar__action--locating {
|
|
2057
|
+
color: var(--strand-blue-primary);
|
|
2058
|
+
border-color: var(--strand-blue-primary);
|
|
2059
|
+
animation: strand-search-bar-pulse 1s var(--strand-ease-in-out-sine) infinite;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
@keyframes strand-search-bar-pulse {
|
|
2063
|
+
0%, 100% { border-color: var(--strand-blue-primary); }
|
|
2064
|
+
50% { border-color: rgba(59, 142, 246, 0.3); }
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
/* ──────────────────────────────────────────────
|
|
2068
|
+
RESULTS PANEL (instrument left sidebar)
|
|
2069
|
+
──────────────────────────────────────────────
|
|
2070
|
+
The list of items returned by an instrument query. Glass-
|
|
2071
|
+
morphic dark surface, fixed left side on desktop, bottom
|
|
2072
|
+
sheet on mobile. Holds count, scrollable items, empty/error
|
|
2073
|
+
states. */
|
|
2074
|
+
|
|
2075
|
+
.strand-results-panel {
|
|
2076
|
+
position: absolute;
|
|
2077
|
+
top: var(--strand-space-4);
|
|
2078
|
+
left: var(--strand-space-4);
|
|
2079
|
+
width: 340px;
|
|
2080
|
+
max-height: calc(100% - var(--strand-space-8));
|
|
2081
|
+
z-index: 10;
|
|
2082
|
+
display: flex;
|
|
2083
|
+
flex-direction: column;
|
|
2084
|
+
background: var(--strand-instrument-bg-glass);
|
|
2085
|
+
-webkit-backdrop-filter: blur(16px);
|
|
2086
|
+
backdrop-filter: blur(16px);
|
|
2087
|
+
border: 1px solid rgba(59, 142, 246, 0.12);
|
|
2088
|
+
border-radius: var(--strand-radius-lg);
|
|
2089
|
+
overflow: hidden;
|
|
2090
|
+
box-shadow: var(--strand-instrument-shadow-medium);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
.strand-results-panel[hidden] {
|
|
2094
|
+
display: none;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
.strand-results-panel__count {
|
|
2098
|
+
font-family: var(--strand-font-mono);
|
|
2099
|
+
font-size: var(--strand-text-xs);
|
|
2100
|
+
letter-spacing: var(--strand-tracking-widest);
|
|
2101
|
+
text-transform: uppercase;
|
|
2102
|
+
color: var(--strand-instrument-text-tertiary);
|
|
2103
|
+
padding: var(--strand-space-3) var(--strand-space-4);
|
|
2104
|
+
border-bottom: 1px solid rgba(59, 142, 246, 0.08);
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
.strand-results-panel__items {
|
|
2108
|
+
overflow-y: auto;
|
|
2109
|
+
-webkit-overflow-scrolling: touch;
|
|
2110
|
+
max-height: calc(100vh - 240px);
|
|
2111
|
+
overscroll-behavior: contain;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
.strand-results-panel__state {
|
|
2115
|
+
padding: var(--strand-space-6) var(--strand-space-4);
|
|
2116
|
+
text-align: center;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
.strand-results-panel__state-title {
|
|
2120
|
+
font-family: var(--strand-font-mono);
|
|
2121
|
+
font-size: var(--strand-text-xs);
|
|
2122
|
+
letter-spacing: var(--strand-tracking-wide);
|
|
2123
|
+
color: var(--strand-instrument-text-tertiary);
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
.strand-results-panel__state-hint {
|
|
2127
|
+
font-size: var(--strand-text-xs);
|
|
2128
|
+
color: var(--strand-instrument-text-quiet);
|
|
2129
|
+
margin-top: var(--strand-space-2);
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
.strand-results-panel__error-link {
|
|
2133
|
+
display: inline-block;
|
|
2134
|
+
margin-top: var(--strand-space-3);
|
|
2135
|
+
font-family: var(--strand-font-mono);
|
|
2136
|
+
font-size: var(--strand-text-xs);
|
|
2137
|
+
color: var(--strand-blue-primary);
|
|
2138
|
+
text-decoration: none;
|
|
2139
|
+
border: 1px solid rgba(59, 142, 246, 0.3);
|
|
2140
|
+
border-radius: var(--strand-radius-sm);
|
|
2141
|
+
padding: var(--strand-space-2) var(--strand-space-3);
|
|
2142
|
+
transition: background var(--strand-duration-fast) ease;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
.strand-results-panel__error-link:hover {
|
|
2146
|
+
background: rgba(59, 142, 246, 0.1);
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
/* ── Result card ── */
|
|
2150
|
+
.strand-result-card {
|
|
2151
|
+
display: block;
|
|
2152
|
+
padding: var(--strand-space-3) var(--strand-space-4);
|
|
2153
|
+
border-bottom: 1px solid rgba(59, 142, 246, 0.06);
|
|
2154
|
+
cursor: pointer;
|
|
2155
|
+
transition: background var(--strand-duration-fast) ease;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
.strand-result-card:hover {
|
|
2159
|
+
background: rgba(59, 142, 246, 0.08);
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
/* Selection state for a master-detail list row. Uses background
|
|
2163
|
+
tint only. No left-edge accent bar (banned pattern, see
|
|
2164
|
+
Card.css comment above .strand-card--active). */
|
|
2165
|
+
.strand-result-card.is-active,
|
|
2166
|
+
.strand-result-card--active {
|
|
2167
|
+
background: rgba(59, 142, 246, 0.12);
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
.strand-result-card__title {
|
|
2171
|
+
font-size: var(--strand-text-sm);
|
|
2172
|
+
font-weight: var(--strand-weight-medium);
|
|
2173
|
+
color: var(--strand-instrument-text-primary);
|
|
2174
|
+
margin-bottom: 2px;
|
|
2175
|
+
line-height: 1.3;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
.strand-result-card__company {
|
|
2179
|
+
font-size: var(--strand-text-xs);
|
|
2180
|
+
color: rgba(148, 163, 184, 0.7);
|
|
2181
|
+
margin-bottom: 4px;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
.strand-result-card__meta {
|
|
2185
|
+
display: flex;
|
|
2186
|
+
align-items: center;
|
|
2187
|
+
gap: var(--strand-space-2);
|
|
2188
|
+
flex-wrap: wrap;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
.strand-result-card__location {
|
|
2192
|
+
font-family: var(--strand-font-mono);
|
|
2193
|
+
font-size: 10px;
|
|
2194
|
+
letter-spacing: var(--strand-tracking-wide);
|
|
2195
|
+
color: var(--strand-instrument-text-tertiary);
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
.strand-result-card__salary {
|
|
2199
|
+
font-family: var(--strand-font-mono);
|
|
2200
|
+
font-size: 10px;
|
|
2201
|
+
color: var(--strand-blue-indicator);
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
.strand-result-card__badge {
|
|
2205
|
+
font-family: var(--strand-font-mono);
|
|
2206
|
+
font-size: 9px;
|
|
2207
|
+
letter-spacing: var(--strand-tracking-wide);
|
|
2208
|
+
text-transform: uppercase;
|
|
2209
|
+
padding: 1px 6px;
|
|
2210
|
+
border-radius: var(--strand-radius-sm);
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
.strand-result-card__badge--remote {
|
|
2214
|
+
color: var(--strand-teal-vital);
|
|
2215
|
+
background: rgba(20, 184, 166, 0.12);
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
.strand-result-card__badge--source {
|
|
2219
|
+
color: rgba(148, 163, 184, 0.4);
|
|
2220
|
+
background: rgba(148, 163, 184, 0.06);
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
/* ──────────────────────────────────────────────
|
|
2224
|
+
DETAIL PANEL (instrument right slide-over)
|
|
2225
|
+
──────────────────────────────────────────────
|
|
2226
|
+
The expanded detail surface that slides in from the right
|
|
2227
|
+
side of the viewport when the user opens an item. Light
|
|
2228
|
+
surface, white frame inside the dark cabinet. Animated
|
|
2229
|
+
reveal with staggered children. */
|
|
2230
|
+
|
|
2231
|
+
.strand-detail-panel {
|
|
2232
|
+
position: absolute;
|
|
2233
|
+
top: 0;
|
|
2234
|
+
right: 0;
|
|
2235
|
+
width: 360px;
|
|
2236
|
+
max-width: 100%;
|
|
2237
|
+
height: 100%;
|
|
2238
|
+
z-index: 20;
|
|
2239
|
+
background: linear-gradient(135deg, rgba(248, 250, 252, 0.98) 0%, rgba(241, 245, 249, 0.97) 100%);
|
|
2240
|
+
-webkit-backdrop-filter: blur(8px);
|
|
2241
|
+
backdrop-filter: blur(8px);
|
|
2242
|
+
border-left: 1px solid rgba(59, 142, 246, 0.1);
|
|
2243
|
+
box-shadow:
|
|
2244
|
+
-4px 0 32px rgba(0, 0, 0, 0.25),
|
|
2245
|
+
inset 1px 0 0 rgba(255, 255, 255, 0.4);
|
|
2246
|
+
padding: 120px var(--strand-space-6) var(--strand-space-8);
|
|
2247
|
+
transform: translateX(100%);
|
|
2248
|
+
transition: transform var(--strand-duration-slow) var(--strand-ease-out-expo);
|
|
2249
|
+
overflow-y: auto;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
.strand-detail-panel[hidden] {
|
|
2253
|
+
display: none;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
.strand-detail-panel.is-open,
|
|
2257
|
+
.strand-detail-panel--open,
|
|
2258
|
+
.strand-detail-panel.open {
|
|
2259
|
+
transform: translateX(0);
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
/* ── Staggered content reveal (Principle 7: alive signal) ──
|
|
2263
|
+
The panel and its children share three equivalent state hooks:
|
|
2264
|
+
.is-open (BEM-state), --open (BEM-modifier), and .open (legacy
|
|
2265
|
+
alias kept for downstream consumers that already use it). All
|
|
2266
|
+
three trigger the same reveal sequence. */
|
|
2267
|
+
.strand-detail-panel.is-open > .strand-overline,
|
|
2268
|
+
.strand-detail-panel--open > .strand-overline,
|
|
2269
|
+
.strand-detail-panel.open > .strand-overline {
|
|
2270
|
+
animation: strand-detail-fade var(--strand-duration-slow) var(--strand-ease-out-expo) 0.1s both;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
.strand-detail-panel.is-open .strand-detail-panel__title,
|
|
2274
|
+
.strand-detail-panel--open .strand-detail-panel__title,
|
|
2275
|
+
.strand-detail-panel.open .strand-detail-panel__title {
|
|
2276
|
+
animation: strand-detail-fade var(--strand-duration-slow) var(--strand-ease-out-expo) 0.18s both;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
.strand-detail-panel.is-open .strand-detail-panel__meta,
|
|
2280
|
+
.strand-detail-panel--open .strand-detail-panel__meta,
|
|
2281
|
+
.strand-detail-panel.open .strand-detail-panel__meta {
|
|
2282
|
+
animation: strand-detail-fade var(--strand-duration-slow) var(--strand-ease-out-expo) 0.26s both;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
.strand-detail-panel.is-open .strand-detail-panel__salary,
|
|
2286
|
+
.strand-detail-panel--open .strand-detail-panel__salary,
|
|
2287
|
+
.strand-detail-panel.open .strand-detail-panel__salary {
|
|
2288
|
+
animation: strand-detail-fade var(--strand-duration-slow) var(--strand-ease-out-expo) 0.34s both;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
.strand-detail-panel.is-open .strand-detail-panel__cta,
|
|
2292
|
+
.strand-detail-panel--open .strand-detail-panel__cta,
|
|
2293
|
+
.strand-detail-panel.open .strand-detail-panel__cta {
|
|
2294
|
+
animation: strand-detail-fade var(--strand-duration-slow) var(--strand-ease-out-expo) 0.42s both;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
.strand-detail-panel.is-open .strand-detail-panel__source,
|
|
2298
|
+
.strand-detail-panel--open .strand-detail-panel__source,
|
|
2299
|
+
.strand-detail-panel.open .strand-detail-panel__source {
|
|
2300
|
+
animation: strand-detail-fade var(--strand-duration-slow) var(--strand-ease-out-expo) 0.48s both;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
@keyframes strand-detail-fade {
|
|
2304
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
2305
|
+
to { opacity: 1; transform: translateY(0); }
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
.strand-detail-panel__close {
|
|
2309
|
+
position: absolute;
|
|
2310
|
+
top: 80px;
|
|
2311
|
+
right: var(--strand-space-4);
|
|
2312
|
+
width: 32px;
|
|
2313
|
+
height: 32px;
|
|
2314
|
+
display: flex;
|
|
2315
|
+
align-items: center;
|
|
2316
|
+
justify-content: center;
|
|
2317
|
+
background: transparent;
|
|
2318
|
+
border: 1px solid var(--strand-gray-200);
|
|
2319
|
+
border-radius: var(--strand-radius-md);
|
|
2320
|
+
font-size: var(--strand-text-lg);
|
|
2321
|
+
color: var(--strand-gray-500);
|
|
2322
|
+
cursor: pointer;
|
|
2323
|
+
transition:
|
|
2324
|
+
color var(--strand-duration-fast) ease,
|
|
2325
|
+
border-color var(--strand-duration-fast) ease,
|
|
2326
|
+
background var(--strand-duration-fast) ease;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
.strand-detail-panel__close:hover {
|
|
2330
|
+
color: var(--strand-blue-primary);
|
|
2331
|
+
border-color: rgba(59, 142, 246, 0.3);
|
|
2332
|
+
background: rgba(59, 142, 246, 0.05);
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
.strand-detail-panel__close-text {
|
|
2336
|
+
display: none;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
.strand-detail-panel__close-icon {
|
|
2340
|
+
display: inline;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
.strand-detail-panel__title {
|
|
2344
|
+
font-size: 1.375rem;
|
|
2345
|
+
font-weight: var(--strand-weight-medium);
|
|
2346
|
+
letter-spacing: var(--strand-tracking-tight);
|
|
2347
|
+
color: var(--strand-gray-900);
|
|
2348
|
+
margin: 0 0 var(--strand-space-4);
|
|
2349
|
+
line-height: 1.3;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
.strand-detail-panel__meta {
|
|
2353
|
+
display: flex;
|
|
2354
|
+
flex-direction: column;
|
|
2355
|
+
gap: var(--strand-space-1);
|
|
2356
|
+
padding-bottom: var(--strand-space-4);
|
|
2357
|
+
border-bottom: 1px solid rgba(59, 142, 246, 0.08);
|
|
2358
|
+
margin-bottom: var(--strand-space-5);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
.strand-detail-panel__company,
|
|
2362
|
+
.strand-detail-panel__location {
|
|
2363
|
+
font-size: var(--strand-text-sm);
|
|
2364
|
+
color: var(--strand-gray-700);
|
|
2365
|
+
line-height: 1.4;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
.strand-detail-panel__salary {
|
|
2369
|
+
font-family: var(--strand-font-mono);
|
|
2370
|
+
font-size: var(--strand-text-lg);
|
|
2371
|
+
font-weight: var(--strand-weight-light);
|
|
2372
|
+
letter-spacing: var(--strand-tracking-tight);
|
|
2373
|
+
color: var(--strand-blue-midnight);
|
|
2374
|
+
margin-bottom: var(--strand-space-6);
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
.strand-detail-panel__cta {
|
|
2378
|
+
display: block;
|
|
2379
|
+
text-align: center;
|
|
2380
|
+
text-decoration: none;
|
|
2381
|
+
margin-bottom: var(--strand-space-3);
|
|
2382
|
+
transition:
|
|
2383
|
+
box-shadow var(--strand-duration-fast) ease,
|
|
2384
|
+
transform var(--strand-duration-fast) var(--strand-ease-out-expo);
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
.strand-detail-panel__cta:hover {
|
|
2388
|
+
box-shadow: 0 8px 24px rgba(29, 90, 216, 0.25);
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
.strand-detail-panel__cta:active {
|
|
2392
|
+
transform: translateY(0) scale(0.98);
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
.strand-detail-panel__source {
|
|
2396
|
+
display: block;
|
|
2397
|
+
font-family: var(--strand-font-mono);
|
|
2398
|
+
font-size: var(--strand-text-xs);
|
|
2399
|
+
letter-spacing: var(--strand-tracking-wide);
|
|
2400
|
+
color: var(--strand-gray-400);
|
|
2401
|
+
text-align: center;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
/* ──────────────────────────────────────────────
|
|
2405
|
+
MAP LOADING (instrument startup screen)
|
|
2406
|
+
──────────────────────────────────────────────
|
|
2407
|
+
The full-bleed loading screen that hides the instrument while
|
|
2408
|
+
it boots. Centered spinner, mono caption, sweeping bar.
|
|
2409
|
+
Fades out via opacity transition. */
|
|
2410
|
+
|
|
2411
|
+
.strand-map-loading {
|
|
2412
|
+
position: absolute;
|
|
2413
|
+
inset: 0;
|
|
2414
|
+
z-index: 30;
|
|
2415
|
+
display: flex;
|
|
2416
|
+
flex-direction: column;
|
|
2417
|
+
align-items: center;
|
|
2418
|
+
justify-content: center;
|
|
2419
|
+
gap: var(--strand-space-6);
|
|
2420
|
+
background: var(--strand-instrument-bg);
|
|
2421
|
+
transition: opacity 0.6s ease;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
.strand-map-loading.is-hidden,
|
|
2425
|
+
.strand-map-loading--hidden,
|
|
2426
|
+
.strand-map-loading.hidden {
|
|
2427
|
+
display: none;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
.strand-map-loading__spinner {
|
|
2431
|
+
width: 40px;
|
|
2432
|
+
height: 40px;
|
|
2433
|
+
border: 1.5px solid rgba(59, 142, 246, 0.15);
|
|
2434
|
+
border-top-color: rgba(59, 142, 246, 0.7);
|
|
2435
|
+
border-radius: var(--strand-radius-full);
|
|
2436
|
+
animation: strand-map-loading-spin 1s linear infinite;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
@keyframes strand-map-loading-spin {
|
|
2440
|
+
to { transform: rotate(360deg); }
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
.strand-map-loading__text {
|
|
2444
|
+
font-family: var(--strand-font-mono);
|
|
2445
|
+
font-size: 11px;
|
|
2446
|
+
letter-spacing: var(--strand-tracking-widest);
|
|
2447
|
+
text-transform: uppercase;
|
|
2448
|
+
color: var(--strand-instrument-text-tertiary);
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
.strand-map-loading__bar {
|
|
2452
|
+
width: 160px;
|
|
2453
|
+
height: 1px;
|
|
2454
|
+
background: rgba(59, 142, 246, 0.1);
|
|
2455
|
+
border-radius: var(--strand-radius-sm);
|
|
2456
|
+
overflow: hidden;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
.strand-map-loading__bar::after {
|
|
2460
|
+
content: "";
|
|
2461
|
+
display: block;
|
|
2462
|
+
height: 100%;
|
|
2463
|
+
width: 40%;
|
|
2464
|
+
background: linear-gradient(
|
|
2465
|
+
90deg,
|
|
2466
|
+
transparent,
|
|
2467
|
+
rgba(59, 142, 246, 0.6),
|
|
2468
|
+
transparent
|
|
2469
|
+
);
|
|
2470
|
+
animation: strand-map-loading-sweep 1.4s var(--strand-ease-in-out-sine) infinite;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
@keyframes strand-map-loading-sweep {
|
|
2474
|
+
0% { transform: translateX(-200%); }
|
|
2475
|
+
100% { transform: translateX(500%); }
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
/* ──────────────────────────────────────────────
|
|
2479
|
+
MAP PINS + CLUSTER MARKERS
|
|
2480
|
+
──────────────────────────────────────────────
|
|
2481
|
+
Pins are the data points the instrument plots on the map.
|
|
2482
|
+
Each pin gets a sector color (tech/health/trades/finance) and
|
|
2483
|
+
carries a continuous breathing ring + an initial beacon ring
|
|
2484
|
+
on spawn. Cluster markers are the metro-level aggregates
|
|
2485
|
+
shown when no query is active. */
|
|
2486
|
+
|
|
2487
|
+
.strand-map-pin {
|
|
2488
|
+
width: 24px;
|
|
2489
|
+
height: 24px;
|
|
2490
|
+
border-radius: var(--strand-radius-full);
|
|
2491
|
+
border: 2.5px solid rgba(255, 255, 255, 0.85);
|
|
2492
|
+
cursor: pointer;
|
|
2493
|
+
animation: strand-map-pin-spawn var(--strand-duration-slow) var(--strand-ease-spring) both;
|
|
2494
|
+
transition:
|
|
2495
|
+
scale var(--strand-duration-fast) var(--strand-ease-out-expo),
|
|
2496
|
+
background var(--strand-duration-fast) var(--strand-ease-out-quart);
|
|
2497
|
+
box-shadow:
|
|
2498
|
+
0 0 0 3px rgba(255, 255, 255, 0.15),
|
|
2499
|
+
0 0 12px currentColor,
|
|
2500
|
+
0 0 28px currentColor,
|
|
2501
|
+
0 0 56px currentColor;
|
|
2502
|
+
position: relative;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
.strand-map-pin::before {
|
|
2506
|
+
content: "";
|
|
2507
|
+
position: absolute;
|
|
2508
|
+
inset: -8px;
|
|
2509
|
+
border-radius: var(--strand-radius-full);
|
|
2510
|
+
border: 1.5px solid currentColor;
|
|
2511
|
+
animation: strand-map-pin-breathe var(--strand-duration-cinematic-slow) var(--strand-ease-in-out-sine) infinite;
|
|
2512
|
+
animation-delay: inherit;
|
|
2513
|
+
pointer-events: none;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
.strand-map-pin::after {
|
|
2517
|
+
content: "";
|
|
2518
|
+
position: absolute;
|
|
2519
|
+
inset: -4px;
|
|
2520
|
+
border-radius: var(--strand-radius-full);
|
|
2521
|
+
border: 2px solid rgba(255, 255, 255, 0.5);
|
|
2522
|
+
opacity: 0;
|
|
2523
|
+
animation: strand-map-pin-beacon var(--strand-duration-cinematic) var(--strand-ease-out-quart) forwards;
|
|
2524
|
+
animation-delay: inherit;
|
|
2525
|
+
pointer-events: none;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
.strand-map-pin:hover {
|
|
2529
|
+
scale: 1.6;
|
|
2530
|
+
box-shadow:
|
|
2531
|
+
0 0 0 4px rgba(255, 255, 255, 0.3),
|
|
2532
|
+
0 0 16px currentColor,
|
|
2533
|
+
0 0 40px currentColor,
|
|
2534
|
+
0 0 72px currentColor;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
.strand-map-pin:hover::before {
|
|
2538
|
+
animation: none;
|
|
2539
|
+
opacity: 0;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
.strand-map-pin:hover::after {
|
|
2543
|
+
animation: strand-map-pin-pulse 1s var(--strand-ease-out-expo) infinite;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
.strand-map-pin--tech {
|
|
2547
|
+
background: var(--strand-sector-tech);
|
|
2548
|
+
color: var(--strand-sector-tech);
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
.strand-map-pin--health {
|
|
2552
|
+
background: var(--strand-sector-health);
|
|
2553
|
+
border-color: rgba(20, 184, 166, 0.7);
|
|
2554
|
+
color: var(--strand-sector-health);
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
.strand-map-pin--trades {
|
|
2558
|
+
background: var(--strand-sector-trades);
|
|
2559
|
+
border-color: rgba(34, 211, 238, 0.7);
|
|
2560
|
+
color: var(--strand-sector-trades);
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
.strand-map-pin--finance {
|
|
2564
|
+
background: var(--strand-sector-finance);
|
|
2565
|
+
border-color: rgba(139, 92, 246, 0.7);
|
|
2566
|
+
color: var(--strand-sector-finance);
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
.strand-map-pin.is-highlighted,
|
|
2570
|
+
.strand-map-pin--highlighted {
|
|
2571
|
+
scale: 1.6;
|
|
2572
|
+
box-shadow:
|
|
2573
|
+
0 0 16px currentColor,
|
|
2574
|
+
0 0 36px currentColor,
|
|
2575
|
+
0 0 64px rgba(59, 142, 246, 0.4);
|
|
2576
|
+
z-index: 10;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
.strand-map-pin.is-dimmed,
|
|
2580
|
+
.strand-map-pin--dimmed {
|
|
2581
|
+
opacity: 0.15;
|
|
2582
|
+
transition: opacity var(--strand-duration-fast) ease;
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
@keyframes strand-map-pin-pulse {
|
|
2586
|
+
0% { box-shadow: 0 0 0 0 rgba(59, 142, 246, 0.6); }
|
|
2587
|
+
100% { box-shadow: 0 0 0 18px rgba(59, 142, 246, 0); }
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
@keyframes strand-map-pin-spawn {
|
|
2591
|
+
0% { scale: 0; opacity: 0; }
|
|
2592
|
+
50% { opacity: 1; }
|
|
2593
|
+
100% { scale: 1; }
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
@keyframes strand-map-pin-beacon {
|
|
2597
|
+
0% { scale: 1; opacity: 0.7; }
|
|
2598
|
+
100% { scale: 3; opacity: 0; }
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
@keyframes strand-map-pin-breathe {
|
|
2602
|
+
0%, 100% { opacity: 0.25; scale: 1; }
|
|
2603
|
+
50% { opacity: 0.55; scale: 1.5; }
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
/* ── Cluster marker (metro-level aggregate) ── */
|
|
2607
|
+
.strand-cluster-marker {
|
|
2608
|
+
display: flex;
|
|
2609
|
+
align-items: center;
|
|
2610
|
+
justify-content: center;
|
|
2611
|
+
border-radius: var(--strand-radius-full);
|
|
2612
|
+
font-family: var(--strand-font-mono);
|
|
2613
|
+
font-size: var(--strand-text-xs);
|
|
2614
|
+
font-weight: var(--strand-weight-semibold);
|
|
2615
|
+
color: var(--strand-instrument-text-primary);
|
|
2616
|
+
border: 2px solid rgba(59, 142, 246, 0.6);
|
|
2617
|
+
box-shadow:
|
|
2618
|
+
0 0 20px rgba(59, 142, 246, 0.4),
|
|
2619
|
+
0 0 40px rgba(59, 142, 246, 0.15);
|
|
2620
|
+
cursor: pointer;
|
|
2621
|
+
transition:
|
|
2622
|
+
transform var(--strand-duration-fast) var(--strand-ease-out-expo),
|
|
2623
|
+
box-shadow var(--strand-duration-fast) ease;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
.strand-cluster-marker:hover {
|
|
2627
|
+
transform: scale(1.15);
|
|
2628
|
+
box-shadow:
|
|
2629
|
+
0 0 28px rgba(59, 142, 246, 0.6),
|
|
2630
|
+
0 0 56px rgba(59, 142, 246, 0.25);
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
/* ──────────────────────────────────────────────
|
|
2634
|
+
MAPLIBRE COMPATIBILITY OVERRIDES
|
|
2635
|
+
──────────────────────────────────────────────
|
|
2636
|
+
When MapLibre GL is rendered inside an instrument viewport,
|
|
2637
|
+
its default chrome must be muted and re-themed to fit the
|
|
2638
|
+
dark cabinet. These overrides should ONLY take effect inside
|
|
2639
|
+
.strand-instrument-viewport so they do not pollute lighter
|
|
2640
|
+
maps elsewhere on the site. */
|
|
2641
|
+
|
|
2642
|
+
.strand-instrument-viewport .maplibregl-marker {
|
|
2643
|
+
z-index: 6 !important;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
.strand-instrument-viewport .maplibregl-ctrl-bottom-left {
|
|
2647
|
+
opacity: 0.25;
|
|
2648
|
+
transition: opacity var(--strand-duration-fast) ease;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
.strand-instrument-viewport .maplibregl-ctrl-bottom-left:hover {
|
|
2652
|
+
opacity: 0.7;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
.strand-instrument-viewport .maplibregl-ctrl-bottom-right {
|
|
2656
|
+
bottom: 40px;
|
|
2657
|
+
right: 12px;
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
.strand-instrument-viewport .maplibregl-ctrl-bottom-right .maplibregl-ctrl-group {
|
|
2661
|
+
background: rgba(15, 25, 42, 0.85);
|
|
2662
|
+
border: 1px solid rgba(59, 142, 246, 0.2);
|
|
2663
|
+
border-radius: var(--strand-radius-md);
|
|
2664
|
+
-webkit-backdrop-filter: blur(8px);
|
|
2665
|
+
backdrop-filter: blur(8px);
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
.strand-instrument-viewport .maplibregl-ctrl-bottom-right .maplibregl-ctrl-group button {
|
|
2669
|
+
width: 36px;
|
|
2670
|
+
height: 36px;
|
|
2671
|
+
background: transparent;
|
|
2672
|
+
border: none;
|
|
2673
|
+
color: var(--strand-gray-300);
|
|
2674
|
+
cursor: pointer;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
.strand-instrument-viewport .maplibregl-ctrl-bottom-right .maplibregl-ctrl-group button:hover {
|
|
2678
|
+
color: var(--strand-blue-primary);
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
.strand-instrument-viewport .maplibregl-ctrl-bottom-right .maplibregl-ctrl-group button + button {
|
|
2682
|
+
border-top: 1px solid rgba(59, 142, 246, 0.15);
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
.strand-instrument-viewport .maplibregl-popup-content {
|
|
2686
|
+
font-family: var(--strand-font-mono);
|
|
2687
|
+
font-size: var(--strand-text-xs);
|
|
2688
|
+
background: var(--strand-instrument-bg-translucent);
|
|
2689
|
+
color: var(--strand-instrument-text-primary);
|
|
2690
|
+
border: 1px solid rgba(59, 142, 246, 0.2);
|
|
2691
|
+
border-radius: var(--strand-radius-md);
|
|
2692
|
+
padding: var(--strand-space-2) var(--strand-space-3);
|
|
2693
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
.strand-instrument-viewport .maplibregl-popup-tip {
|
|
2697
|
+
border-top-color: var(--strand-instrument-bg-translucent);
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
/* ──────────────────────────────────────────────
|
|
2701
|
+
RESPONSIVE
|
|
2702
|
+
──────────────────────────────────────────────
|
|
2703
|
+
On mobile, the results panel becomes a bottom sheet, the
|
|
2704
|
+
detail panel slides up from the bottom, the search bar
|
|
2705
|
+
takes the full width, and the FUI overlays (legend, coord
|
|
2706
|
+
readout) hide because they would crowd small screens. */
|
|
2707
|
+
|
|
2708
|
+
@media (max-width: 768px) {
|
|
2709
|
+
.strand-results-panel {
|
|
2710
|
+
top: auto;
|
|
2711
|
+
bottom: 0;
|
|
2712
|
+
left: 0;
|
|
2713
|
+
width: 100%;
|
|
2714
|
+
max-height: 40vh;
|
|
2715
|
+
border-radius: var(--strand-radius-lg) var(--strand-radius-lg) 0 0;
|
|
2716
|
+
overflow-y: auto;
|
|
2717
|
+
-webkit-overflow-scrolling: touch;
|
|
2718
|
+
overscroll-behavior: contain;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
.strand-results-panel__items {
|
|
2722
|
+
max-height: none;
|
|
2723
|
+
overflow-y: visible;
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
.strand-search-bar,
|
|
2727
|
+
.strand-search-bar--shifted {
|
|
2728
|
+
top: var(--strand-space-3);
|
|
2729
|
+
left: var(--strand-space-4) !important;
|
|
2730
|
+
width: calc(100% - (var(--strand-space-4) * 2)) !important;
|
|
2731
|
+
transform: none !important;
|
|
2732
|
+
transition: none !important;
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
.strand-search-bar__action {
|
|
2736
|
+
width: 44px;
|
|
2737
|
+
height: 44px;
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
.strand-map-pin {
|
|
2741
|
+
width: 24px;
|
|
2742
|
+
height: 24px;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
@media (max-width: 640px) {
|
|
2747
|
+
.strand-detail-panel {
|
|
2748
|
+
width: 100%;
|
|
2749
|
+
height: auto;
|
|
2750
|
+
max-height: 70vh;
|
|
2751
|
+
top: auto;
|
|
2752
|
+
bottom: 0;
|
|
2753
|
+
border-radius: var(--strand-radius-xl) var(--strand-radius-xl) 0 0;
|
|
2754
|
+
padding: 0 var(--strand-space-4) var(--strand-space-6);
|
|
2755
|
+
transform: translateY(100%);
|
|
2756
|
+
box-shadow: 0 -4px 32px rgba(0, 0, 0, 0.2);
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
.strand-detail-panel.is-open,
|
|
2760
|
+
.strand-detail-panel--open,
|
|
2761
|
+
.strand-detail-panel.open {
|
|
2762
|
+
transform: translateY(0);
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
.strand-detail-panel__close {
|
|
2766
|
+
position: relative;
|
|
2767
|
+
top: 0;
|
|
2768
|
+
left: 0;
|
|
2769
|
+
right: 0;
|
|
2770
|
+
width: 100%;
|
|
2771
|
+
height: 48px;
|
|
2772
|
+
border-radius: 0;
|
|
2773
|
+
border: none;
|
|
2774
|
+
border-bottom: 1px solid var(--strand-gray-200);
|
|
2775
|
+
background: transparent;
|
|
2776
|
+
font-family: var(--strand-font-mono);
|
|
2777
|
+
font-size: var(--strand-text-sm);
|
|
2778
|
+
font-weight: var(--strand-weight-medium);
|
|
2779
|
+
letter-spacing: var(--strand-tracking-wider);
|
|
2780
|
+
color: var(--strand-blue-primary);
|
|
2781
|
+
margin-bottom: var(--strand-space-4);
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
.strand-detail-panel__close:hover {
|
|
2785
|
+
background: var(--strand-blue-glow);
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
.strand-detail-panel__close-icon {
|
|
2789
|
+
display: none;
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
.strand-detail-panel__close-text {
|
|
2793
|
+
display: inline;
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
.strand-detail-panel > .strand-overline {
|
|
2797
|
+
margin-bottom: var(--strand-space-2);
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
.strand-map-legend,
|
|
2801
|
+
.strand-coord-readout {
|
|
2802
|
+
display: none;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
.strand-search-bar__input {
|
|
2806
|
+
font-size: 16px;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
/* ──────────────────────────────────────────────
|
|
2811
|
+
REDUCED MOTION
|
|
2812
|
+
──────────────────────────────────────────────
|
|
2813
|
+
Strip every animation and transition from the FUI primitives
|
|
2814
|
+
when the user has prefers-reduced-motion. The instrument is
|
|
2815
|
+
still readable, just static. */
|
|
2816
|
+
|
|
2817
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2818
|
+
.strand-scanline,
|
|
2819
|
+
.strand-scanline--ambient {
|
|
2820
|
+
display: none;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
.strand-instrument-viewport--full-bleed::after {
|
|
2824
|
+
animation: none;
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
.strand-map-pin {
|
|
2828
|
+
animation: none;
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
.strand-map-pin::before,
|
|
2832
|
+
.strand-map-pin::after {
|
|
2833
|
+
animation: none;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
.strand-map-pin:hover {
|
|
2837
|
+
animation: none;
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
.strand-map-pin:hover::before,
|
|
2841
|
+
.strand-map-pin:hover::after {
|
|
2842
|
+
animation: none;
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
.strand-detail-panel {
|
|
2846
|
+
transition: none;
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
.strand-detail-panel.is-open > .strand-overline,
|
|
2850
|
+
.strand-detail-panel--open > .strand-overline,
|
|
2851
|
+
.strand-detail-panel.open > .strand-overline,
|
|
2852
|
+
.strand-detail-panel.is-open .strand-detail-panel__title,
|
|
2853
|
+
.strand-detail-panel--open .strand-detail-panel__title,
|
|
2854
|
+
.strand-detail-panel.open .strand-detail-panel__title,
|
|
2855
|
+
.strand-detail-panel.is-open .strand-detail-panel__meta,
|
|
2856
|
+
.strand-detail-panel--open .strand-detail-panel__meta,
|
|
2857
|
+
.strand-detail-panel.open .strand-detail-panel__meta,
|
|
2858
|
+
.strand-detail-panel.is-open .strand-detail-panel__salary,
|
|
2859
|
+
.strand-detail-panel--open .strand-detail-panel__salary,
|
|
2860
|
+
.strand-detail-panel.open .strand-detail-panel__salary,
|
|
2861
|
+
.strand-detail-panel.is-open .strand-detail-panel__cta,
|
|
2862
|
+
.strand-detail-panel--open .strand-detail-panel__cta,
|
|
2863
|
+
.strand-detail-panel.open .strand-detail-panel__cta,
|
|
2864
|
+
.strand-detail-panel.is-open .strand-detail-panel__source,
|
|
2865
|
+
.strand-detail-panel--open .strand-detail-panel__source,
|
|
2866
|
+
.strand-detail-panel.open .strand-detail-panel__source {
|
|
2867
|
+
animation: none;
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
.strand-map-legend__title::before {
|
|
2871
|
+
animation: none;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
.strand-coord-readout::before {
|
|
2875
|
+
animation: none;
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
.strand-map-loading__spinner {
|
|
2879
|
+
animation: none;
|
|
2880
|
+
border-color: rgba(59, 142, 246, 0.4);
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
.strand-search-bar__action--locating {
|
|
2884
|
+
animation: none;
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
|
|
2889
|
+
/* Link */
|
|
2890
|
+
/*! Strand UI | MIT License | dillingerstaffing.com */
|
|
2891
|
+
|
|
2892
|
+
/*! Strand UI | MIT License | dillingerstaffing.com */
|
|
2893
|
+
|
|
2894
|
+
.strand-link {
|
|
2895
|
+
color: var(--strand-blue-primary);
|
|
2896
|
+
text-decoration: none;
|
|
2897
|
+
font-family: var(--strand-font-sans);
|
|
2898
|
+
background-image: linear-gradient(currentColor, currentColor);
|
|
2899
|
+
background-position: 0% 100%;
|
|
2900
|
+
background-repeat: no-repeat;
|
|
2901
|
+
background-size: 0% 1px;
|
|
2902
|
+
transition: background-size var(--strand-duration-normal) var(--strand-ease-out-expo);
|
|
2903
|
+
cursor: pointer;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
.strand-link:hover {
|
|
2907
|
+
background-size: 100% 1px;
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
/* ── Focus ring ── */
|
|
2911
|
+
.strand-link:focus-visible {
|
|
2912
|
+
outline: 2px solid var(--strand-blue-primary);
|
|
2913
|
+
outline-offset: 2px;
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
/* ── CTA link variant ── */
|
|
2917
|
+
.strand-link--cta {
|
|
2918
|
+
display: inline-flex;
|
|
2919
|
+
align-items: center;
|
|
2920
|
+
min-height: var(--strand-touch-target);
|
|
2921
|
+
padding-block: var(--strand-space-2);
|
|
2922
|
+
font-size: var(--strand-text-sm);
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
.strand-link--cta:hover {
|
|
2926
|
+
color: var(--strand-blue-vivid);
|
|
1188
2927
|
}
|
|
1189
2928
|
|
|
1190
2929
|
/* ── Monospace link variant (footer, overline-style) ── */
|
|
@@ -1215,195 +2954,302 @@
|
|
|
1215
2954
|
|
|
1216
2955
|
/* ── Nav bar ── */
|
|
1217
2956
|
.strand-nav {
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
/* ── Inner layout ── */
|
|
1227
|
-
.strand-nav__inner {
|
|
1228
|
-
display: flex;
|
|
1229
|
-
align-items: center;
|
|
1230
|
-
height: 100%;
|
|
1231
|
-
padding: 0 var(--strand-space-6);
|
|
1232
|
-
max-width: 1280px;
|
|
1233
|
-
margin: 0 auto;
|
|
2957
|
+
position: relative;
|
|
2958
|
+
width: 100%;
|
|
2959
|
+
height: var(--strand-nav-height);
|
|
2960
|
+
background: var(--strand-surface-elevated);
|
|
2961
|
+
border-bottom: 1px solid var(--strand-gray-200);
|
|
2962
|
+
font-family: var(--strand-font-sans);
|
|
1234
2963
|
}
|
|
1235
2964
|
|
|
1236
|
-
/* ──
|
|
2965
|
+
/* ── Inner layout ── */
|
|
2966
|
+
.strand-nav__inner {
|
|
2967
|
+
display: flex;
|
|
2968
|
+
align-items: center;
|
|
2969
|
+
height: 100%;
|
|
2970
|
+
padding: 0 var(--strand-space-6);
|
|
2971
|
+
max-width: 1280px;
|
|
2972
|
+
margin: 0 auto;
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
/* ── Logo ──
|
|
2976
|
+
DL Part IV.5 brand identifier: mono uppercase wide letter-spacing,
|
|
2977
|
+
semibold weight, blue-midnight color. The logo is the only typographic
|
|
2978
|
+
identity on the page, so it carries the brand even at this small size. */
|
|
1237
2979
|
.strand-nav__logo {
|
|
1238
|
-
|
|
1239
|
-
|
|
2980
|
+
flex-shrink: 0;
|
|
2981
|
+
margin-right: var(--strand-space-8);
|
|
2982
|
+
font-family: var(--strand-font-mono);
|
|
2983
|
+
font-size: var(--strand-text-sm);
|
|
2984
|
+
font-weight: var(--strand-weight-semibold);
|
|
2985
|
+
letter-spacing: var(--strand-tracking-widest);
|
|
2986
|
+
text-transform: uppercase;
|
|
2987
|
+
color: var(--strand-blue-midnight);
|
|
2988
|
+
text-decoration: none;
|
|
2989
|
+
position: relative;
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
/* ── Logo with animated pulse underline (DL Principle 7: alive signal) ──
|
|
2993
|
+
A 2px blue underline that pulses in slow rhythm. Use on the brand
|
|
2994
|
+
identifier for the active site to signal "this is the live system." */
|
|
2995
|
+
.strand-nav__logo--pulse::after {
|
|
2996
|
+
content: "";
|
|
2997
|
+
position: absolute;
|
|
2998
|
+
bottom: -4px;
|
|
2999
|
+
left: 0;
|
|
3000
|
+
right: 0;
|
|
3001
|
+
height: 2px;
|
|
3002
|
+
background: var(--strand-blue-primary);
|
|
3003
|
+
border-radius: var(--strand-radius-sm);
|
|
3004
|
+
animation: strand-nav-logo-pulse 2s var(--strand-ease-in-out-sine) infinite;
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
@keyframes strand-nav-logo-pulse {
|
|
3008
|
+
0%,
|
|
3009
|
+
100% {
|
|
3010
|
+
opacity: 1;
|
|
3011
|
+
}
|
|
3012
|
+
50% {
|
|
3013
|
+
opacity: 0.4;
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3018
|
+
.strand-nav__logo--pulse::after {
|
|
3019
|
+
animation: none;
|
|
3020
|
+
opacity: 0.6;
|
|
3021
|
+
}
|
|
1240
3022
|
}
|
|
1241
3023
|
|
|
1242
|
-
/* ── Desktop items ──
|
|
3024
|
+
/* ── Desktop items ──
|
|
3025
|
+
Pushed to the right edge so logo and primary nav have visual breathing
|
|
3026
|
+
room. The old (pre-v0.5) "flex: 1" caused items to cluster against the
|
|
3027
|
+
logo, leaving the right edge empty. margin-left auto restores the
|
|
3028
|
+
space-between layout most navbars expect. */
|
|
1243
3029
|
.strand-nav__items {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
3030
|
+
display: flex;
|
|
3031
|
+
align-items: center;
|
|
3032
|
+
gap: var(--strand-space-6);
|
|
3033
|
+
margin-left: auto;
|
|
3034
|
+
list-style: none;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
/* ── Nav slot ──
|
|
3038
|
+
A content slot that lives in the nav. Typical uses: an account/auth
|
|
3039
|
+
affordance, a utility button, a search field. Position behavior is
|
|
3040
|
+
context-aware:
|
|
3041
|
+
- If the slot follows .strand-nav__items (which already absorbs free
|
|
3042
|
+
space via margin-left: auto), the slot sits 24px to its right so
|
|
3043
|
+
it does not visually crash into the last nav link.
|
|
3044
|
+
- If the slot stands alone (no items list before it, as on simpler
|
|
3045
|
+
nav bars), the slot pushes itself to the right edge with
|
|
3046
|
+
margin-left: auto.
|
|
3047
|
+
This keeps a single class usable across both long and short navs
|
|
3048
|
+
without modifiers. */
|
|
3049
|
+
.strand-nav__slot {
|
|
3050
|
+
display: flex;
|
|
3051
|
+
align-items: center;
|
|
3052
|
+
margin-left: auto;
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
.strand-nav__items + .strand-nav__slot {
|
|
3056
|
+
margin-left: var(--strand-space-6);
|
|
1248
3057
|
}
|
|
1249
3058
|
|
|
1250
3059
|
/* ── Links ── */
|
|
1251
3060
|
.strand-nav__link {
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
3061
|
+
color: var(--strand-gray-600);
|
|
3062
|
+
text-decoration: none;
|
|
3063
|
+
font-size: var(--strand-text-sm);
|
|
3064
|
+
font-weight: var(--strand-weight-medium);
|
|
3065
|
+
transition: color var(--strand-duration-fast) var(--strand-ease-out-quart);
|
|
1257
3066
|
}
|
|
1258
3067
|
|
|
1259
3068
|
.strand-nav__link:hover {
|
|
1260
|
-
|
|
3069
|
+
color: var(--strand-gray-900);
|
|
1261
3070
|
}
|
|
1262
3071
|
|
|
1263
3072
|
.strand-nav__link:focus-visible {
|
|
1264
|
-
|
|
1265
|
-
|
|
3073
|
+
outline: 2px solid var(--strand-blue-primary);
|
|
3074
|
+
outline-offset: 2px;
|
|
1266
3075
|
}
|
|
1267
3076
|
|
|
1268
3077
|
.strand-nav__link--active {
|
|
1269
|
-
|
|
1270
|
-
|
|
3078
|
+
color: var(--strand-blue-primary);
|
|
3079
|
+
font-weight: var(--strand-weight-medium);
|
|
1271
3080
|
}
|
|
1272
3081
|
|
|
1273
3082
|
/* ── Actions ── */
|
|
1274
3083
|
.strand-nav__actions {
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
3084
|
+
display: flex;
|
|
3085
|
+
align-items: center;
|
|
3086
|
+
gap: var(--strand-space-3);
|
|
3087
|
+
margin-left: auto;
|
|
1279
3088
|
}
|
|
1280
3089
|
|
|
1281
3090
|
/* ── Hamburger ── */
|
|
1282
3091
|
.strand-nav__hamburger {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
3092
|
+
display: none;
|
|
3093
|
+
align-items: center;
|
|
3094
|
+
justify-content: center;
|
|
3095
|
+
width: 40px;
|
|
3096
|
+
height: 40px;
|
|
3097
|
+
margin-left: auto;
|
|
3098
|
+
padding: 0;
|
|
3099
|
+
border: none;
|
|
3100
|
+
border-radius: var(--strand-radius-md);
|
|
3101
|
+
background: transparent;
|
|
3102
|
+
color: var(--strand-gray-600);
|
|
3103
|
+
cursor: pointer;
|
|
3104
|
+
transition: background var(--strand-duration-fast)
|
|
3105
|
+
var(--strand-ease-out-quart);
|
|
1296
3106
|
}
|
|
1297
3107
|
|
|
1298
3108
|
.strand-nav__hamburger:hover {
|
|
1299
|
-
|
|
3109
|
+
background: var(--strand-gray-200);
|
|
1300
3110
|
}
|
|
1301
3111
|
|
|
1302
3112
|
.strand-nav__hamburger:focus-visible {
|
|
1303
|
-
|
|
1304
|
-
|
|
3113
|
+
outline: 2px solid var(--strand-blue-primary);
|
|
3114
|
+
outline-offset: 2px;
|
|
1305
3115
|
}
|
|
1306
3116
|
|
|
1307
3117
|
.strand-nav__hamburger-icon {
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
3118
|
+
display: block;
|
|
3119
|
+
width: 20px;
|
|
3120
|
+
height: 2px;
|
|
3121
|
+
background: currentColor;
|
|
3122
|
+
position: relative;
|
|
1313
3123
|
}
|
|
1314
3124
|
|
|
1315
3125
|
.strand-nav__hamburger-icon::before,
|
|
1316
3126
|
.strand-nav__hamburger-icon::after {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
3127
|
+
content: "";
|
|
3128
|
+
position: absolute;
|
|
3129
|
+
left: 0;
|
|
3130
|
+
width: 100%;
|
|
3131
|
+
height: 2px;
|
|
3132
|
+
background: currentColor;
|
|
1323
3133
|
}
|
|
1324
3134
|
|
|
1325
3135
|
.strand-nav__hamburger-icon::before {
|
|
1326
|
-
|
|
3136
|
+
top: -6px;
|
|
1327
3137
|
}
|
|
1328
3138
|
|
|
1329
3139
|
.strand-nav__hamburger-icon::after {
|
|
1330
|
-
|
|
3140
|
+
top: 6px;
|
|
1331
3141
|
}
|
|
1332
3142
|
|
|
1333
|
-
/* ── Mobile menu ──
|
|
3143
|
+
/* ── Mobile menu ──
|
|
3144
|
+
Drops down from beneath the nav as an overlay. Hidden by default;
|
|
3145
|
+
show via JS by adding strand-nav__mobile-menu--open. The position:
|
|
3146
|
+
fixed (vs absolute on the parent nav) means the menu floats above
|
|
3147
|
+
page content instead of pushing layout. */
|
|
1334
3148
|
.strand-nav__mobile-menu {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
3149
|
+
display: none;
|
|
3150
|
+
position: fixed;
|
|
3151
|
+
top: var(--strand-nav-height);
|
|
3152
|
+
left: 0;
|
|
3153
|
+
right: 0;
|
|
3154
|
+
flex-direction: column;
|
|
3155
|
+
width: 100%;
|
|
3156
|
+
padding: var(--strand-space-4) var(--strand-space-6);
|
|
3157
|
+
background: var(--strand-surface-elevated);
|
|
3158
|
+
border-bottom: 1px solid var(--strand-gray-200);
|
|
3159
|
+
box-shadow: var(--strand-elevation-2);
|
|
3160
|
+
z-index: 99;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
.strand-nav__mobile-menu--open {
|
|
3164
|
+
display: flex;
|
|
1341
3165
|
}
|
|
1342
3166
|
|
|
1343
3167
|
.strand-nav__mobile-link {
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
3168
|
+
display: block;
|
|
3169
|
+
padding: var(--strand-space-3) 0;
|
|
3170
|
+
color: var(--strand-gray-600);
|
|
3171
|
+
text-decoration: none;
|
|
3172
|
+
font-size: var(--strand-text-sm);
|
|
3173
|
+
font-weight: var(--strand-weight-medium);
|
|
3174
|
+
transition: color var(--strand-duration-fast) var(--strand-ease-out-quart);
|
|
1351
3175
|
}
|
|
1352
3176
|
|
|
1353
3177
|
.strand-nav__mobile-link:hover {
|
|
1354
|
-
|
|
3178
|
+
color: var(--strand-gray-900);
|
|
1355
3179
|
}
|
|
1356
3180
|
|
|
1357
3181
|
.strand-nav__mobile-link--active {
|
|
1358
|
-
|
|
3182
|
+
color: var(--strand-blue-primary);
|
|
3183
|
+
font-weight: var(--strand-weight-semibold);
|
|
1359
3184
|
}
|
|
1360
3185
|
|
|
1361
|
-
/* ── Responsive ──
|
|
3186
|
+
/* ── Responsive ──
|
|
3187
|
+
At mobile widths the desktop items + actions hide and the
|
|
3188
|
+
hamburger button shows. The mobile menu stays display:none until
|
|
3189
|
+
JS toggles strand-nav__mobile-menu--open on click. */
|
|
1362
3190
|
@media (max-width: 767px) {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
3191
|
+
.strand-nav__items {
|
|
3192
|
+
display: none;
|
|
3193
|
+
}
|
|
1366
3194
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
3195
|
+
.strand-nav__actions {
|
|
3196
|
+
display: none;
|
|
3197
|
+
}
|
|
1370
3198
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
3199
|
+
/* The slot remains visible on mobile because it typically carries the
|
|
3200
|
+
account affordance (avatar + sign-in/out) which should stay reachable
|
|
3201
|
+
from every viewport. Its margin-left collapses on mobile because
|
|
3202
|
+
the only remaining siblings are the logo (left) and the hamburger
|
|
3203
|
+
(right), and margin-left:auto on one of them handles the layout. */
|
|
3204
|
+
.strand-nav__slot {
|
|
3205
|
+
margin-left: auto;
|
|
3206
|
+
}
|
|
1374
3207
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
3208
|
+
.strand-nav__hamburger {
|
|
3209
|
+
display: inline-flex;
|
|
3210
|
+
}
|
|
1378
3211
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
3212
|
+
.strand-nav {
|
|
3213
|
+
height: auto;
|
|
3214
|
+
min-height: var(--strand-nav-height);
|
|
3215
|
+
}
|
|
1383
3216
|
}
|
|
1384
3217
|
|
|
1385
3218
|
/* ── Glassmorphic variant (DL 11.5: "white or glassmorphic") ── */
|
|
1386
3219
|
.strand-nav--glass {
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
3220
|
+
position: fixed;
|
|
3221
|
+
top: 0;
|
|
3222
|
+
left: 0;
|
|
3223
|
+
right: 0;
|
|
3224
|
+
z-index: 100;
|
|
3225
|
+
height: auto;
|
|
3226
|
+
padding: var(--strand-space-4) 0;
|
|
3227
|
+
background: var(--strand-glass-bg);
|
|
3228
|
+
-webkit-backdrop-filter: blur(var(--strand-glass-blur));
|
|
3229
|
+
backdrop-filter: blur(var(--strand-glass-blur));
|
|
3230
|
+
border-bottom: 1px solid var(--strand-glass-border);
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
/* ── Scrolled state (DL Part XIX.1) ──
|
|
3234
|
+
Apply via JS scroll listener. Adds shadow + blue-shifted border. */
|
|
3235
|
+
.strand-nav--scrolled {
|
|
3236
|
+
box-shadow: var(--strand-elevation-1);
|
|
3237
|
+
border-bottom-color: var(--strand-nav-scrolled-border);
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
.strand-nav--glass {
|
|
3241
|
+
transition: box-shadow var(--strand-duration-normal) ease, border-color
|
|
3242
|
+
var(--strand-duration-normal) ease;
|
|
1398
3243
|
}
|
|
1399
3244
|
|
|
1400
3245
|
/* ── Reduced motion ── */
|
|
1401
3246
|
@media (prefers-reduced-motion: reduce) {
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
3247
|
+
.strand-nav__link,
|
|
3248
|
+
.strand-nav__mobile-link,
|
|
3249
|
+
.strand-nav__hamburger,
|
|
3250
|
+
.strand-nav--glass {
|
|
3251
|
+
transition: none;
|
|
3252
|
+
}
|
|
1407
3253
|
}
|
|
1408
3254
|
|
|
1409
3255
|
|
|
@@ -1444,7 +3290,7 @@
|
|
|
1444
3290
|
/* ── Bar indeterminate ── */
|
|
1445
3291
|
.strand-progress--bar.strand-progress--indeterminate .strand-progress__fill {
|
|
1446
3292
|
width: 40%;
|
|
1447
|
-
animation: strand-progress-shimmer
|
|
3293
|
+
animation: strand-progress-shimmer var(--strand-duration-cinematic) var(--strand-ease-in-out-sine) infinite;
|
|
1448
3294
|
}
|
|
1449
3295
|
|
|
1450
3296
|
@keyframes strand-progress-shimmer {
|
|
@@ -1674,39 +3520,67 @@
|
|
|
1674
3520
|
|
|
1675
3521
|
/* ── Base ── */
|
|
1676
3522
|
.strand-section {
|
|
1677
|
-
|
|
3523
|
+
width: 100%;
|
|
1678
3524
|
}
|
|
1679
3525
|
|
|
1680
3526
|
/* ── Variants ── */
|
|
1681
3527
|
.strand-section--standard {
|
|
1682
|
-
|
|
3528
|
+
padding-block: clamp(4rem, 8vw, 8rem);
|
|
1683
3529
|
}
|
|
1684
3530
|
|
|
1685
3531
|
.strand-section--hero {
|
|
1686
|
-
|
|
3532
|
+
padding-block: clamp(6rem, 12vw, 12rem);
|
|
3533
|
+
text-align: center;
|
|
3534
|
+
position: relative;
|
|
3535
|
+
overflow: hidden;
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
/* ── Compact hero ──
|
|
3539
|
+
A tighter hero variant for pages where a secondary CTA (install tabs,
|
|
3540
|
+
pricing, signup) sits directly below the hero and must be at least
|
|
3541
|
+
partially visible in the default viewport on desktop. Keeps the hero
|
|
3542
|
+
treatment (center text, positioning, overflow handling) but trims the
|
|
3543
|
+
vertical padding so the fold sits later in the page. */
|
|
3544
|
+
.strand-section--hero-compact {
|
|
3545
|
+
padding-block: clamp(3.5rem, 6vw, 6rem);
|
|
3546
|
+
text-align: center;
|
|
3547
|
+
position: relative;
|
|
3548
|
+
overflow: hidden;
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
.strand-section--hero-xl {
|
|
3552
|
+
padding-block: clamp(8rem, 15vw, 14rem);
|
|
3553
|
+
text-align: center;
|
|
3554
|
+
position: relative;
|
|
3555
|
+
overflow: hidden;
|
|
1687
3556
|
}
|
|
1688
3557
|
|
|
1689
3558
|
/* ── Backgrounds ── */
|
|
1690
3559
|
.strand-section--bg-primary {
|
|
1691
|
-
|
|
3560
|
+
background-color: var(--strand-surface-primary);
|
|
1692
3561
|
}
|
|
1693
3562
|
|
|
1694
3563
|
.strand-section--bg-elevated {
|
|
1695
|
-
|
|
3564
|
+
background-color: var(--strand-surface-elevated);
|
|
1696
3565
|
}
|
|
1697
3566
|
|
|
1698
3567
|
.strand-section--bg-recessed {
|
|
1699
|
-
|
|
3568
|
+
background-color: var(--strand-surface-recessed);
|
|
1700
3569
|
}
|
|
1701
3570
|
|
|
1702
3571
|
/* ── Compact variant ── */
|
|
1703
3572
|
.strand-section--compact {
|
|
1704
|
-
|
|
3573
|
+
padding-block: var(--strand-space-12);
|
|
1705
3574
|
}
|
|
1706
3575
|
|
|
1707
3576
|
/* ── Border variant ── */
|
|
1708
3577
|
.strand-section--border-top {
|
|
1709
|
-
|
|
3578
|
+
border-top: 1px solid var(--strand-gray-200);
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
/* ── Scroll target (anchored section with nav offset) ── */
|
|
3582
|
+
.strand-section--scroll-target {
|
|
3583
|
+
scroll-margin-top: 5rem;
|
|
1710
3584
|
}
|
|
1711
3585
|
|
|
1712
3586
|
|
|
@@ -1748,7 +3622,24 @@
|
|
|
1748
3622
|
cursor: pointer;
|
|
1749
3623
|
}
|
|
1750
3624
|
|
|
1751
|
-
/* ── Arrow indicator ──
|
|
3625
|
+
/* ── Arrow indicator (auto via ::after, no markup needed) ──
|
|
3626
|
+
Renders a chevron-down on the right edge of every strand-select
|
|
3627
|
+
wrapper, sized to align with the field text. Consumers can also
|
|
3628
|
+
place an explicit <span class="strand-select__arrow"> for control. */
|
|
3629
|
+
.strand-select::after {
|
|
3630
|
+
content: "";
|
|
3631
|
+
position: absolute;
|
|
3632
|
+
right: var(--strand-space-3);
|
|
3633
|
+
top: 50%;
|
|
3634
|
+
width: 0;
|
|
3635
|
+
height: 0;
|
|
3636
|
+
margin-top: -2px;
|
|
3637
|
+
border-left: 5px solid transparent;
|
|
3638
|
+
border-right: 5px solid transparent;
|
|
3639
|
+
border-top: 5px solid var(--strand-gray-500);
|
|
3640
|
+
pointer-events: none;
|
|
3641
|
+
}
|
|
3642
|
+
|
|
1752
3643
|
.strand-select__arrow {
|
|
1753
3644
|
position: absolute;
|
|
1754
3645
|
right: var(--strand-space-3);
|
|
@@ -1862,25 +3753,50 @@
|
|
|
1862
3753
|
|
|
1863
3754
|
/* ── Field ── */
|
|
1864
3755
|
.strand-slider__field {
|
|
3756
|
+
/* The field itself is a 44px-tall transparent hit area so the
|
|
3757
|
+
full WCAG 2.5.5 touch target is draggable. The visible track
|
|
3758
|
+
is painted via the ::before-equivalent (we cannot use
|
|
3759
|
+
pseudo-elements on form controls, so the track tint is set
|
|
3760
|
+
directly on the field with a small height plus a vertical
|
|
3761
|
+
gradient that fades the bottom). */
|
|
1865
3762
|
width: 100%;
|
|
1866
|
-
height:
|
|
3763
|
+
min-height: var(--strand-touch-target);
|
|
3764
|
+
height: var(--strand-touch-target);
|
|
1867
3765
|
appearance: none;
|
|
1868
|
-
background:
|
|
1869
|
-
border-radius: var(--strand-radius-full);
|
|
3766
|
+
background: transparent;
|
|
1870
3767
|
outline: none;
|
|
1871
3768
|
cursor: pointer;
|
|
1872
|
-
transition: background var(--strand-duration-fast) var(--strand-ease-out-quart);
|
|
1873
3769
|
}
|
|
1874
3770
|
|
|
1875
|
-
/* ──
|
|
3771
|
+
/* ── Track: Webkit ── */
|
|
3772
|
+
.strand-slider__field::-webkit-slider-runnable-track {
|
|
3773
|
+
height: 6px;
|
|
3774
|
+
background: var(--strand-gray-200);
|
|
3775
|
+
border-radius: var(--strand-radius-full);
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
/* ── Thumb: Webkit ──
|
|
3779
|
+
The earlier approach of width: 20px + border: 12px solid transparent +
|
|
3780
|
+
background-clip: padding-box collapses to a hollow ring in webkit
|
|
3781
|
+
because the slider thumb pseudo-element does not honor the
|
|
3782
|
+
padding-box clip the way regular elements do. The painted box
|
|
3783
|
+
shows only the (currently empty) padding region while the
|
|
3784
|
+
transparent border consumes the visible area. The fix is to use
|
|
3785
|
+
a flat 20x20 thumb with NO transparent border. The 44px hit area
|
|
3786
|
+
is provided by the field height above, not by an oversized
|
|
3787
|
+
thumb. */
|
|
1876
3788
|
.strand-slider__field::-webkit-slider-thumb {
|
|
1877
3789
|
appearance: none;
|
|
1878
3790
|
width: 20px;
|
|
1879
3791
|
height: 20px;
|
|
3792
|
+
/* Vertically center the thumb on the 6px track inside the
|
|
3793
|
+
44px field: (44 - 20) / 2 = 12 from the top, but the track is
|
|
3794
|
+
also vertically centered, so the thumb sits on the track when
|
|
3795
|
+
margin-top equals (track-height - thumb-height) / 2 = -7px. */
|
|
3796
|
+
margin-top: -7px;
|
|
1880
3797
|
background: var(--strand-blue-primary);
|
|
1881
|
-
border:
|
|
3798
|
+
border: none;
|
|
1882
3799
|
border-radius: var(--strand-radius-full);
|
|
1883
|
-
background-clip: padding-box;
|
|
1884
3800
|
cursor: pointer;
|
|
1885
3801
|
box-shadow: var(--strand-elevation-1);
|
|
1886
3802
|
transition:
|
|
@@ -1890,24 +3806,23 @@
|
|
|
1890
3806
|
|
|
1891
3807
|
.strand-slider__field:hover:not(:disabled)::-webkit-slider-thumb {
|
|
1892
3808
|
background: var(--strand-blue-vivid);
|
|
1893
|
-
background-clip: padding-box;
|
|
1894
3809
|
transform: scale(1.15);
|
|
1895
3810
|
}
|
|
1896
3811
|
|
|
1897
3812
|
.strand-slider__field:active:not(:disabled)::-webkit-slider-thumb {
|
|
1898
3813
|
background: var(--strand-blue-deep);
|
|
1899
|
-
background-clip: padding-box;
|
|
1900
3814
|
transform: scale(1.05);
|
|
1901
3815
|
}
|
|
1902
3816
|
|
|
1903
|
-
/* ── Thumb: Firefox
|
|
3817
|
+
/* ── Thumb: Firefox ──
|
|
3818
|
+
Firefox does not need the manual margin offset because
|
|
3819
|
+
::-moz-range-thumb is centered on the track automatically. */
|
|
1904
3820
|
.strand-slider__field::-moz-range-thumb {
|
|
1905
3821
|
width: 20px;
|
|
1906
3822
|
height: 20px;
|
|
1907
3823
|
background: var(--strand-blue-primary);
|
|
1908
|
-
border:
|
|
3824
|
+
border: none;
|
|
1909
3825
|
border-radius: var(--strand-radius-full);
|
|
1910
|
-
background-clip: padding-box;
|
|
1911
3826
|
cursor: pointer;
|
|
1912
3827
|
box-shadow: var(--strand-elevation-1);
|
|
1913
3828
|
transition:
|
|
@@ -1917,13 +3832,11 @@
|
|
|
1917
3832
|
|
|
1918
3833
|
.strand-slider__field:hover:not(:disabled)::-moz-range-thumb {
|
|
1919
3834
|
background: var(--strand-blue-vivid);
|
|
1920
|
-
background-clip: padding-box;
|
|
1921
3835
|
transform: scale(1.15);
|
|
1922
3836
|
}
|
|
1923
3837
|
|
|
1924
3838
|
.strand-slider__field:active:not(:disabled)::-moz-range-thumb {
|
|
1925
3839
|
background: var(--strand-blue-deep);
|
|
1926
|
-
background-clip: padding-box;
|
|
1927
3840
|
transform: scale(1.05);
|
|
1928
3841
|
}
|
|
1929
3842
|
|
|
@@ -2097,6 +4010,24 @@
|
|
|
2097
4010
|
flex-wrap: wrap;
|
|
2098
4011
|
}
|
|
2099
4012
|
|
|
4013
|
+
/* ── Responsive direction (DL Part XV: small-screen reflow) ──
|
|
4014
|
+
Combine with strand-stack--horizontal: at <=768px the stack
|
|
4015
|
+
collapses to a vertical column, alignment recenters, and
|
|
4016
|
+
children are allowed to grow to a comfortable max-width via
|
|
4017
|
+
strand-stack__item--responsive (sets max-width 280px width 100%). */
|
|
4018
|
+
@media (max-width: 768px) {
|
|
4019
|
+
.strand-stack--horizontal.strand-stack--responsive {
|
|
4020
|
+
flex-direction: column;
|
|
4021
|
+
align-items: center;
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
.strand-stack--responsive > .strand-stack__item--full-mobile,
|
|
4025
|
+
.strand-stack--responsive > .strand-btn {
|
|
4026
|
+
width: 100%;
|
|
4027
|
+
max-width: 280px;
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
|
|
2100
4031
|
/* ── Gap utilities ── */
|
|
2101
4032
|
.strand-stack--gap-1 { gap: var(--strand-space-1); }
|
|
2102
4033
|
.strand-stack--gap-2 { gap: var(--strand-space-2); }
|
|
@@ -2338,16 +4269,63 @@
|
|
|
2338
4269
|
border-bottom-color: var(--strand-blue-primary);
|
|
2339
4270
|
}
|
|
2340
4271
|
|
|
4272
|
+
/* ── Instrument variant (mono uppercase, control-panel feel) ──
|
|
4273
|
+
Use for state switches like "For Talent / For Employers" where
|
|
4274
|
+
the tab acts like a toggle on a control panel rather than a
|
|
4275
|
+
content navigation. Pairs with strand-tabs__panel--reveal. */
|
|
4276
|
+
.strand-tabs--instrument [role="tablist"] {
|
|
4277
|
+
justify-content: center;
|
|
4278
|
+
gap: 0;
|
|
4279
|
+
border-bottom: 1px solid var(--strand-surface-subtle);
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
.strand-tabs--instrument .strand-tabs__tab {
|
|
4283
|
+
font-family: var(--strand-font-mono);
|
|
4284
|
+
font-size: var(--strand-text-xs);
|
|
4285
|
+
font-weight: var(--strand-weight-medium);
|
|
4286
|
+
letter-spacing: var(--strand-tracking-widest);
|
|
4287
|
+
text-transform: uppercase;
|
|
4288
|
+
color: var(--strand-gray-600);
|
|
4289
|
+
padding: var(--strand-space-3) var(--strand-space-6);
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
.strand-tabs--instrument .strand-tabs__tab--active {
|
|
4293
|
+
color: var(--strand-blue-midnight);
|
|
4294
|
+
border-bottom-color: var(--strand-blue-primary);
|
|
4295
|
+
}
|
|
4296
|
+
|
|
2341
4297
|
/* ── Panel ── */
|
|
2342
4298
|
.strand-tabs [role="tabpanel"] {
|
|
2343
4299
|
padding: var(--strand-space-4) 0;
|
|
2344
4300
|
}
|
|
2345
4301
|
|
|
4302
|
+
/* ── Panel reveal animation (DL Part VI motion: subtle entry) ──
|
|
4303
|
+
Apply to the active tab panel when switching tabs. Fades in
|
|
4304
|
+
with a small upward slide. JS toggles strand-tabs__panel--reveal
|
|
4305
|
+
on the panel that becomes active. */
|
|
4306
|
+
@keyframes strand-tabs-reveal {
|
|
4307
|
+
from {
|
|
4308
|
+
opacity: 0;
|
|
4309
|
+
transform: translateY(8px);
|
|
4310
|
+
}
|
|
4311
|
+
to {
|
|
4312
|
+
opacity: 1;
|
|
4313
|
+
transform: translateY(0);
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
.strand-tabs__panel--reveal {
|
|
4318
|
+
animation: strand-tabs-reveal var(--strand-duration-normal) var(--strand-ease-out-expo) both;
|
|
4319
|
+
}
|
|
4320
|
+
|
|
2346
4321
|
/* ── Reduced motion ── */
|
|
2347
4322
|
@media (prefers-reduced-motion: reduce) {
|
|
2348
4323
|
.strand-tabs__tab {
|
|
2349
4324
|
transition: none;
|
|
2350
4325
|
}
|
|
4326
|
+
.strand-tabs__panel--reveal {
|
|
4327
|
+
animation: none;
|
|
4328
|
+
}
|
|
2351
4329
|
}
|
|
2352
4330
|
|
|
2353
4331
|
|
|
@@ -2446,11 +4424,62 @@
|
|
|
2446
4424
|
opacity: 1;
|
|
2447
4425
|
}
|
|
2448
4426
|
|
|
4427
|
+
/* ── Joined chip (interactive pill) ──
|
|
4428
|
+
A compact pill the user taps to toggle membership off. Used
|
|
4429
|
+
for "joined channel" style affordances where the in-content
|
|
4430
|
+
pill carries the active signal and the parent card chrome
|
|
4431
|
+
stays quiet. Designed to live inside a card header row. */
|
|
4432
|
+
.strand-chip--joined {
|
|
4433
|
+
display: inline-flex;
|
|
4434
|
+
align-items: center;
|
|
4435
|
+
gap: var(--strand-space-1);
|
|
4436
|
+
flex-shrink: 0;
|
|
4437
|
+
padding: 2px var(--strand-space-2);
|
|
4438
|
+
background: var(--strand-blue-glow);
|
|
4439
|
+
color: var(--strand-blue-deep);
|
|
4440
|
+
border: 1px solid var(--strand-blue-indicator);
|
|
4441
|
+
border-radius: var(--strand-radius-full);
|
|
4442
|
+
font-family: var(--strand-font-sans);
|
|
4443
|
+
font-size: var(--strand-text-xs);
|
|
4444
|
+
font-weight: var(--strand-weight-semibold);
|
|
4445
|
+
line-height: 1;
|
|
4446
|
+
letter-spacing: var(--strand-tracking-tight);
|
|
4447
|
+
cursor: pointer;
|
|
4448
|
+
transition:
|
|
4449
|
+
background var(--strand-duration-fast) var(--strand-ease-out-quart),
|
|
4450
|
+
border-color var(--strand-duration-fast) var(--strand-ease-out-quart),
|
|
4451
|
+
color var(--strand-duration-fast) var(--strand-ease-out-quart);
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
.strand-chip--joined:hover:not(:disabled) {
|
|
4455
|
+
background: var(--strand-blue-tint);
|
|
4456
|
+
border-color: var(--strand-blue-deep);
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
.strand-chip--joined:focus-visible {
|
|
4460
|
+
outline: 2px solid var(--strand-blue-primary);
|
|
4461
|
+
outline-offset: 2px;
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
.strand-chip--joined:disabled {
|
|
4465
|
+
opacity: 0.5;
|
|
4466
|
+
cursor: not-allowed;
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4469
|
+
.strand-chip--joined__check {
|
|
4470
|
+
font-size: 0.875em;
|
|
4471
|
+
line-height: 1;
|
|
4472
|
+
}
|
|
4473
|
+
|
|
2449
4474
|
/* ── Reduced motion ── */
|
|
2450
4475
|
@media (prefers-reduced-motion: reduce) {
|
|
2451
4476
|
.strand-tag__remove {
|
|
2452
4477
|
transition: none;
|
|
2453
4478
|
}
|
|
4479
|
+
|
|
4480
|
+
.strand-chip--joined {
|
|
4481
|
+
transition: none;
|
|
4482
|
+
}
|
|
2454
4483
|
}
|
|
2455
4484
|
|
|
2456
4485
|
|
|
@@ -2740,37 +4769,149 @@
|
|
|
2740
4769
|
position: static;
|
|
2741
4770
|
}
|
|
2742
4771
|
|
|
2743
|
-
.strand-static .strand-tooltip {
|
|
2744
|
-
position: static;
|
|
4772
|
+
.strand-static .strand-tooltip {
|
|
4773
|
+
position: static;
|
|
4774
|
+
}
|
|
4775
|
+
|
|
4776
|
+
.strand-static *,
|
|
4777
|
+
.strand-static *::before,
|
|
4778
|
+
.strand-static *::after {
|
|
4779
|
+
transition: none !important;
|
|
4780
|
+
animation: none !important;
|
|
4781
|
+
}
|
|
4782
|
+
|
|
4783
|
+
/* ── Recessed viewport ──
|
|
4784
|
+
The instrument viewport sits below the card surface.
|
|
4785
|
+
Use for component previews, showcases, and documentation. */
|
|
4786
|
+
|
|
4787
|
+
.strand-viewport {
|
|
4788
|
+
background: var(--strand-surface-recessed);
|
|
4789
|
+
box-shadow: var(--strand-shadow-inset);
|
|
4790
|
+
border-radius: var(--strand-radius-lg);
|
|
4791
|
+
padding: var(--strand-space-6);
|
|
4792
|
+
}
|
|
4793
|
+
|
|
4794
|
+
/* ── Frosted viewport (DL Part 9.4) ──
|
|
4795
|
+
Light data-dense context with frosted-glass character. The
|
|
4796
|
+
"frosted instrument panel embedded in the lab bench" feel,
|
|
4797
|
+
without going dark. Backdrop-filter requires content behind it. */
|
|
4798
|
+
.strand-viewport--frosted {
|
|
4799
|
+
background: var(--strand-glass-bg);
|
|
4800
|
+
-webkit-backdrop-filter: blur(var(--strand-glass-blur));
|
|
4801
|
+
backdrop-filter: blur(var(--strand-glass-blur));
|
|
4802
|
+
border: 1px solid var(--strand-glass-border);
|
|
4803
|
+
border-radius: var(--strand-radius-lg);
|
|
4804
|
+
box-shadow: var(--strand-shadow-inset);
|
|
4805
|
+
padding: var(--strand-space-6);
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
/* ── Glass surface utility (DL Part 9.5) ──
|
|
4809
|
+
Frosted-glass treatment for any element that wants the
|
|
4810
|
+
"glass walls with controlled daylight" character. Apply to
|
|
4811
|
+
nav, sticky toolbars, modal headers, surfaces above content.
|
|
4812
|
+
backdrop-filter only renders meaningfully when there is
|
|
4813
|
+
something behind the element. */
|
|
4814
|
+
.strand-glass-surface {
|
|
4815
|
+
background: var(--strand-glass-bg);
|
|
4816
|
+
-webkit-backdrop-filter: blur(var(--strand-glass-blur));
|
|
4817
|
+
backdrop-filter: blur(var(--strand-glass-blur));
|
|
4818
|
+
border: 1px solid var(--strand-glass-border);
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
/* ── Overline (specimen label pattern, DL Part IV.5) ── */
|
|
4822
|
+
.strand-overline {
|
|
4823
|
+
font-family: var(--strand-font-mono);
|
|
4824
|
+
font-size: var(--strand-text-xs);
|
|
4825
|
+
font-weight: var(--strand-weight-medium);
|
|
4826
|
+
letter-spacing: var(--strand-tracking-ultra);
|
|
4827
|
+
text-transform: uppercase;
|
|
4828
|
+
color: var(--strand-gray-500);
|
|
4829
|
+
line-height: var(--strand-leading-normal);
|
|
4830
|
+
}
|
|
4831
|
+
|
|
4832
|
+
/* ── Overline accent variant (blue instead of gray) ──
|
|
4833
|
+
Uses blue-deep so the small uppercase text meets WCAG AA 4.5:1
|
|
4834
|
+
contrast on the default white surface. blue-primary (#3b82f6)
|
|
4835
|
+
only scores 3.28:1 at 10.4px mono. blue-deep stays vibrant
|
|
4836
|
+
without violating contrast. */
|
|
4837
|
+
.strand-overline--accent {
|
|
4838
|
+
color: var(--strand-blue-deep);
|
|
4839
|
+
}
|
|
4840
|
+
|
|
4841
|
+
/* ── Overline with pulse dot (DL Part 7: alive signal) ──
|
|
4842
|
+
Adds an animated dot prefix that pulses in a slow rhythm. Use on
|
|
4843
|
+
the primary hero overline to signal "this is a living system."
|
|
4844
|
+
Combine with strand-overline--accent for the blue brand color.
|
|
4845
|
+
The dot uses blue-primary; on dark backgrounds use teal-vital.
|
|
4846
|
+
|
|
4847
|
+
The display: inline-block + relative positioning shrinks the
|
|
4848
|
+
overline to its text width so the absolutely-positioned dot
|
|
4849
|
+
sits immediately to the LEFT of the text -- not to the left of
|
|
4850
|
+
a full-width parent container. */
|
|
4851
|
+
.strand-overline--pulse {
|
|
4852
|
+
position: relative;
|
|
4853
|
+
padding-left: var(--strand-space-4);
|
|
4854
|
+
display: inline-block !important;
|
|
4855
|
+
}
|
|
4856
|
+
|
|
4857
|
+
.strand-overline--pulse::before {
|
|
4858
|
+
content: "";
|
|
4859
|
+
position: absolute;
|
|
4860
|
+
left: 0;
|
|
4861
|
+
top: 50%;
|
|
4862
|
+
width: 6px;
|
|
4863
|
+
height: 6px;
|
|
4864
|
+
margin-top: -3px;
|
|
4865
|
+
border-radius: var(--strand-radius-full);
|
|
4866
|
+
background: var(--strand-blue-primary);
|
|
4867
|
+
animation: strand-overline-pulse 2s var(--strand-ease-in-out-sine) infinite;
|
|
4868
|
+
}
|
|
4869
|
+
|
|
4870
|
+
@keyframes strand-overline-pulse {
|
|
4871
|
+
0%, 100% { opacity: 1; }
|
|
4872
|
+
50% { opacity: 0.4; }
|
|
4873
|
+
}
|
|
4874
|
+
|
|
4875
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4876
|
+
.strand-overline--pulse::before {
|
|
4877
|
+
animation: none;
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
|
|
4881
|
+
/* DL Part XVII.2: Contrast-safe pairing on recessed surfaces.
|
|
4882
|
+
gray-500 on surface-recessed fails 4.5:1 for small text.
|
|
4883
|
+
Auto-darken to gray-600 when inside a recessed context. */
|
|
4884
|
+
.strand-section--bg-recessed .strand-overline {
|
|
4885
|
+
color: var(--strand-gray-600);
|
|
4886
|
+
}
|
|
4887
|
+
|
|
4888
|
+
.strand-section--bg-recessed .strand-overline--accent {
|
|
4889
|
+
color: var(--strand-blue-deep);
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4892
|
+
.strand-section--bg-recessed .strand-lead {
|
|
4893
|
+
color: var(--strand-gray-600);
|
|
4894
|
+
}
|
|
4895
|
+
|
|
4896
|
+
.strand-section--bg-recessed .strand-text-secondary {
|
|
4897
|
+
color: var(--strand-gray-600);
|
|
2745
4898
|
}
|
|
2746
4899
|
|
|
2747
|
-
.strand-
|
|
2748
|
-
|
|
2749
|
-
.strand-static *::after {
|
|
2750
|
-
transition: none !important;
|
|
2751
|
-
animation: none !important;
|
|
4900
|
+
.strand-section--bg-recessed .strand-step-indicator {
|
|
4901
|
+
color: var(--strand-blue-deep);
|
|
2752
4902
|
}
|
|
2753
4903
|
|
|
2754
|
-
/*
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
.strand-
|
|
2759
|
-
|
|
2760
|
-
box-shadow: var(--strand-shadow-inset);
|
|
2761
|
-
border-radius: var(--strand-radius-lg);
|
|
2762
|
-
padding: var(--strand-space-6);
|
|
4904
|
+
/* All small text on recessed surfaces needs darker colors for WCAG 4.5:1 */
|
|
4905
|
+
.strand-section--bg-recessed .strand-tabs__tab,
|
|
4906
|
+
.strand-section--bg-recessed .strand-form-field__label,
|
|
4907
|
+
.strand-section--bg-recessed .strand-form-field__hint,
|
|
4908
|
+
.strand-section--bg-recessed .strand-badge--default {
|
|
4909
|
+
color: var(--strand-gray-600);
|
|
2763
4910
|
}
|
|
2764
4911
|
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
font-size: var(--strand-text-xs);
|
|
2769
|
-
font-weight: var(--strand-weight-medium);
|
|
2770
|
-
letter-spacing: var(--strand-tracking-ultra);
|
|
2771
|
-
text-transform: uppercase;
|
|
2772
|
-
color: var(--strand-gray-500);
|
|
2773
|
-
line-height: var(--strand-leading-normal);
|
|
4912
|
+
.strand-section--bg-recessed .strand-tabs__tab--active {
|
|
4913
|
+
color: var(--strand-blue-deep);
|
|
4914
|
+
border-bottom-color: var(--strand-blue-deep);
|
|
2774
4915
|
}
|
|
2775
4916
|
|
|
2776
4917
|
/* ── Headline (display heading, DL Part IV.5) ── */
|
|
@@ -2793,6 +4934,14 @@
|
|
|
2793
4934
|
letter-spacing: var(--strand-tracking-tighter);
|
|
2794
4935
|
}
|
|
2795
4936
|
|
|
4937
|
+
/* ── Gradient text effect (premium hero headlines) ── */
|
|
4938
|
+
.strand-headline--gradient {
|
|
4939
|
+
background: linear-gradient(180deg, var(--strand-gray-900) 0%, var(--strand-blue-midnight) 100%);
|
|
4940
|
+
-webkit-background-clip: text;
|
|
4941
|
+
background-clip: text;
|
|
4942
|
+
-webkit-text-fill-color: transparent;
|
|
4943
|
+
}
|
|
4944
|
+
|
|
2796
4945
|
/* ── Title (human voice display, DL Part IV.7) ── */
|
|
2797
4946
|
.strand-title {
|
|
2798
4947
|
font-family: var(--strand-font-sans);
|
|
@@ -2818,6 +4967,19 @@
|
|
|
2818
4967
|
line-height: var(--strand-leading-relaxed);
|
|
2819
4968
|
}
|
|
2820
4969
|
|
|
4970
|
+
.strand-text-secondary strong {
|
|
4971
|
+
color: var(--strand-gray-700);
|
|
4972
|
+
}
|
|
4973
|
+
|
|
4974
|
+
.strand-text-secondary a {
|
|
4975
|
+
color: var(--strand-blue-primary);
|
|
4976
|
+
text-decoration: none;
|
|
4977
|
+
}
|
|
4978
|
+
|
|
4979
|
+
.strand-text-secondary a:hover {
|
|
4980
|
+
color: var(--strand-blue-vivid);
|
|
4981
|
+
}
|
|
4982
|
+
|
|
2821
4983
|
.strand-text-secondary--xs {
|
|
2822
4984
|
font-size: var(--strand-text-xs);
|
|
2823
4985
|
}
|
|
@@ -3035,6 +5197,59 @@
|
|
|
3035
5197
|
margin: 0;
|
|
3036
5198
|
}
|
|
3037
5199
|
|
|
5200
|
+
/* ═══ HEADLINE MONO VARIANT (DL Part IV.5) ═══
|
|
5201
|
+
Mono-family headline with medium weight, tight tracking,
|
|
5202
|
+
and sentence case (NOT uppercase). Use for product/lab
|
|
5203
|
+
titles where the brand identity is typed in lowercase or
|
|
5204
|
+
mixed case ("trust" vs "TRUST"). Composes with
|
|
5205
|
+
strand-headline--sm/md/lg for size. */
|
|
5206
|
+
.strand-headline--mono {
|
|
5207
|
+
font-family: var(--strand-font-mono);
|
|
5208
|
+
font-weight: var(--strand-weight-medium);
|
|
5209
|
+
text-transform: none;
|
|
5210
|
+
letter-spacing: var(--strand-tracking-tight);
|
|
5211
|
+
}
|
|
5212
|
+
|
|
5213
|
+
.strand-headline--md {
|
|
5214
|
+
font-size: clamp(1.25rem, 2vw + 0.5rem, 1.75rem);
|
|
5215
|
+
letter-spacing: var(--strand-tracking-tight);
|
|
5216
|
+
}
|
|
5217
|
+
|
|
5218
|
+
/* ═══ STATUS CHIP (DL Part XVII: small classification label) ═══
|
|
5219
|
+
Inline pill for lab/product status. Mono uppercase, small tint.
|
|
5220
|
+
Pair with strand-card or header rows. */
|
|
5221
|
+
.strand-status-chip {
|
|
5222
|
+
display: inline-block;
|
|
5223
|
+
font-family: var(--strand-font-mono);
|
|
5224
|
+
font-size: var(--strand-text-xs);
|
|
5225
|
+
font-weight: var(--strand-weight-medium);
|
|
5226
|
+
letter-spacing: var(--strand-tracking-wide);
|
|
5227
|
+
text-transform: uppercase;
|
|
5228
|
+
padding: var(--strand-space-1) var(--strand-space-2);
|
|
5229
|
+
border-radius: var(--strand-radius-sm);
|
|
5230
|
+
line-height: var(--strand-leading-normal);
|
|
5231
|
+
}
|
|
5232
|
+
|
|
5233
|
+
.strand-status-chip--live {
|
|
5234
|
+
color: var(--strand-on-teal-tint);
|
|
5235
|
+
background: var(--strand-teal-tint);
|
|
5236
|
+
}
|
|
5237
|
+
|
|
5238
|
+
.strand-status-chip--neutral {
|
|
5239
|
+
color: var(--strand-gray-600);
|
|
5240
|
+
background: var(--strand-gray-tint);
|
|
5241
|
+
}
|
|
5242
|
+
|
|
5243
|
+
.strand-status-chip--accent {
|
|
5244
|
+
color: var(--strand-blue-deep);
|
|
5245
|
+
background: var(--strand-blue-tint);
|
|
5246
|
+
}
|
|
5247
|
+
|
|
5248
|
+
.strand-status-chip--caution {
|
|
5249
|
+
color: var(--strand-on-amber-tint);
|
|
5250
|
+
background: var(--strand-amber-tint);
|
|
5251
|
+
}
|
|
5252
|
+
|
|
3038
5253
|
/* ── Viewport flex modifiers (component showcase layout) ── */
|
|
3039
5254
|
.strand-viewport--flex {
|
|
3040
5255
|
display: flex;
|
|
@@ -3050,3 +5265,362 @@
|
|
|
3050
5265
|
gap: var(--strand-space-4);
|
|
3051
5266
|
}
|
|
3052
5267
|
|
|
5268
|
+
/* ═══ CONNECTED STEPS (DL Part XI-B: connected-sequence production) ═══
|
|
5269
|
+
Sequential process steps with a visual connector line between cards.
|
|
5270
|
+
Use with a grid of strand-card elements inside. */
|
|
5271
|
+
.strand-steps-connected {
|
|
5272
|
+
position: relative;
|
|
5273
|
+
}
|
|
5274
|
+
|
|
5275
|
+
.strand-steps-connected::before {
|
|
5276
|
+
content: "";
|
|
5277
|
+
position: absolute;
|
|
5278
|
+
top: 50%;
|
|
5279
|
+
left: var(--strand-space-8);
|
|
5280
|
+
right: var(--strand-space-8);
|
|
5281
|
+
height: 1px;
|
|
5282
|
+
background: linear-gradient(90deg, transparent 0%, var(--strand-blue-indicator) 20%, var(--strand-blue-indicator) 80%, transparent 100%);
|
|
5283
|
+
z-index: 0;
|
|
5284
|
+
pointer-events: none;
|
|
5285
|
+
}
|
|
5286
|
+
|
|
5287
|
+
.strand-steps-connected > * {
|
|
5288
|
+
position: relative;
|
|
5289
|
+
z-index: 1;
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5292
|
+
@media (max-width: 767px) {
|
|
5293
|
+
.strand-steps-connected::before {
|
|
5294
|
+
display: none;
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
|
|
5298
|
+
/* ═══ FOOTER (DL Part XIX.5) ═══
|
|
5299
|
+
Compact closing panel. Border-top, centered, quiet. */
|
|
5300
|
+
.strand-footer {
|
|
5301
|
+
padding: var(--strand-space-12) 0;
|
|
5302
|
+
border-top: 1px solid var(--strand-gray-200);
|
|
5303
|
+
text-align: center;
|
|
5304
|
+
}
|
|
5305
|
+
|
|
5306
|
+
.strand-footer__nav {
|
|
5307
|
+
display: flex;
|
|
5308
|
+
justify-content: center;
|
|
5309
|
+
gap: var(--strand-space-6);
|
|
5310
|
+
flex-wrap: wrap;
|
|
5311
|
+
margin-bottom: var(--strand-space-6);
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5314
|
+
.strand-footer__link {
|
|
5315
|
+
font-family: var(--strand-font-mono);
|
|
5316
|
+
font-size: var(--strand-text-xs);
|
|
5317
|
+
letter-spacing: var(--strand-tracking-wider);
|
|
5318
|
+
color: var(--strand-gray-400);
|
|
5319
|
+
text-decoration: none;
|
|
5320
|
+
transition: color var(--strand-duration-fast) ease;
|
|
5321
|
+
}
|
|
5322
|
+
|
|
5323
|
+
.strand-footer__link:hover {
|
|
5324
|
+
color: var(--strand-blue-primary);
|
|
5325
|
+
}
|
|
5326
|
+
|
|
5327
|
+
.strand-footer__copy {
|
|
5328
|
+
font-size: var(--strand-text-xs);
|
|
5329
|
+
color: var(--strand-gray-400);
|
|
5330
|
+
}
|
|
5331
|
+
|
|
5332
|
+
/* ═══ FORM LAYOUT (DL Part XVIII.1: Specimen Collection Room) ═══ */
|
|
5333
|
+
.strand-form-grid {
|
|
5334
|
+
display: grid;
|
|
5335
|
+
gap: var(--strand-space-6);
|
|
5336
|
+
}
|
|
5337
|
+
|
|
5338
|
+
.strand-form-row {
|
|
5339
|
+
display: grid;
|
|
5340
|
+
grid-template-columns: 1fr 1fr;
|
|
5341
|
+
gap: var(--strand-space-6);
|
|
5342
|
+
}
|
|
5343
|
+
|
|
5344
|
+
@media (max-width: 639px) {
|
|
5345
|
+
.strand-form-row {
|
|
5346
|
+
grid-template-columns: 1fr;
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
|
|
5350
|
+
/* ═══ HONEYPOT (DL Part XI.1: form security) ═══
|
|
5351
|
+
Hidden field that bots fill but humans never see.
|
|
5352
|
+
NOT display:none (some bots skip those).
|
|
5353
|
+
Positioned offscreen with aria-hidden and tabindex=-1. */
|
|
5354
|
+
.strand-honeypot {
|
|
5355
|
+
position: absolute;
|
|
5356
|
+
left: -9999px;
|
|
5357
|
+
top: -9999px;
|
|
5358
|
+
width: 1px;
|
|
5359
|
+
height: 1px;
|
|
5360
|
+
overflow: hidden;
|
|
5361
|
+
opacity: 0;
|
|
5362
|
+
pointer-events: none;
|
|
5363
|
+
}
|
|
5364
|
+
|
|
5365
|
+
/* ═══ HERO BACKGROUND (DL Part IX.4: hero surface treatment) ═══
|
|
5366
|
+
Full-bleed container behind hero content. Holds animated SVG,
|
|
5367
|
+
WebGL canvas, gradient, or any hero visual. */
|
|
5368
|
+
.strand-hero-bg {
|
|
5369
|
+
position: absolute;
|
|
5370
|
+
inset: 0;
|
|
5371
|
+
overflow: hidden;
|
|
5372
|
+
pointer-events: none;
|
|
5373
|
+
z-index: 0;
|
|
5374
|
+
}
|
|
5375
|
+
|
|
5376
|
+
.strand-hero-bg > * {
|
|
5377
|
+
width: 100%;
|
|
5378
|
+
height: 100%;
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5381
|
+
/* ═══ HERO NODE GRID (DL Part IX.4 + Principle 7: alive signal) ═══
|
|
5382
|
+
Animated SVG background showing a field of pulsing connection
|
|
5383
|
+
points. Use inside strand-hero-bg with an inline <svg>. Nodes and
|
|
5384
|
+
lines inherit color via currentColor; wire animation delays with
|
|
5385
|
+
the .strand-hero-grid__line--N modifiers (1-18). */
|
|
5386
|
+
.strand-hero-grid {
|
|
5387
|
+
color: var(--strand-blue-primary);
|
|
5388
|
+
}
|
|
5389
|
+
|
|
5390
|
+
.strand-hero-grid svg {
|
|
5391
|
+
width: 100%;
|
|
5392
|
+
height: 100%;
|
|
5393
|
+
}
|
|
5394
|
+
|
|
5395
|
+
.strand-hero-grid__nodes {
|
|
5396
|
+
fill: currentColor;
|
|
5397
|
+
opacity: 0.2;
|
|
5398
|
+
}
|
|
5399
|
+
|
|
5400
|
+
.strand-hero-grid__lines {
|
|
5401
|
+
stroke: currentColor;
|
|
5402
|
+
fill: none;
|
|
5403
|
+
stroke-width: 0.5;
|
|
5404
|
+
opacity: 0.08;
|
|
5405
|
+
}
|
|
5406
|
+
|
|
5407
|
+
.strand-hero-grid__line {
|
|
5408
|
+
animation: strand-hero-grid-glow 12s var(--strand-ease-in-out-sine) infinite;
|
|
5409
|
+
}
|
|
5410
|
+
|
|
5411
|
+
.strand-hero-grid__line--1 { animation-delay: 0s; }
|
|
5412
|
+
.strand-hero-grid__line--2 { animation-delay: 1s; }
|
|
5413
|
+
.strand-hero-grid__line--3 { animation-delay: 2s; }
|
|
5414
|
+
.strand-hero-grid__line--4 { animation-delay: 0.5s; }
|
|
5415
|
+
.strand-hero-grid__line--5 { animation-delay: 3s; }
|
|
5416
|
+
.strand-hero-grid__line--6 { animation-delay: 1.5s; }
|
|
5417
|
+
.strand-hero-grid__line--7 { animation-delay: 4s; }
|
|
5418
|
+
.strand-hero-grid__line--8 { animation-delay: 2.5s; }
|
|
5419
|
+
.strand-hero-grid__line--9 { animation-delay: 5s; }
|
|
5420
|
+
.strand-hero-grid__line--10 { animation-delay: 3.5s; }
|
|
5421
|
+
.strand-hero-grid__line--11 { animation-delay: 6s; }
|
|
5422
|
+
.strand-hero-grid__line--12 { animation-delay: 4.5s; }
|
|
5423
|
+
.strand-hero-grid__line--13 { animation-delay: 7s; }
|
|
5424
|
+
.strand-hero-grid__line--14 { animation-delay: 5.5s; }
|
|
5425
|
+
.strand-hero-grid__line--15 { animation-delay: 8s; }
|
|
5426
|
+
.strand-hero-grid__line--16 { animation-delay: 6.5s; }
|
|
5427
|
+
.strand-hero-grid__line--17 { animation-delay: 9s; }
|
|
5428
|
+
.strand-hero-grid__line--18 { animation-delay: 7.5s; }
|
|
5429
|
+
|
|
5430
|
+
@keyframes strand-hero-grid-glow {
|
|
5431
|
+
0%, 100% { stroke-opacity: 0.08; }
|
|
5432
|
+
50% { stroke-opacity: 0.4; }
|
|
5433
|
+
}
|
|
5434
|
+
|
|
5435
|
+
/* ═══ PULSE INDICATOR (DL Principle 7: alive signal) ═══
|
|
5436
|
+
Small pulsing dot. Indicates "this element is live/active."
|
|
5437
|
+
Same concept as strand-card--active but as a point indicator.
|
|
5438
|
+
Use as ::after on logos, status dots, live badges. */
|
|
5439
|
+
.strand-pulse {
|
|
5440
|
+
position: relative;
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5443
|
+
.strand-pulse::after {
|
|
5444
|
+
content: "";
|
|
5445
|
+
position: absolute;
|
|
5446
|
+
top: 0;
|
|
5447
|
+
right: -6px;
|
|
5448
|
+
width: 6px;
|
|
5449
|
+
height: 6px;
|
|
5450
|
+
border-radius: var(--strand-radius-full);
|
|
5451
|
+
background: var(--strand-teal-vital);
|
|
5452
|
+
animation: strand-pulse-glow 3s var(--strand-ease-in-out-sine) infinite;
|
|
5453
|
+
}
|
|
5454
|
+
|
|
5455
|
+
@keyframes strand-pulse-glow {
|
|
5456
|
+
0%, 100% { opacity: 0.4; transform: scale(1); }
|
|
5457
|
+
50% { opacity: 1; transform: scale(1.2); }
|
|
5458
|
+
}
|
|
5459
|
+
|
|
5460
|
+
/* ═══ AUTH INDICATOR (nav utility) ═══
|
|
5461
|
+
Small text indicator for signed-in state in nav. */
|
|
5462
|
+
.strand-auth-indicator {
|
|
5463
|
+
font-family: var(--strand-font-mono);
|
|
5464
|
+
font-size: var(--strand-text-xs);
|
|
5465
|
+
color: var(--strand-gray-400);
|
|
5466
|
+
letter-spacing: var(--strand-tracking-wide);
|
|
5467
|
+
white-space: nowrap;
|
|
5468
|
+
}
|
|
5469
|
+
|
|
5470
|
+
.strand-auth-avatar {
|
|
5471
|
+
width: 28px;
|
|
5472
|
+
height: 28px;
|
|
5473
|
+
border-radius: var(--strand-radius-full);
|
|
5474
|
+
background: var(--strand-blue-glow);
|
|
5475
|
+
color: var(--strand-blue-primary);
|
|
5476
|
+
font-family: var(--strand-font-mono);
|
|
5477
|
+
font-size: var(--strand-text-xs);
|
|
5478
|
+
font-weight: var(--strand-weight-semibold);
|
|
5479
|
+
display: inline-flex;
|
|
5480
|
+
align-items: center;
|
|
5481
|
+
justify-content: center;
|
|
5482
|
+
text-decoration: none;
|
|
5483
|
+
}
|
|
5484
|
+
|
|
5485
|
+
/* ═══ EMPTY STATES (DL Part XXI) ═══
|
|
5486
|
+
When an instrument has no data, it does not disappear.
|
|
5487
|
+
It shows an idle state. The label remains, the value
|
|
5488
|
+
becomes a placeholder. The user understands the
|
|
5489
|
+
instrument exists and will activate when data arrives. */
|
|
5490
|
+
|
|
5491
|
+
/* ── Idle Readout (DL Part 21.1) ──
|
|
5492
|
+
A DataReadout with no value. Shows -- in the value
|
|
5493
|
+
position, with the label still visible. Apply alongside
|
|
5494
|
+
.strand-data-readout. */
|
|
5495
|
+
.strand-idle-readout .strand-data-readout__value::before {
|
|
5496
|
+
content: "--";
|
|
5497
|
+
color: var(--strand-gray-300);
|
|
5498
|
+
font-variant-numeric: tabular-nums;
|
|
5499
|
+
}
|
|
5500
|
+
|
|
5501
|
+
.strand-idle-readout .strand-data-readout__value > * {
|
|
5502
|
+
display: none;
|
|
5503
|
+
}
|
|
5504
|
+
|
|
5505
|
+
/* ── Empty Collection (DL Part 21.2) ──
|
|
5506
|
+
A list or grid with no items. Centered message in
|
|
5507
|
+
instrument voice. No illustration. The absence of data
|
|
5508
|
+
IS the visual. */
|
|
5509
|
+
.strand-empty-collection {
|
|
5510
|
+
display: flex;
|
|
5511
|
+
flex-direction: column;
|
|
5512
|
+
align-items: center;
|
|
5513
|
+
justify-content: center;
|
|
5514
|
+
padding: var(--strand-space-16) var(--strand-space-6);
|
|
5515
|
+
text-align: center;
|
|
5516
|
+
color: var(--strand-gray-400);
|
|
5517
|
+
font-family: var(--strand-font-sans);
|
|
5518
|
+
}
|
|
5519
|
+
|
|
5520
|
+
.strand-empty-collection__message {
|
|
5521
|
+
font-size: var(--strand-text-base);
|
|
5522
|
+
color: var(--strand-gray-500);
|
|
5523
|
+
margin: 0;
|
|
5524
|
+
}
|
|
5525
|
+
|
|
5526
|
+
.strand-empty-collection__action {
|
|
5527
|
+
margin-top: var(--strand-space-4);
|
|
5528
|
+
font-family: var(--strand-font-mono);
|
|
5529
|
+
font-size: var(--strand-text-xs);
|
|
5530
|
+
font-weight: var(--strand-weight-medium);
|
|
5531
|
+
letter-spacing: var(--strand-tracking-widest);
|
|
5532
|
+
text-transform: uppercase;
|
|
5533
|
+
color: var(--strand-blue-primary);
|
|
5534
|
+
text-decoration: none;
|
|
5535
|
+
background-image: linear-gradient(var(--strand-blue-primary), var(--strand-blue-primary));
|
|
5536
|
+
background-size: 0% 1px;
|
|
5537
|
+
background-position: left bottom;
|
|
5538
|
+
background-repeat: no-repeat;
|
|
5539
|
+
transition: background-size var(--strand-duration-normal) var(--strand-ease-out-expo);
|
|
5540
|
+
}
|
|
5541
|
+
|
|
5542
|
+
.strand-empty-collection__action:hover {
|
|
5543
|
+
background-size: 100% 1px;
|
|
5544
|
+
}
|
|
5545
|
+
|
|
5546
|
+
/* ── Empty Search (DL Part 21.3) ──
|
|
5547
|
+
"0 matches detected." plus suggestion in secondary text.
|
|
5548
|
+
The search instrument ran and returned nothing. */
|
|
5549
|
+
.strand-empty-search {
|
|
5550
|
+
display: flex;
|
|
5551
|
+
flex-direction: column;
|
|
5552
|
+
align-items: center;
|
|
5553
|
+
padding: var(--strand-space-12) var(--strand-space-6);
|
|
5554
|
+
text-align: center;
|
|
5555
|
+
}
|
|
5556
|
+
|
|
5557
|
+
.strand-empty-search__count {
|
|
5558
|
+
font-family: var(--strand-font-mono);
|
|
5559
|
+
font-size: var(--strand-text-sm);
|
|
5560
|
+
font-weight: var(--strand-weight-medium);
|
|
5561
|
+
letter-spacing: var(--strand-tracking-wider);
|
|
5562
|
+
text-transform: uppercase;
|
|
5563
|
+
color: var(--strand-gray-500);
|
|
5564
|
+
margin-bottom: var(--strand-space-2);
|
|
5565
|
+
}
|
|
5566
|
+
|
|
5567
|
+
.strand-empty-search__suggestion {
|
|
5568
|
+
font-size: var(--strand-text-sm);
|
|
5569
|
+
color: var(--strand-gray-400);
|
|
5570
|
+
line-height: var(--strand-leading-relaxed);
|
|
5571
|
+
max-width: 50ch;
|
|
5572
|
+
}
|
|
5573
|
+
|
|
5574
|
+
/* ═══ UTILITY CLASSES ══════════════════════════════
|
|
5575
|
+
Atomic, single-property helpers. Token-only values.
|
|
5576
|
+
Use sparingly: prefer named primitives and molecules
|
|
5577
|
+
when a coherent concept exists. These exist to absorb
|
|
5578
|
+
the long tail of one-off layout glue without forcing
|
|
5579
|
+
consumers to ship inline styles or per-page CSS files.
|
|
5580
|
+
═══════════════════════════════════════════════════ */
|
|
5581
|
+
|
|
5582
|
+
/* ── Display ── */
|
|
5583
|
+
.strand-block { display: block; }
|
|
5584
|
+
|
|
5585
|
+
/* ── Flex / sizing ── */
|
|
5586
|
+
.strand-flex-1 { flex: 1; }
|
|
5587
|
+
.strand-min-w-0 { min-width: 0; }
|
|
5588
|
+
.strand-full-width { width: 100%; }
|
|
5589
|
+
.strand-w-full { width: 100%; }
|
|
5590
|
+
|
|
5591
|
+
/* ── Margin top (token scale 1-8) ── */
|
|
5592
|
+
.strand-mt-1 { margin-top: var(--strand-space-1); }
|
|
5593
|
+
.strand-mt-2 { margin-top: var(--strand-space-2); }
|
|
5594
|
+
.strand-mt-3 { margin-top: var(--strand-space-3); }
|
|
5595
|
+
.strand-mt-4 { margin-top: var(--strand-space-4); }
|
|
5596
|
+
.strand-mt-5 { margin-top: var(--strand-space-5); }
|
|
5597
|
+
.strand-mt-6 { margin-top: var(--strand-space-6); }
|
|
5598
|
+
.strand-mt-8 { margin-top: var(--strand-space-8); }
|
|
5599
|
+
|
|
5600
|
+
/* ── Margin bottom (token scale 1-8) ── */
|
|
5601
|
+
.strand-mb-1 { margin-bottom: var(--strand-space-1); }
|
|
5602
|
+
.strand-mb-2 { margin-bottom: var(--strand-space-2); }
|
|
5603
|
+
.strand-mb-3 { margin-bottom: var(--strand-space-3); }
|
|
5604
|
+
.strand-mb-4 { margin-bottom: var(--strand-space-4); }
|
|
5605
|
+
.strand-mb-5 { margin-bottom: var(--strand-space-5); }
|
|
5606
|
+
.strand-mb-6 { margin-bottom: var(--strand-space-6); }
|
|
5607
|
+
.strand-mb-8 { margin-bottom: var(--strand-space-8); }
|
|
5608
|
+
|
|
5609
|
+
/* ── Margin auto (centering) ── */
|
|
5610
|
+
.strand-mx-auto { margin-inline: auto; }
|
|
5611
|
+
|
|
5612
|
+
@media (prefers-reduced-motion: reduce) {
|
|
5613
|
+
.strand-footer__link {
|
|
5614
|
+
transition: none;
|
|
5615
|
+
}
|
|
5616
|
+
|
|
5617
|
+
.strand-pulse::after {
|
|
5618
|
+
animation: none;
|
|
5619
|
+
opacity: 0.6;
|
|
5620
|
+
}
|
|
5621
|
+
|
|
5622
|
+
.strand-hero-bg {
|
|
5623
|
+
display: none;
|
|
5624
|
+
}
|
|
5625
|
+
}
|
|
5626
|
+
|