tin-spa 20.13.0 → 20.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tin-spa",
3
- "version": "20.13.0",
3
+ "version": "20.14.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/tnyagato/ng-space.git"
@@ -454,9 +454,26 @@ table mat-icon,
454
454
  }
455
455
 
456
456
  .mat-mdc-text-field-wrapper {
457
-
457
+
458
458
  padding-top: 0px !important; /* Remove extra padding above input */
459
459
  padding-bottom: 0px !important; /* Remove extra padding below input */
460
+
461
+ /* FORM-4 — restore the horizontal inset on BOTH sides, always.
462
+ Material zeroes padding-right when a field has an icon suffix, and padding-left when it has a prefix,
463
+ because it assumes the adjacent control is a stock 48px mat-icon-button whose own padding supplies the
464
+ visual gap. This library breaks that assumption on purpose: `.suffix-icons .mat-mdc-icon-button` is forced
465
+ to 20x20 with `padding: 0` so the icons read as small. The gap Material handed over therefore never came
466
+ back, and every measured field had its trailing icon sitting EXACTLY 0.0px from the border — the select
467
+ arrows, the bitwise action icons, the password toggle, and the money field's "USD" prefix hard against
468
+ the left edge.
469
+ It affected every field, not only the ones with visible icons: all 13 components render their
470
+ `<div matSuffix class="suffix-icons">` unconditionally, so `mat-mdc-form-field-has-icon-suffix` is
471
+ present even when spa-suffix draws nothing, and plain text fields were left with a 4px right inset
472
+ against a 16px left one.
473
+ 16px matches the inset Material already applies on the unadorned side, so adorned and unadorned fields
474
+ line up with each other instead of each having its own margin. */
475
+ padding-left: 16px !important;
476
+ padding-right: 16px !important;
460
477
  }
461
478
 
462
479
  /* Center floating label vertically only when empty and not focused */
@@ -470,6 +487,70 @@ table mat-icon,
470
487
  margin-bottom: 5px !important; /* Minimal top margin from hint area */
471
488
  }
472
489
 
490
+ /* ---------------------------------------------------------------------------
491
+ FORM FIELD ALIGNMENT (FORM-1/2/3)
492
+
493
+ Every form row is a .tin-grid cell, and the cells are top-aligned. That keeps
494
+ the CONTROL BAND (the boxed part of a mat-form-field) of two neighbours level
495
+ only while both cells start at the same offset — which is why any component
496
+ that is not a mat-form-field, and any change in a field's own height, shows up
497
+ as a misaligned row.
498
+
499
+ Two numbers govern the whole thing, measured off the rendered form rather than
500
+ assumed: the control band is 40px (infix 8px + input 24px + 8px above), and a
501
+ hint or error adds exactly 20px of subscript beneath it.
502
+ --------------------------------------------------------------------------- */
503
+ :root {
504
+ --tin-field-band: 40px; /* height of a mat-form-field's boxed control area */
505
+ --tin-field-subscript: 20px; /* height one line of hint/error text occupies */
506
+ }
507
+
508
+ /* FORM-2 — a hint must not change a field's height.
509
+
510
+ subscriptSizing="dynamic" (used by all 13 field components) collapses the
511
+ subscript to 0 when there is no hint and expands it to 20px when there is, so a
512
+ hinted field is 20px taller than the plain field beside it. The tops still line
513
+ up, but the row grows and every following row is pushed down, which is what
514
+ reads as "the form is misaligned".
515
+
516
+ Reserving the band unconditionally makes every field exactly the same height
517
+ whether or not it has a hint — and, more valuably, stops the form jumping when a
518
+ validation error appears mid-typing. The 5px margin above is dropped in the same
519
+ breath, so a plain field grows by 15px rather than 20 and a hinted field
520
+ actually loses 5px. Scoped to spa-form so filters and toolbars keep their
521
+ current compact height. */
522
+ spa-form .mat-mdc-form-field-subscript-wrapper {
523
+ min-height: var(--tin-field-subscript) !important;
524
+ margin-bottom: 0 !important;
525
+ }
526
+
527
+ /* FORM-1 / FORM-3 — components that are NOT mat-form-fields must still centre
528
+ themselves on the control band of the field beside them.
529
+
530
+ spa-label rendered as a bare text block anchored to the top of its cell, so a
531
+ read-only label sat several pixels above the input text it was paired with (the
532
+ reported "Balance : USD 500.00" row). spa-check happens to be 40px today and so
533
+ lines up by coincidence; pinning it to the same variable makes that a rule
534
+ rather than a coincidence, and keeps it true if Material's density changes.
535
+
536
+ Scoped to spa-form ON PURPOSE. Both components are also used outside forms —
537
+ spa-check in table-row, title-actions and page toolbars, spa-label in the
538
+ profile and tenant-settings pages — where a 40px floor and a flex host would
539
+ inflate table rows and change inline flow for no benefit. */
540
+ spa-form spa-label,
541
+ spa-form spa-check {
542
+ display: flex;
543
+ align-items: center;
544
+ min-height: var(--tin-field-band);
545
+ }
546
+
547
+ /* Composite subfields share a row via flex, which defaults to align-items:stretch —
548
+ a checkbox or label among boxed inputs would stretch instead of sitting on the
549
+ band. Match the grid's top alignment so composites align by the same rule. */
550
+ .composite-field-group {
551
+ align-items: flex-start;
552
+ }
553
+
473
554
  /* Set default white background for all Material cards */
474
555
  .mat-mdc-card,
475
556
  mat-card {
@@ -733,3 +814,446 @@ spa-groups mat-chip .mat-icon {
733
814
  .spa-editor-wrap.spa-readonly .spa-editor-label label {
734
815
  color: rgba(0, 0, 0, 0.38);
735
816
  }
817
+
818
+ /* ══════════════════════════════════════════════════════════════════════════════════════════════════════
819
+ MODERN DIALOGS — opt-in via appConfig.dialogStyle = 'modern'
820
+ ══════════════════════════════════════════════════════════════════════════════════════════════════════
821
+ Dialogs add the `tin-dialog-modern` panel class to their own overlay pane when the app opts in. These
822
+ rules live here, globally, because the Material dialog SURFACE is an ancestor of every dialog component's
823
+ view — component CSS can style downwards but never up to its own container.
824
+
825
+ Design intent: this is a dense line-of-business app, so "modern" means a calmer, more deliberate surface —
826
+ softer corners, layered depth instead of a flat drop shadow, a clear header band, and dialog actions that
827
+ read as a footer rather than as loose buttons. It stays inside Material's language; the point is to make
828
+ the app feel considered, not to make dialogs look like they came from a different product.
829
+ Nothing below applies unless the panel class is present, so default apps are untouched. */
830
+
831
+ /* Band colour is a VARIABLE, not a hard-coded grey, so an app can retheme every dialog band from one place:
832
+ :root { --tin-dlg-band: #ede9f6; --tin-dlg-band-text: rgba(16,24,40,0.92); }
833
+
834
+ The default is a faintly violet neutral rather than the blue-grey used almost everywhere else in the shell
835
+ (page background, table rows, hover states). Those were all near-neighbours, so the band read as "more of
836
+ the same surface" instead of as chrome — at this level of subtlety it has to differ in HUE, not just in
837
+ lightness, to register at all.
838
+
839
+ It is deliberately SOLID. A translucent band lets table rows and text show through, and the header becomes
840
+ unreadable over busy content.
841
+
842
+ On following the navigation colour: that is one line — set --tin-dlg-band to your navColor. But navColor is
843
+ frequently near-black (test-spa uses rgba(0,0,0,0.78)), and a dark band with dark text is unreadable, so
844
+ --tin-dlg-band-text exists to be flipped to white in that case. It is NOT derived automatically, because
845
+ silently inverting text based on a colour an app set for a different purpose is how you end up with a
846
+ dialog nobody can read on exactly one deployment. */
847
+ :root {
848
+ --tin-dlg-band: #f2f1f7;
849
+ --tin-dlg-band-border: rgba(31, 27, 61, 0.13);
850
+ --tin-dlg-band-text: rgba(16, 24, 40, 0.92);
851
+ --tin-dlg-band-muted: rgba(16, 24, 40, 0.55);
852
+ }
853
+
854
+ .tin-dialog-modern .mat-mdc-dialog-surface {
855
+ border-radius: 14px;
856
+ /* Two layered shadows rather than one: a tight contact shadow to seat the dialog, and a wide soft one for
857
+ depth. A single large blur reads as haze and makes the edge look undefined against a busy table. */
858
+ box-shadow:
859
+ 0 1px 2px rgba(16, 24, 40, 0.10),
860
+ 0 12px 32px -8px rgba(16, 24, 40, 0.28) !important;
861
+ overflow: hidden; /* lets the header band and footer meet the rounded corners cleanly */
862
+ }
863
+
864
+ /* Header band — separates "what this dialog is" from its content without a heavy rule. */
865
+ .tin-dialog-modern .tin-dlg-head {
866
+ display: flex;
867
+ align-items: center;
868
+ justify-content: space-between;
869
+ gap: 12px;
870
+ padding: 14px 20px;
871
+ /* A flat tint rather than a gradient. The gradient was too faint to read as a band at all, which left the
872
+ dialog looking exactly as plain as before — the whole point of the band is that the eye can tell where the
873
+ chrome ends and the record begins. */
874
+ background: var(--tin-dlg-band);
875
+ border-bottom: 1px solid var(--tin-dlg-band-border);
876
+ }
877
+
878
+ .tin-dialog-modern .tin-dlg-title {
879
+ margin: 0;
880
+ font-size: 17px;
881
+ font-weight: 600;
882
+ letter-spacing: -0.01em;
883
+ color: var(--tin-dlg-band-text);
884
+ line-height: 1.3;
885
+ }
886
+
887
+ /* NOTE: the .tin-dlg-sub subtitle was REMOVED from the details dialog on request — the record reference under
888
+ the title was noise rather than information. The variable --tin-dlg-band-muted remains for any future
889
+ secondary text inside a band; do not reintroduce a subtitle without being asked. */
890
+
891
+ /* The header's action icons (edit, refresh, close) are chrome, not content — grouping them on a subtle
892
+ surface stops them competing with the title for attention and gives the band a right-hand anchor. */
893
+ .tin-dialog-modern .tin-dlg-head .mat-mdc-icon-button {
894
+ width: 36px;
895
+ height: 36px;
896
+ padding: 0;
897
+ }
898
+
899
+ /* Footer — a seated action bar. Buttons stay Material; the band is what makes them read as a footer. */
900
+ .tin-dialog-modern .mat-mdc-dialog-actions {
901
+ padding: 12px 20px;
902
+ background: var(--tin-dlg-band);
903
+ border-top: 1px solid var(--tin-dlg-band-border);
904
+ margin: 0;
905
+ min-height: unset;
906
+ }
907
+
908
+ .tin-dialog-modern .mat-mdc-dialog-content {
909
+ padding-top: 16px;
910
+ padding-bottom: 16px;
911
+ }
912
+
913
+ /* Tabs inside a modern dialog: a lighter, tighter bar. The default Material tab header is tall enough to
914
+ dominate a dialog whose whole purpose is the form underneath it. */
915
+ /* Tinted so the tab strip reads as a divider between the record's own fields above and its related lists
916
+ below. On a plain white surface the tabs float, and a long form runs into its child tables with nothing
917
+ marking the boundary — which is most of why this dialog reads as "plain". */
918
+ .tin-dialog-modern .mat-mdc-tab-header {
919
+ background: var(--tin-dlg-band);
920
+ border-top: 1px solid var(--tin-dlg-band-border);
921
+ border-bottom: 1px solid var(--tin-dlg-band-border);
922
+ --mat-tab-header-divider-height: 0;
923
+ }
924
+
925
+ .tin-dialog-modern .mat-mdc-tab .mdc-tab__text-label {
926
+ font-size: 13px;
927
+ font-weight: 500;
928
+ letter-spacing: 0.01em;
929
+ }
930
+
931
+ @media (max-width: 480px) {
932
+ .tin-dialog-modern .mat-mdc-dialog-surface { border-radius: 12px; }
933
+ .tin-dialog-modern .tin-dlg-head { padding: 12px 16px; }
934
+ .tin-dialog-modern .mat-mdc-dialog-actions { padding: 10px 16px; }
935
+ }
936
+
937
+ /* ---------------------------------------------------------------------------
938
+ QUIET LOADING — SKELETON FOUNDATION
939
+
940
+ Shared placeholder primitives for the opt-in Quiet Loading style: table ghost
941
+ rows, tile value chips and the details-dialog form skeleton all build out of
942
+ these two classes so they shimmer as one system rather than three.
943
+
944
+ Colours are color-mix over the Material 3 system tokens, never hardcoded
945
+ greys, so a skeleton themes itself in light, dark and any branded palette.
946
+ Heights reuse --tin-field-band (declared with the form-field alignment rules
947
+ above) so a form skeleton occupies exactly the space the real control band
948
+ will, and nothing jumps when the data arrives.
949
+ --------------------------------------------------------------------------- */
950
+ :root {
951
+ --tin-skel-base: color-mix(in srgb, var(--mat-sys-on-surface, #000) 8%, transparent); /* resting band */
952
+ --tin-skel-sheen: color-mix(in srgb, var(--mat-sys-on-surface, #000) 14%, transparent); /* moving highlight */
953
+ --tin-skel-radius: 4px;
954
+ --tin-skel-speed: 1.4s;
955
+ }
956
+
957
+ /* Base chip. The 200% background is what the sweep translates across; without it the
958
+ gradient has nowhere to travel and the animation renders as a static band. */
959
+ .tin-skel {
960
+ display: block;
961
+ border-radius: var(--tin-skel-radius);
962
+ background-color: var(--tin-skel-base);
963
+ background-image: linear-gradient(90deg, transparent 0%, var(--tin-skel-sheen) 50%, transparent 100%);
964
+ background-size: 200% 100%;
965
+ background-repeat: no-repeat;
966
+ background-position: -100% 0;
967
+ animation: tin-skel-sweep var(--tin-skel-speed) ease-in-out infinite;
968
+ /* a skeleton is decoration around content that has not arrived — never selectable or interactive */
969
+ user-select: none;
970
+ pointer-events: none;
971
+ }
972
+
973
+ /* Inline variant for text-shaped placeholders (labels, captions, tile values):
974
+ sits on the text baseline and takes its height from the surrounding font size. */
975
+ .tin-skel-text {
976
+ display: inline-block;
977
+ height: 1em;
978
+ vertical-align: middle;
979
+ border-radius: var(--tin-skel-radius);
980
+ }
981
+
982
+ @keyframes tin-skel-sweep {
983
+ from { background-position: -100% 0; }
984
+ to { background-position: 100% 0; }
985
+ }
986
+
987
+ /* Reduced motion: the placeholder still communicates "content is coming" by occupying
988
+ the space, but nothing moves. The sheen is dropped rather than paused so the chip
989
+ does not freeze mid-sweep with a bright band stuck across it. */
990
+ @media (prefers-reduced-motion: reduce) {
991
+ .tin-skel {
992
+ animation: none;
993
+ background-image: none;
994
+ }
995
+ }
996
+
997
+ /* ---------------------------------------------------------------------------
998
+ QUIET LOADING — TABLE STAGE (PHASE 2, FSD D3/D4)
999
+
1000
+ Two loading states for a table, and only ever one of them at a time:
1001
+
1002
+ .tin-load-stage first load, nothing on screen yet — an eased progress bar
1003
+ with a counting percentage, a caption, and ghost rows that
1004
+ hold the exact space the real rows will fill.
1005
+ .tin-load-line refresh, rows already on screen — a 2px line under the
1006
+ header and a spinning refresh icon, nothing else. Rows
1007
+ are never dimmed or covered; they stay live.
1008
+
1009
+ Lives here rather than in table.component.css because .tin-spin is shared
1010
+ with the filter component (which owns the refresh button) and the palette
1011
+ has to match the skeleton primitives declared directly above.
1012
+ --------------------------------------------------------------------------- */
1013
+
1014
+ .tin-load-stage {
1015
+ padding: 8px 0 4px 0;
1016
+ }
1017
+
1018
+ /* Progress module — bar and percentage on one baseline */
1019
+ .tin-load-progress {
1020
+ display: flex;
1021
+ align-items: center;
1022
+ gap: 12px;
1023
+ }
1024
+
1025
+ .tin-load-bar {
1026
+ position: relative;
1027
+ flex: 1 1 auto;
1028
+ height: 3px;
1029
+ border-radius: 999px;
1030
+ background-color: var(--tin-skel-base);
1031
+ overflow: hidden;
1032
+ }
1033
+
1034
+ /* The fill is driven from the component (width %, PerceivedProgress curve). The transition only smooths the
1035
+ gap between animation frames — the easing itself is in the curve, not here. */
1036
+ .tin-load-bar-fill {
1037
+ position: absolute;
1038
+ inset: 0 auto 0 0;
1039
+ display: block;
1040
+ width: 0;
1041
+ border-radius: 999px;
1042
+ background-color: var(--mat-sys-primary, #1976d2);
1043
+ transition: width 120ms linear;
1044
+ }
1045
+
1046
+ /* Soft sheen travelling along the filled portion, so a stalled backend still looks alive */
1047
+ .tin-load-bar-fill::after {
1048
+ content: '';
1049
+ position: absolute;
1050
+ inset: 0;
1051
+ background-image: linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--mat-sys-on-primary, #fff) 45%, transparent) 50%, transparent 100%);
1052
+ background-size: 200% 100%;
1053
+ animation: tin-skel-sweep var(--tin-skel-speed) ease-in-out infinite;
1054
+ }
1055
+
1056
+ /* Tabular numerals: without them the width of the digits changes as the value counts up and the caption jitters. */
1057
+ .tin-load-percent {
1058
+ flex: 0 0 auto;
1059
+ min-width: 4ch;
1060
+ text-align: right;
1061
+ font-size: 12px;
1062
+ font-variant-numeric: tabular-nums;
1063
+ font-feature-settings: 'tnum' 1;
1064
+ color: color-mix(in srgb, var(--mat-sys-on-surface, #000) 60%, transparent);
1065
+ }
1066
+
1067
+ .tin-load-caption {
1068
+ margin-top: 6px;
1069
+ font-size: 12px;
1070
+ color: color-mix(in srgb, var(--mat-sys-on-surface, #000) 55%, transparent);
1071
+ }
1072
+
1073
+ /* Ghost rows — one cell per visible column, so the stage is the same shape as the table that replaces it */
1074
+ .tin-load-ghosts {
1075
+ margin-top: 12px;
1076
+ }
1077
+
1078
+ .tin-load-ghost-row {
1079
+ display: flex;
1080
+ align-items: center;
1081
+ gap: 16px;
1082
+ padding: 0 10px;
1083
+ height: var(--tin-field-band, 40px);
1084
+ border-bottom: 1px solid color-mix(in srgb, var(--mat-sys-on-surface, #000) 6%, transparent);
1085
+ }
1086
+
1087
+ .tin-load-ghost-cell {
1088
+ flex: 1 1 0;
1089
+ min-width: 0;
1090
+ }
1091
+
1092
+ .tin-load-ghost-cell .tin-skel {
1093
+ height: 12px;
1094
+ }
1095
+
1096
+ /* Refresh: a 2px line flush under the table header. Deliberately thinner and quieter than the initial bar —
1097
+ the user already has content to read, this is peripheral. */
1098
+ .tin-load-line {
1099
+ position: relative;
1100
+ height: 2px;
1101
+ margin-bottom: 4px;
1102
+ background-color: var(--tin-skel-base);
1103
+ overflow: hidden;
1104
+ }
1105
+
1106
+ .tin-load-line-fill {
1107
+ position: absolute;
1108
+ inset: 0 auto 0 0;
1109
+ display: block;
1110
+ width: 0;
1111
+ background-color: color-mix(in srgb, var(--mat-sys-primary, #1976d2) 70%, transparent);
1112
+ transition: width 120ms linear;
1113
+ }
1114
+
1115
+ /* Hand-off: the real rows arrive where the ghosts were, so they fade and settle rather than snapping in. The
1116
+ animation fires once, when the table element is created — a refresh never re-runs it because the element is
1117
+ already in the DOM (D4: rows are never re-mounted on refresh). */
1118
+ .tin-load-in {
1119
+ animation: tin-load-fade-in 180ms ease-out;
1120
+ }
1121
+
1122
+ @keyframes tin-load-fade-in {
1123
+ from { opacity: 0; transform: translateY(4px); }
1124
+ to { opacity: 1; transform: none; }
1125
+ }
1126
+
1127
+ /* Shared spin, applied to the header refresh icon while a quiet refresh is in flight */
1128
+ .tin-spin {
1129
+ animation: tin-spin-rotate 1.2s linear infinite;
1130
+ }
1131
+
1132
+ @keyframes tin-spin-rotate {
1133
+ to { transform: rotate(360deg); }
1134
+ }
1135
+
1136
+ /* Reduced motion: the stage still reports progress (the bar grows, the percentage counts — both are value
1137
+ changes, not motion), but nothing sweeps, pulses or rotates. */
1138
+ @media (prefers-reduced-motion: reduce) {
1139
+ .tin-load-bar-fill::after {
1140
+ animation: none;
1141
+ background-image: none;
1142
+ }
1143
+
1144
+ .tin-spin,
1145
+ .tin-load-in {
1146
+ animation: none;
1147
+ }
1148
+ }
1149
+
1150
+ /* ---------------------------------------------------------------------------
1151
+ QUIET LOADING — ghost furniture for the NON-table view types.
1152
+
1153
+ The table's ghost rows are column-shaped, which is exactly wrong for a card
1154
+ grid or a capsule strip: a placeholder should promise the layout that is
1155
+ about to arrive, and a row of cells promises a table. Each view therefore
1156
+ gets its own shape. They all reuse the shared .tin-skel shimmer, so there is
1157
+ one animation and one palette across every surface.
1158
+ --------------------------------------------------------------------------- */
1159
+
1160
+ /* Capsules — a wrapping strip of pills. Varied widths so it reads as labels
1161
+ rather than as a progress meter; deterministic, so it looks the same each time. */
1162
+ .tin-load-ghost-capsules {
1163
+ display: flex;
1164
+ flex-wrap: wrap;
1165
+ gap: 10px;
1166
+ margin-top: 14px;
1167
+ }
1168
+
1169
+ .tin-load-ghost-capsule {
1170
+ height: 32px;
1171
+ width: 96px;
1172
+ border-radius: 16px;
1173
+ }
1174
+
1175
+ .tin-load-ghost-capsules .tin-load-ghost-capsule:nth-child(2) { width: 132px; }
1176
+ .tin-load-ghost-capsules .tin-load-ghost-capsule:nth-child(3) { width: 78px; }
1177
+ .tin-load-ghost-capsules .tin-load-ghost-capsule:nth-child(4) { width: 116px; }
1178
+ .tin-load-ghost-capsules .tin-load-ghost-capsule:nth-child(5) { width: 88px; }
1179
+
1180
+ /* Cards — the same auto-fit grid the real card view uses, so the ghosts occupy
1181
+ the space the cards will land in rather than reflowing when they arrive. */
1182
+ .tin-load-ghost-cards {
1183
+ display: grid;
1184
+ grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
1185
+ gap: 16px;
1186
+ margin-top: 14px;
1187
+ }
1188
+
1189
+ .tin-load-ghost-card {
1190
+ display: flex;
1191
+ flex-direction: column;
1192
+ gap: 10px;
1193
+ padding: 16px;
1194
+ border-radius: 8px;
1195
+ border: 1px solid color-mix(in srgb, var(--mat-sys-on-surface, #000) 8%, transparent);
1196
+ }
1197
+
1198
+ .tin-load-ghost-card-title {
1199
+ height: 1.15em;
1200
+ width: 55%;
1201
+ }
1202
+
1203
+ .tin-load-ghost-card-short {
1204
+ width: 40%;
1205
+ }
1206
+
1207
+ /* Grouped — a heading bar over a few rows, twice. Mirrors the group-card header
1208
+ whose alignment was fixed in WS-8, so the stage lines up with what replaces it. */
1209
+ .tin-load-ghost-groups {
1210
+ display: flex;
1211
+ flex-direction: column;
1212
+ gap: 16px;
1213
+ margin-top: 14px;
1214
+ }
1215
+
1216
+ .tin-load-ghost-group {
1217
+ display: flex;
1218
+ flex-direction: column;
1219
+ gap: 8px;
1220
+ padding: 12px 14px;
1221
+ border-radius: 8px;
1222
+ border: 1px solid color-mix(in srgb, var(--mat-sys-on-surface, #000) 8%, transparent);
1223
+ }
1224
+
1225
+ .tin-load-ghost-group-head {
1226
+ height: 20px;
1227
+ width: 34%;
1228
+ margin-bottom: 4px;
1229
+ }
1230
+
1231
+ .tin-load-ghost-group-row {
1232
+ width: 100%;
1233
+ }
1234
+
1235
+ .tin-load-ghost-groups .tin-load-ghost-group-row:last-child { width: 62%; }
1236
+
1237
+ /* Grouped ghosts sit on the same white card surface the real group cards use — an outline alone read as a
1238
+ flat bar against the page tint and did not look like a card at all. */
1239
+ .tin-load-ghost-group {
1240
+ background: var(--mat-sys-surface, #fff);
1241
+ box-shadow: 0 1px 3px color-mix(in srgb, var(--mat-sys-on-surface, #000) 10%, transparent);
1242
+ border-color: transparent;
1243
+ }
1244
+
1245
+ .tin-load-ghost-group-items {
1246
+ display: flex;
1247
+ flex-wrap: wrap;
1248
+ gap: 10px;
1249
+ margin-top: 4px;
1250
+ }
1251
+
1252
+ /* Card ghosts get the same surface treatment as the grouped ones, and for the same reason: a 8%-opacity
1253
+ outline on a tinted page reads as a flat band, not as a card. The real card view is a white raised
1254
+ surface, so the placeholder should be too. */
1255
+ .tin-load-ghost-card {
1256
+ background: var(--mat-sys-surface, #fff);
1257
+ box-shadow: 0 1px 3px color-mix(in srgb, var(--mat-sys-on-surface, #000) 10%, transparent);
1258
+ border-color: transparent;
1259
+ }