@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.
@@ -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
- /* ── Base ── */
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-fast) var(--strand-ease-out-expo),
518
- box-shadow var(--strand-duration-fast) var(--strand-ease-out-expo);
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
- /* ── Focus ring (interactive cards) ── */
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
- /* ── Reduced motion ── */
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
- .strand-code-block__pre {
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-midnight);
692
- background: var(--strand-surface-recessed);
693
- box-shadow: var(--strand-shadow-inset);
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-red-alert);
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
- /* Optional grid overlay */
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(rgba(59, 142, 246, 0.04) 1px, transparent 1px),
1144
- linear-gradient(90deg, rgba(59, 142, 246, 0.04) 1px, transparent 1px);
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
- /* Link */
1151
- /*! Strand UI | MIT License | dillingerstaffing.com */
1152
-
1153
- /*! Strand UI | MIT License | dillingerstaffing.com */
1154
-
1155
- .strand-link {
1156
- color: var(--strand-blue-primary);
1157
- text-decoration: none;
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
- .strand-link:hover {
1168
- background-size: 100% 1px;
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
- /* ── Focus ring ── */
1172
- .strand-link:focus-visible {
1173
- outline: 2px solid var(--strand-blue-primary);
1174
- outline-offset: 2px;
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
- /* ── CTA link variant ── */
1178
- .strand-link--cta {
1179
- display: inline-flex;
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
- .strand-link--cta:hover {
1187
- color: var(--strand-blue-vivid);
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
- position: relative;
1219
- width: 100%;
1220
- height: var(--strand-nav-height);
1221
- background: var(--strand-surface-elevated);
1222
- border-bottom: 1px solid var(--strand-gray-200);
1223
- font-family: var(--strand-font-sans);
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
- /* ── Logo ── */
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
- flex-shrink: 0;
1239
- margin-right: var(--strand-space-8);
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
- display: flex;
1245
- align-items: center;
1246
- gap: var(--strand-space-6);
1247
- flex: 1;
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
- color: var(--strand-gray-600);
1253
- text-decoration: none;
1254
- font-size: var(--strand-text-sm);
1255
- font-weight: var(--strand-weight-medium);
1256
- transition: color var(--strand-duration-fast) var(--strand-ease-out-quart);
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
- color: var(--strand-gray-900);
3069
+ color: var(--strand-gray-900);
1261
3070
  }
1262
3071
 
1263
3072
  .strand-nav__link:focus-visible {
1264
- outline: 2px solid var(--strand-blue-primary);
1265
- outline-offset: 2px;
3073
+ outline: 2px solid var(--strand-blue-primary);
3074
+ outline-offset: 2px;
1266
3075
  }
1267
3076
 
1268
3077
  .strand-nav__link--active {
1269
- color: var(--strand-blue-primary);
1270
- font-weight: var(--strand-weight-medium);
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
- display: flex;
1276
- align-items: center;
1277
- gap: var(--strand-space-3);
1278
- margin-left: auto;
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
- display: none;
1284
- align-items: center;
1285
- justify-content: center;
1286
- width: 40px;
1287
- height: 40px;
1288
- margin-left: auto;
1289
- padding: 0;
1290
- border: none;
1291
- border-radius: var(--strand-radius-md);
1292
- background: transparent;
1293
- color: var(--strand-gray-600);
1294
- cursor: pointer;
1295
- transition: background var(--strand-duration-fast) var(--strand-ease-out-quart);
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
- background: var(--strand-gray-200);
3109
+ background: var(--strand-gray-200);
1300
3110
  }
1301
3111
 
1302
3112
  .strand-nav__hamburger:focus-visible {
1303
- outline: 2px solid var(--strand-blue-primary);
1304
- outline-offset: 2px;
3113
+ outline: 2px solid var(--strand-blue-primary);
3114
+ outline-offset: 2px;
1305
3115
  }
1306
3116
 
1307
3117
  .strand-nav__hamburger-icon {
1308
- display: block;
1309
- width: 20px;
1310
- height: 2px;
1311
- background: currentColor;
1312
- position: relative;
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
- content: "";
1318
- position: absolute;
1319
- left: 0;
1320
- width: 100%;
1321
- height: 2px;
1322
- background: currentColor;
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
- top: -6px;
3136
+ top: -6px;
1327
3137
  }
1328
3138
 
1329
3139
  .strand-nav__hamburger-icon::after {
1330
- top: 6px;
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
- display: none;
1336
- flex-direction: column;
1337
- width: 100%;
1338
- padding: var(--strand-space-4) var(--strand-space-6);
1339
- background: var(--strand-surface-elevated);
1340
- border-bottom: 1px solid var(--strand-gray-200);
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
- display: block;
1345
- padding: var(--strand-space-3) 0;
1346
- color: var(--strand-gray-600);
1347
- text-decoration: none;
1348
- font-size: var(--strand-text-sm);
1349
- font-weight: var(--strand-weight-medium);
1350
- transition: color var(--strand-duration-fast) var(--strand-ease-out-quart);
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
- color: var(--strand-gray-900);
3178
+ color: var(--strand-gray-900);
1355
3179
  }
1356
3180
 
1357
3181
  .strand-nav__mobile-link--active {
1358
- color: var(--strand-blue-primary);
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
- .strand-nav__items {
1364
- display: none;
1365
- }
3191
+ .strand-nav__items {
3192
+ display: none;
3193
+ }
1366
3194
 
1367
- .strand-nav__actions {
1368
- display: none;
1369
- }
3195
+ .strand-nav__actions {
3196
+ display: none;
3197
+ }
1370
3198
 
1371
- .strand-nav__hamburger {
1372
- display: inline-flex;
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
- .strand-nav__mobile-menu {
1376
- display: flex;
1377
- }
3208
+ .strand-nav__hamburger {
3209
+ display: inline-flex;
3210
+ }
1378
3211
 
1379
- .strand-nav {
1380
- height: auto;
1381
- min-height: var(--strand-nav-height);
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
- position: fixed;
1388
- top: 0;
1389
- left: 0;
1390
- right: 0;
1391
- z-index: 100;
1392
- height: auto;
1393
- padding: var(--strand-space-4) 0;
1394
- background: rgba(250, 252, 254, 0.85);
1395
- backdrop-filter: blur(12px);
1396
- -webkit-backdrop-filter: blur(12px);
1397
- border-bottom: 1px solid rgba(148, 163, 184, 0.1);
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
- .strand-nav__link,
1403
- .strand-nav__mobile-link,
1404
- .strand-nav__hamburger {
1405
- transition: none;
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 1.5s var(--strand-ease-in-out-sine) infinite;
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
- width: 100%;
3523
+ width: 100%;
1678
3524
  }
1679
3525
 
1680
3526
  /* ── Variants ── */
1681
3527
  .strand-section--standard {
1682
- padding-block: clamp(4rem, 8vw, 8rem);
3528
+ padding-block: clamp(4rem, 8vw, 8rem);
1683
3529
  }
1684
3530
 
1685
3531
  .strand-section--hero {
1686
- padding-block: clamp(6rem, 12vw, 12rem);
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
- background-color: var(--strand-surface-primary);
3560
+ background-color: var(--strand-surface-primary);
1692
3561
  }
1693
3562
 
1694
3563
  .strand-section--bg-elevated {
1695
- background-color: var(--strand-surface-elevated);
3564
+ background-color: var(--strand-surface-elevated);
1696
3565
  }
1697
3566
 
1698
3567
  .strand-section--bg-recessed {
1699
- background-color: var(--strand-surface-recessed);
3568
+ background-color: var(--strand-surface-recessed);
1700
3569
  }
1701
3570
 
1702
3571
  /* ── Compact variant ── */
1703
3572
  .strand-section--compact {
1704
- padding-block: var(--strand-space-12);
3573
+ padding-block: var(--strand-space-12);
1705
3574
  }
1706
3575
 
1707
3576
  /* ── Border variant ── */
1708
3577
  .strand-section--border-top {
1709
- border-top: 1px solid var(--strand-gray-200);
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: 6px;
3763
+ min-height: var(--strand-touch-target);
3764
+ height: var(--strand-touch-target);
1867
3765
  appearance: none;
1868
- background: var(--strand-gray-200);
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
- /* ── Thumb: Webkit (44px touch target, 20px visual) ── */
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: 12px solid transparent;
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 (44px touch target, 20px visual) ── */
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: 12px solid transparent;
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-static *,
2748
- .strand-static *::before,
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
- /* ── Recessed viewport ──
2755
- The instrument viewport sits below the card surface.
2756
- Use for component previews, showcases, and documentation. */
2757
-
2758
- .strand-viewport {
2759
- background: var(--strand-surface-recessed);
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
- /* ── Overline (specimen label pattern, DL Part IV.5) ── */
2766
- .strand-overline {
2767
- font-family: var(--strand-font-mono);
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
+