@7365admin1/layer-common 3.2.7 → 3.2.8-staging.201

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.
Files changed (283) hide show
  1. package/assets/css/primitives.css +1113 -0
  2. package/assets/css/screens.css +1097 -0
  3. package/assets/css/shell.css +258 -0
  4. package/assets/css/tokens.css +236 -0
  5. package/components/AcceptDialog.vue +17 -18
  6. package/components/AccessCard/AvailableStats.vue +24 -16
  7. package/components/AccessCardAddForm.vue +42 -42
  8. package/components/AccessCardAssignToUnitForm.vue +13 -21
  9. package/components/AccessCardDeleteDialog.vue +41 -39
  10. package/components/AccessCardDetailsDialog.vue +125 -79
  11. package/components/AccessCardPreviewDialog.vue +58 -84
  12. package/components/AccessCardQrTagging.vue +60 -63
  13. package/components/AccessCardReplaceForm.vue +27 -23
  14. package/components/AccessManagement.vue +86 -85
  15. package/components/AddEqupmentForm.vue +18 -22
  16. package/components/AddPassKeyToVisitor.vue +95 -73
  17. package/components/AppButton.vue +62 -0
  18. package/components/AppCard.vue +20 -0
  19. package/components/AppDateField.vue +49 -0
  20. package/components/AppField.vue +76 -0
  21. package/components/AppKpiTile.vue +65 -0
  22. package/components/AppSegmented.vue +38 -0
  23. package/components/AppSelect.vue +247 -0
  24. package/components/AreaChecklistHistoryLogs.vue +51 -44
  25. package/components/AreaChecklistHistoryMain.vue +14 -7
  26. package/components/AreaFormDialog.vue +71 -59
  27. package/components/AreaMain.vue +73 -107
  28. package/components/AttachmentsViewer.vue +84 -22
  29. package/components/AttendanceCheckInOutDialog.vue +85 -88
  30. package/components/AttendanceDetailsDialog.vue +57 -63
  31. package/components/AttendanceMain.vue +4 -3
  32. package/components/AttendanceMapSearchDialog.vue +42 -44
  33. package/components/AttendanceSettingsDialog.vue +79 -63
  34. package/components/Avatar/Main.vue +54 -2
  35. package/components/BillingMain.vue +26 -30
  36. package/components/BtnUploadFile.vue +53 -11
  37. package/components/BuildingForm.vue +34 -27
  38. package/components/BuildingManagement/buildings.vue +39 -39
  39. package/components/BuildingManagement/units.vue +26 -42
  40. package/components/BuildingUnitFormAdd.vue +20 -23
  41. package/components/BuildingUnitFormEdit.vue +8 -4
  42. package/components/BulletinBoardForm.vue +19 -24
  43. package/components/BulletinBoardManagement.vue +49 -57
  44. package/components/BulletinBoardView.vue +9 -6
  45. package/components/BulletinExpirationChip.vue +13 -4
  46. package/components/Button/Close.vue +2 -2
  47. package/components/CameraForm.vue +21 -22
  48. package/components/CameraMain.vue +50 -49
  49. package/components/Card/DeleteConfirmation.vue +24 -22
  50. package/components/Card/MemberInfoSummary.vue +28 -3
  51. package/components/Card/Toggle.vue +15 -3
  52. package/components/CardToolbar.vue +37 -0
  53. package/components/Carousel.vue +56 -36
  54. package/components/Chat/Bubbles.vue +43 -14
  55. package/components/Chat/Information.vue +49 -32
  56. package/components/Chat/ListCard.vue +38 -9
  57. package/components/Chat/Message.vue +41 -15
  58. package/components/Chat/Navigation.vue +27 -15
  59. package/components/Chat/SkeletonLoader.vue +9 -2
  60. package/components/ChecklistItemRow.vue +25 -1
  61. package/components/CleaningScheduleMain.vue +44 -49
  62. package/components/ClientDetailForm.vue +81 -56
  63. package/components/ClientMain.vue +92 -82
  64. package/components/ConfirmDialog.vue +14 -8
  65. package/components/Container/Standard.vue +9 -7
  66. package/components/CreateSubsciptionPlan.vue +148 -89
  67. package/components/DashboardEmptyState.vue +48 -3
  68. package/components/DashboardMain.vue +924 -676
  69. package/components/DashboardPanel.vue +84 -21
  70. package/components/DeliveryCompany.vue +34 -20
  71. package/components/Dialog/DeleteConfirmation.vue +20 -21
  72. package/components/Dialog/ReplaceAutofillPrompt.vue +19 -26
  73. package/components/Dialog/ReusablePrompt.vue +30 -29
  74. package/components/Dialog/UpdateMoreAction.vue +62 -59
  75. package/components/DocumentForm.vue +14 -21
  76. package/components/DocumentManagement.vue +162 -108
  77. package/components/DocumentViewer.vue +29 -12
  78. package/components/DrawImage.vue +29 -21
  79. package/components/EntryPass/QrTemplatePreview.vue +27 -16
  80. package/components/EntryPassInformation.vue +40 -10
  81. package/components/EntryPassMain.vue +49 -27
  82. package/components/EquipmentItemMain.vue +84 -97
  83. package/components/EquipmentManagementMain.vue +81 -103
  84. package/components/Facility/BookingSetup.vue +43 -52
  85. package/components/Feedback/Form.vue +26 -12
  86. package/components/FeedbackMain.vue +35 -49
  87. package/components/FileInput.vue +75 -58
  88. package/components/FileInputWithList.vue +59 -12
  89. package/components/FormDialog.vue +60 -21
  90. package/components/HidAccessLogDashboard.vue +512 -490
  91. package/components/HidCameraCaptureDialog.vue +216 -0
  92. package/components/HidEnabledGate.vue +67 -9
  93. package/components/HidIntercomManagement.vue +1322 -653
  94. package/components/HidQrCodeConfiguration.vue +1030 -258
  95. package/components/HidReaderForm.vue +142 -59
  96. package/components/HidReaderManagement.vue +211 -323
  97. package/components/HidServiceSettingsPanel.vue +44 -13
  98. package/components/HidUserEnrollment.vue +1185 -412
  99. package/components/HygieneUpdateMoreAction.vue +44 -44
  100. package/components/IncidentReport/Authorities.vue +30 -18
  101. package/components/IncidentReport/IncidentInformation.vue +10 -10
  102. package/components/IncidentReport/IncidentInformationDownload.vue +3 -4
  103. package/components/IncidentReport/affectedEntities.vue +41 -20
  104. package/components/Input/Date.vue +3 -0
  105. package/components/Input/DatePicker.vue +1 -2
  106. package/components/Input/DateTimePicker.vue +1 -1
  107. package/components/Input/File.vue +55 -10
  108. package/components/Input/FileV2.vue +39 -6
  109. package/components/Input/InputPhoneNumberV2.vue +2 -1
  110. package/components/Input/ListGroupSelection.vue +56 -8
  111. package/components/Input/NRICNumber.vue +1 -0
  112. package/components/Input/NewDate.vue +1 -0
  113. package/components/Input/Number.vue +1 -0
  114. package/components/Input/Password.vue +1 -0
  115. package/components/Input/PhoneNumber.vue +21 -1
  116. package/components/Input/VehicleNumber.vue +1 -0
  117. package/components/InputLabel.vue +2 -2
  118. package/components/InvitationClientForm.vue +21 -19
  119. package/components/InvitationForm.vue +15 -21
  120. package/components/InvitationMain.vue +46 -61
  121. package/components/Layout/Header.vue +263 -36
  122. package/components/Layout/NavigationDrawer.vue +300 -30
  123. package/components/ListItem.vue +33 -3
  124. package/components/LocalPagination.vue +27 -3
  125. package/components/ManageChecklistMain.vue +136 -86
  126. package/components/MemberInformation.vue +32 -16
  127. package/components/MemberMain.vue +113 -93
  128. package/components/MyAttendanceMain.vue +3 -5
  129. package/components/NavigationItem.vue +83 -3
  130. package/components/Nfc/NFCPatrolReportMain.vue +47 -33
  131. package/components/Nfc/NFCPatrolRouteForm.vue +18 -20
  132. package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
  133. package/components/Nfc/NFCPatrolSettings.vue +14 -4
  134. package/components/Nfc/NFCTagForm.vue +22 -8
  135. package/components/Nfc/NFCTagMain.vue +41 -43
  136. package/components/Nfc/PatrolReport/DailyReportTab.vue +8 -1
  137. package/components/Nfc/PatrolReport/MonthlyReportTab.vue +8 -1
  138. package/components/Nfc/PatrolReport/MonthlyReportTable.vue +5 -3
  139. package/components/Nfc/PatrolReport/PatrolActivityTable.vue +26 -6
  140. package/components/Nfc/PatrolReport/PatrolReportTab.vue +8 -0
  141. package/components/Nfc/PatrolReport/PatrolRouteSummary.vue +41 -9
  142. package/components/Nfc/PatrolReport/RemarksDialog.vue +39 -9
  143. package/components/Nfc/PatrolReport/ReportEmptyState.vue +29 -9
  144. package/components/Nfc/PatrolReport/ReportFilters.vue +19 -10
  145. package/components/Nfc/PatrolReport/ReportLocationHeader.vue +32 -6
  146. package/components/Nfc/PatrolReport/SummaryReportTable.vue +28 -6
  147. package/components/NotificationSettings.vue +373 -0
  148. package/components/NumberSettingField.vue +9 -2
  149. package/components/OnlineFormConfigurationForm.vue +725 -237
  150. package/components/OnlineFormFill.vue +213 -106
  151. package/components/OnlineFormRenderPreview.vue +25 -30
  152. package/components/OnlineFormsConfiguration.vue +113 -109
  153. package/components/OrgViewDialog.vue +34 -16
  154. package/components/OvernightParkingAvailability.vue +49 -26
  155. package/components/OvernightParkingManagement.vue +78 -18
  156. package/components/PageHeader.vue +43 -0
  157. package/components/PassInformation.vue +109 -53
  158. package/components/PasswordConfirmation.vue +44 -26
  159. package/components/PeopleForm.vue +41 -20
  160. package/components/PhotoUpload.vue +110 -48
  161. package/components/PlaceholderComponent.vue +35 -19
  162. package/components/PlanActionsMenu.vue +12 -2
  163. package/components/PlanStatusBadge.vue +7 -8
  164. package/components/PlateNumberDisplay.vue +9 -6
  165. package/components/QrTemplate/PrintDialog.vue +66 -43
  166. package/components/RolePermissionFormCreate.vue +104 -79
  167. package/components/RolePermissionFormPreviewUpdate.vue +111 -109
  168. package/components/RolePermissionMain.vue +183 -81
  169. package/components/ScanVisitorQRCode.vue +99 -35
  170. package/components/ScheduleAreaMain.vue +34 -59
  171. package/components/ScheduleTaskForm.vue +61 -42
  172. package/components/ScheduleTaskMain.vue +56 -69
  173. package/components/SearchVehicleNumberUser.vue +39 -37
  174. package/components/ServiceProviderFormCreate.vue +29 -20
  175. package/components/ServiceProviderInvitationPrompt.vue +155 -0
  176. package/components/ServiceProviderMain.vue +601 -432
  177. package/components/Signature.vue +40 -34
  178. package/components/SignaturePad.vue +5 -29
  179. package/components/SiteSettings.vue +41 -11
  180. package/components/SiteSettingsWorkOrder.vue +61 -30
  181. package/components/SlideCardGroup.vue +35 -11
  182. package/components/Snackbar.vue +36 -3
  183. package/components/SpecificAttr.vue +18 -8
  184. package/components/StatusChip.vue +156 -0
  185. package/components/StockCard.vue +17 -12
  186. package/components/SubscriptionPlanTable.vue +207 -140
  187. package/components/SwitchContext.vue +201 -9
  188. package/components/TableHygiene.vue +36 -46
  189. package/components/TableList.vue +10 -10
  190. package/components/TableListSecondary.vue +105 -54
  191. package/components/TableMain.vue +251 -78
  192. package/components/TableV3.vue +61 -70
  193. package/components/TiptapEditor.vue +33 -40
  194. package/components/TiptapEditorQr.vue +5 -2
  195. package/components/Tooltip/Info.vue +69 -24
  196. package/components/UnitMain.vue +40 -60
  197. package/components/UnitPersonCard.vue +42 -13
  198. package/components/VehicleAddSelection.vue +48 -32
  199. package/components/VehicleForm.vue +112 -129
  200. package/components/VehicleManagement.vue +26 -12
  201. package/components/VehicleUpdateMoreAction.vue +59 -46
  202. package/components/VisitorDataFromScannedQRCodeForm.vue +90 -65
  203. package/components/VisitorForm.vue +774 -210
  204. package/components/VisitorFormSelection.vue +40 -37
  205. package/components/VisitorManagement.vue +239 -82
  206. package/components/VisitorPassKeyQRScanner.vue +65 -18
  207. package/components/VisitorSearchResultSummary.vue +60 -16
  208. package/components/VisitorSocketPopUp.vue +22 -14
  209. package/components/VisitorsReportPreview.vue +44 -29
  210. package/components/WorkOrder/Create.vue +26 -17
  211. package/components/WorkOrder/ListView.vue +38 -7
  212. package/components/WorkOrder/Main.vue +62 -59
  213. package/composables/useFeedback.ts +3 -3
  214. package/composables/useHidAmico.ts +269 -0
  215. package/composables/useHidNavigation.ts +2 -9
  216. package/composables/useMember.ts +3 -3
  217. package/composables/useNFCPatrolDailyReport.ts +13 -0
  218. package/composables/useNFCPatrolMonthlyReport.ts +22 -56
  219. package/composables/useNFCPatrolReport.ts +13 -0
  220. package/composables/useNotificationPreference.ts +87 -0
  221. package/composables/useOnlineFormPages.ts +2 -0
  222. package/composables/useRole.ts +22 -0
  223. package/composables/useServiceProvider.ts +3 -3
  224. package/composables/useServiceProviderInvitation.ts +145 -0
  225. package/composables/useSipWebPhone.ts +336 -0
  226. package/composables/useSubscriptionPlan.ts +62 -0
  227. package/composables/useThemePreference.ts +63 -0
  228. package/composables/useVisitor.ts +6 -1
  229. package/composables/useWebUsb.ts +127 -37
  230. package/composables/useWorkOrder.ts +17 -3
  231. package/error.vue +30 -6
  232. package/nuxt.config.ts +36 -0
  233. package/package.json +25 -4
  234. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +2 -2
  235. package/pages/[org]/[site]/access-mgmt/{identity-mapping → hid-cards}/index.vue +2 -2
  236. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  237. package/pages/notification-settings.vue +19 -0
  238. package/pages/payment-method-linked.vue +34 -10
  239. package/pages/require-customer.vue +48 -10
  240. package/pages/require-organization-membership.vue +32 -8
  241. package/pages/unauthorized.vue +29 -4
  242. package/plugins/secure-member.client.ts +21 -56
  243. package/plugins/vuetify.ts +69 -2
  244. package/types/nfc-patrol-report.ts +3 -2
  245. package/types/site.d.ts +71 -0
  246. package/types/subscription.ts +18 -11
  247. package/utils/breadcrumb.test.ts +108 -0
  248. package/utils/breadcrumb.ts +82 -0
  249. package/utils/dashboard.test.ts +118 -0
  250. package/utils/dashboard.ts +53 -0
  251. package/utils/data.test.ts +95 -0
  252. package/utils/data.ts +40 -2
  253. package/utils/hid-discover.test.ts +102 -0
  254. package/utils/list-page.test.ts +60 -0
  255. package/utils/list-page.ts +38 -0
  256. package/utils/materialSymbols.ts +363 -0
  257. package/utils/nav-sections.test.ts +190 -0
  258. package/utils/nav-sections.ts +151 -0
  259. package/utils/nfc-patrol-report.test.ts +130 -0
  260. package/utils/nfc-patrol-report.ts +101 -0
  261. package/utils/role.test.ts +69 -0
  262. package/utils/role.ts +10 -0
  263. package/utils/route-name.test.ts +85 -0
  264. package/utils/route-name.ts +24 -0
  265. package/utils/status.test.ts +110 -0
  266. package/utils/status.ts +135 -0
  267. package/utils/table-card.test.ts +84 -0
  268. package/utils/theme-aa-ledger.ts +148 -0
  269. package/utils/theme.test.ts +485 -7
  270. package/utils/theme.ts +295 -110
  271. package/.changeset/README.md +0 -8
  272. package/.changeset/config.json +0 -11
  273. package/.editorconfig +0 -12
  274. package/.github/workflows/main.yml +0 -17
  275. package/.github/workflows/publish-staging.yml +0 -47
  276. package/.github/workflows/publish.yml +0 -39
  277. package/PUBLISHING.md +0 -269
  278. package/components/HidIdentityMapping.vue +0 -975
  279. package/components/ScheduleTaskAreaFormDialog.vue +0 -144
  280. package/components/ScheduleTaskAreaUpdateMoreAction.vue +0 -109
  281. package/components/ScheduleTastTicketMain.vue +0 -182
  282. package/components/TableWithButton.vue +0 -94
  283. package/test/visitor-socket.test.mjs +0 -36
@@ -0,0 +1,1097 @@
1
+ /**
2
+ * PHASE 4 - THE MODULE-SCREEN PATTERNS.
3
+ *
4
+ * Phase 1 wrote the tokens, Phase 2 the shell, Phase 3 the table card. What is
5
+ * left on a module screen is everything AROUND that table: the filter bar over
6
+ * it, the tabs that switch it, the dialogs it opens, the plain cards and the
7
+ * settings accordion. Those patterns are drawn once, here, and the screens opt
8
+ * in by class name - so a screen's diff is class names and chips, not a block
9
+ * of CSS repeated twenty times.
10
+ *
11
+ * OPT-IN, NOT GLOBAL. Every rule below is under a class this layer's own
12
+ * components add to their own markup. Nothing here selects a bare Vuetify class
13
+ * on its own, so no application page is repainted by a stylesheet it never
14
+ * asked for - the eleven apps get these surfaces through the components they
15
+ * already mount, and their own markup is Phases 6 and 7.
16
+ *
17
+ * VISUAL ONLY. No rule here hides, moves, reorders, disables or reveals a
18
+ * control: everything is colour, type, radius, border, shadow and spacing.
19
+ */
20
+
21
+ /* ------------------------------------------------------------------ */
22
+ /* 1. The filter bar. */
23
+ /* ------------------------------------------------------------------ */
24
+
25
+ /**
26
+ * The row of filters that sits over a table. On these screens it is a
27
+ * `v-card flat` with a fixed 60px height and `ga-5`, which on a phone puts
28
+ * three 300px fields on one 60px line and clips them. This is the same fields
29
+ * in the same order, on the design's surface, wrapping instead of clipping.
30
+ */
31
+ .filter-bar {
32
+ display: flex;
33
+ flex-wrap: wrap;
34
+ align-items: center;
35
+ gap: var(--grid-gap);
36
+ width: 100%;
37
+ padding: 10px var(--cellpad-x);
38
+ background: transparent;
39
+ font-family: var(--font-sans);
40
+ }
41
+
42
+ /* 40px, 10px radius, 13.5px - the design's input, whatever Vuetify's density
43
+ would otherwise make of it. `.filter-field` is the same input on its own,
44
+ for the screens whose one search box sits in a toolbar rather than a row. */
45
+ .filter-bar .v-field,
46
+ .filter-field .v-field {
47
+ border-radius: var(--r-inner);
48
+ font-size: var(--fs-cell);
49
+ font-family: var(--font-sans);
50
+ }
51
+
52
+ .filter-bar .v-field__input,
53
+ .filter-field .v-field__input {
54
+ min-height: var(--input-h);
55
+ font-size: var(--fs-cell);
56
+ }
57
+
58
+ .filter-bar .v-label,
59
+ .filter-field .v-label,
60
+ .filter-bar .v-field__input input::placeholder,
61
+ .filter-field .v-field__input input::placeholder {
62
+ font-size: var(--fs-cell);
63
+ font-family: var(--font-sans);
64
+ }
65
+
66
+ /* A field is at least readable-wide and then shares what is left. Below 600
67
+ there is no room for two, so they stack - the same rule `.filter-row` uses. */
68
+ .filter-bar > * {
69
+ flex: 1 1 200px;
70
+ min-width: 0;
71
+ max-width: 100%;
72
+ }
73
+
74
+ .filter-bar > .filter-bar__fit {
75
+ flex: 0 0 auto;
76
+ }
77
+
78
+ @media (max-width: 599.98px) {
79
+ .filter-bar {
80
+ flex-direction: column;
81
+ align-items: stretch;
82
+ }
83
+
84
+ .filter-bar > * {
85
+ flex: 1 1 auto;
86
+ width: 100%;
87
+ }
88
+ }
89
+
90
+ /* ------------------------------------------------------------------ */
91
+ /* 2. Tabs. */
92
+ /* ------------------------------------------------------------------ */
93
+
94
+ /**
95
+ * 13px, the active one at 800 in `--accent-text` over a 2px accent underline.
96
+ * Vuetify's own tab label is uppercase 14px/500 with wide tracking and it puts
97
+ * `text-uppercase` on it as a utility, so these need the same weight of hand
98
+ * the nav items needed in Phase 2.
99
+ */
100
+ .screen-tabs .v-tab {
101
+ font-family: var(--font-sans);
102
+ font-size: var(--fs-nav) !important;
103
+ font-weight: var(--fw-cell) !important;
104
+ letter-spacing: 0 !important;
105
+ text-transform: none !important;
106
+ color: var(--muted);
107
+ min-width: 0;
108
+ }
109
+
110
+ .screen-tabs .v-tab.v-tab--selected {
111
+ font-weight: var(--fw-table-head) !important;
112
+ color: var(--accent-text);
113
+ }
114
+
115
+ .screen-tabs .v-tab .v-tab__slider {
116
+ height: 2px;
117
+ background: var(--accent);
118
+ }
119
+
120
+ /* ------------------------------------------------------------------ */
121
+ /* 3. Cards, section headings and empty states. */
122
+ /* ------------------------------------------------------------------ */
123
+
124
+ /** The card contract, for the surfaces that are not the table card. */
125
+ .screen-card {
126
+ font-family: var(--font-sans);
127
+ background: var(--card);
128
+ border: 1px solid var(--border);
129
+ border-radius: var(--r-card);
130
+ box-shadow: var(--shadow);
131
+ }
132
+
133
+ .screen-title {
134
+ font-family: var(--font-sans);
135
+ font-size: var(--fs-h1);
136
+ font-weight: var(--fw-h1);
137
+ letter-spacing: var(--ls-h1);
138
+ color: var(--text);
139
+ }
140
+
141
+ .screen-card-title {
142
+ font-family: var(--font-sans);
143
+ font-size: var(--fs-card-title) !important;
144
+ font-weight: var(--fw-card-title) !important;
145
+ letter-spacing: 0 !important;
146
+ color: var(--text);
147
+ }
148
+
149
+ .screen-empty {
150
+ display: flex;
151
+ flex-direction: column;
152
+ align-items: center;
153
+ justify-content: center;
154
+ gap: 8px;
155
+ padding: 52px 16px;
156
+ font-family: var(--font-sans);
157
+ font-size: var(--fs-cell);
158
+ color: var(--muted);
159
+ text-align: center;
160
+ }
161
+
162
+ .screen-empty .v-icon {
163
+ color: var(--muted);
164
+ }
165
+
166
+ /**
167
+ * The one-line explanation under a setting's name. It was
168
+ * `text-grey-darken-1` - a fixed Vuetify grey, i.e. the same dark grey on the
169
+ * dark page - so the sentence that explains a switch was the hardest thing on
170
+ * the screen to read. `--muted` is the design's own, and it is a token, so it
171
+ * follows the theme.
172
+ */
173
+ .screen-hint {
174
+ font-family: var(--font-sans);
175
+ color: var(--muted);
176
+ }
177
+
178
+ /**
179
+ * `fontgray` is a legacy application-side Vuetify colour - a fixed `#7e7e7e`
180
+ * that is the SAME grey in both themes, so it neither darkens for the light
181
+ * page nor lightens for the dark one. Measured on the glass it reads 4.06:1 on
182
+ * a light card and 4.10:1 on a dark card: below AA on both, on labels that
183
+ * appear on 35 files across the layer and eleven applications.
184
+ *
185
+ * `--muted` is the design's own secondary ink and is a token, so it follows
186
+ * the theme. Redirected here, once, rather than renamed at 35 call sites - the
187
+ * layer's stylesheet ships to every application, so the class keeps working.
188
+ *
189
+ * THE READING THIS COMMENT USED TO CLAIM IS NO LONGER TRUE, and the correction
190
+ * matters because it inverts the justification above.
191
+ *
192
+ * It said "4.77:1 light, 4.71:1 dark". That was measured when `--muted` was
193
+ * `#6b7079` - one of the six values darkened on 2026-08-13 to clear AA. The
194
+ * owner REVERSED that on 2026-08-14 in favour of 100% design fidelity, and
195
+ * `--muted` went back to the handoff's `#8b8f98`. Re-measured on the current
196
+ * token, alpha composited:
197
+ *
198
+ * --muted on a card 3.24:1 (the old #7e7e7e read 4.06:1)
199
+ * --muted on the page 2.99:1 - below even the 3.0 large-text floor
200
+ *
201
+ * So on the LIGHT theme this redirect is now a DOWNGRADE for the ~5 files that
202
+ * still use the class, not the improvement the original comment describes. It
203
+ * is left in place deliberately: `#7e7e7e` was a fixed grey that ignored the
204
+ * theme entirely and read 4.10:1 on dark where `--muted` reads 4.93:1, so
205
+ * reverting would trade a light-theme gain for a dark-theme loss. Which way
206
+ * this should go is a PALETTE decision for the owner, not a per-class one -
207
+ * see `utils/theme-aa-ledger.ts`, which pins every one of these readings.
208
+ */
209
+ .text-fontgray {
210
+ color: var(--muted) !important;
211
+ }
212
+
213
+ /* ------------------------------------------------------------------ */
214
+ /* 4. Dialogs. */
215
+ /* ------------------------------------------------------------------ */
216
+
217
+ /**
218
+ * THE ONE RULE HERE THAT IS NOT OPT-IN, AND WHY.
219
+ *
220
+ * The product opens its dialogs from more than sixty different form and dialog
221
+ * components, not from the twenty-three module screens. Giving the design's
222
+ * 16px corner to a modal by editing sixty files would be sixty chances to
223
+ * break a dialog for a corner radius. So the SHAPE is stated once, for any
224
+ * card that Vuetify has put inside an overlay - shape and shadow only. It
225
+ * repaints no colour, moves no control and changes no type, and it is the same
226
+ * `.v-overlay__content > .v-card` selector Phase 3 already had to out-specify.
227
+ */
228
+ .v-overlay__content > .v-card,
229
+ .v-overlay__content > form > .v-card {
230
+ border-radius: var(--r-modal) !important;
231
+ box-shadow: var(--shadow-modal) !important;
232
+ }
233
+
234
+ /**
235
+ * `.v-overlay__content > .v-card` is MORE SPECIFIC than a class on the card
236
+ * (Phase 3 measured a dialog stuck at its factory 4px radius because of it),
237
+ * so the radius has to be `!important` to be reached at all.
238
+ *
239
+ * The body is what scrolls, not the whole card: a modal whose card scrolls
240
+ * takes its Cancel/Submit footer off screen with it. `.screen-modal__body` is
241
+ * only applied where a screen's dialog already has a distinct body element.
242
+ */
243
+ .screen-modal {
244
+ font-family: var(--font-sans);
245
+ border-radius: var(--r-modal) !important;
246
+ box-shadow: var(--shadow-modal) !important;
247
+ background: var(--card) !important;
248
+ max-height: calc(100vh - 32px);
249
+ }
250
+
251
+ .screen-modal .v-card-title {
252
+ font-family: var(--font-sans);
253
+ font-size: var(--fs-card-title) !important;
254
+ font-weight: var(--fw-card-title) !important;
255
+ letter-spacing: 0 !important;
256
+ color: var(--text);
257
+ }
258
+
259
+ .screen-modal__body {
260
+ overflow-y: auto;
261
+ font-size: var(--fs-cell);
262
+ }
263
+
264
+ /**
265
+ * ...and the same size again, because inside a real dialog the rule above
266
+ * never reached the glass. Vuetify scopes
267
+ *
268
+ * .v-dialog > .v-overlay__content > .v-card > .v-card-text { font-size: inherit }
269
+ *
270
+ * which is a FOUR-class chain against our one, so a `.screen-modal__body` in a
271
+ * `v-dialog` inherited the root 16px instead of the design's 13.5px --fs-cell.
272
+ * Measured on `AccessCardDeleteDialog`: 16px/400 where --fs-cell is 13.5px.
273
+ * It is invisible in a harness that mounts the card WITHOUT its dialog, which
274
+ * is how it survived. Naming the same chain plus the card's own class wins it
275
+ * back on specificity alone - no `!important`, same technique as the button
276
+ * height fix.
277
+ */
278
+ .v-dialog > .v-overlay__content > .v-card.screen-modal > .screen-modal__body {
279
+ font-size: var(--fs-cell);
280
+ }
281
+
282
+ /**
283
+ * The design's modal footer, literal from the prototype's Create Role dialog:
284
+ *
285
+ * <div style="display:flex;gap:10px;padding:16px 22px;
286
+ * border-top:1px solid var(--border)">
287
+ * <button style="flex:1; ...ghost">Cancel</button>
288
+ * <button style="flex:1; ...primary">Submit</button>
289
+ *
290
+ * Two EQUAL-WIDTH buttons on a bordered strip, ghost first. Stated once
291
+ * because the product opens confirm dialogs from a couple of dozen components
292
+ * and each of them was drawing its own footer by hand.
293
+ */
294
+ .screen-modal__footer {
295
+ display: flex;
296
+ gap: 10px;
297
+ padding: 16px 22px;
298
+ border-top: 1px solid var(--border);
299
+ }
300
+
301
+ .screen-modal__footer > .v-btn,
302
+ .screen-modal__footer > .app-btn {
303
+ flex: 1;
304
+ }
305
+
306
+ /**
307
+ * The modal's close control, literal from the same Create Role dialog:
308
+ *
309
+ * <button style="display:flex;align-items:center;justify-content:center;
310
+ * width:32px;height:32px;border:none;border-radius:8px;
311
+ * background:transparent;color:var(--muted);"
312
+ * style-hover="background:var(--hover)">close</button>
313
+ *
314
+ * Borderless and 32px square - NOT `.screen-icon-btn`, which is the bordered
315
+ * 40px square the design draws in a toolbar. The glyph is 19px.
316
+ *
317
+ * MEASURED, 2026-08-15: the size never reached the glass. Vuetify's own
318
+ *
319
+ * .v-btn--size-default.v-btn--icon { width: calc(var(--v-btn-height) + 12px);
320
+ * height: same }
321
+ *
322
+ * is TWO classes against this one, so every `.screen-modal__close` in the
323
+ * package drew at 36 + 12 = **48px**, with Vuetify's `padding: 0 16px` on top -
324
+ * half again the design's square, on all thirteen components that use it
325
+ * (`AreaFormDialog`, `Carousel`, `ConfirmDialog`, `DocumentViewer`, `DrawImage`,
326
+ * `EntryPass/QrTemplatePreview`, `EquipmentItemMain`, `EquipmentManagementMain`,
327
+ * `FileInput`, `ManageChecklistMain`, `PhotoUpload`, `ServiceProviderMain`,
328
+ * `Signature`). The `border-radius: 8px` DID win, which is what hid it: the
329
+ * control looked right and simply sat in a box a third too big.
330
+ *
331
+ * Naming `.v-btn` alongside it makes this (0,2,0) and the later sheet wins -
332
+ * the same specificity fix `.v-btn.screen-btn-primary` needed, no `!important`.
333
+ */
334
+ .v-btn.screen-modal__close {
335
+ width: 32px;
336
+ height: 32px;
337
+ min-width: 32px;
338
+ padding: 0;
339
+ border-radius: 8px;
340
+ background: transparent;
341
+ color: var(--muted);
342
+ }
343
+
344
+ .screen-modal__close:hover {
345
+ background: var(--hover);
346
+ }
347
+
348
+ .screen-modal__close .v-icon {
349
+ font-size: 19px;
350
+ }
351
+
352
+ /**
353
+ * A DIALOG'S OWN HEADER/FOOTER BAR - OPT-IN, DELIBERATELY.
354
+ *
355
+ * Some dialogs draw their title row and their Cancel/Submit row as a
356
+ * `v-toolbar`. Vuetify paints that from its own `surface`, which in dark mode
357
+ * is a visible grey slab sitting on the card. The obvious fix - one blanket
358
+ * `.v-overlay__content .v-toolbar` rule - was measured and REJECTED: sixty-odd
359
+ * dialog components draw such a bar, and several set `color="black"` or
360
+ * `color="grey-darken-3"` ON PURPOSE. A blanket rule would flatten those too,
361
+ * unrendered and unverified.
362
+ *
363
+ * So this is a class a converted dialog opts into. Background and text only -
364
+ * it moves no control and changes no button.
365
+ */
366
+ .screen-dialog-bar.v-toolbar {
367
+ background: var(--card) !important;
368
+ color: var(--text) !important;
369
+ }
370
+
371
+ /**
372
+ * The design's two buttons, for the dialogs and toolbars that draw their own.
373
+ *
374
+ * `.v-btn` is named on every selector below for SPECIFICITY, not decoration.
375
+ * Vuetify sizes its button from `--v-btn-height` (36px, set on
376
+ * `.v-btn--size-default`) and applies it at `.v-btn.v-btn--density-default` -
377
+ * two classes, which outrank a bare `.screen-btn-*`. So `height: var(--btn-h)`
378
+ * never reached the glass and EVERY one of these buttons drew at 36px against
379
+ * the design's 40. `min-width` lost the same argument at 64px.
380
+ *
381
+ * Beaten by specificity, not `!important` - the same way the central font
382
+ * re-point beat Vuetify's own font declarations. And the fix FEEDS
383
+ * `--v-btn-height` rather than overwriting `height`, so Vuetify's density
384
+ * arithmetic (including the icon button's `+12px`) keeps working off our number.
385
+ */
386
+ .v-btn.screen-btn-primary {
387
+ --v-btn-height: var(--btn-h);
388
+ min-width: 0;
389
+ border-radius: var(--r-inner);
390
+ padding: 0 16px;
391
+ font-family: var(--font-sans);
392
+ font-size: var(--fs-btn);
393
+ font-weight: var(--fw-btn-strong);
394
+ letter-spacing: 0;
395
+ text-transform: none;
396
+ background: var(--accent-strong);
397
+ color: var(--on-accent-strong);
398
+ }
399
+
400
+ /*
401
+ * `min-width: 0` above is what lets a button shrink to its label, and it is
402
+ * one class more specific than Vuetify's `.v-btn--block { min-width: 100% }` -
403
+ * so it silently CANCELLED `block`. Measured on the ten create/edit forms:
404
+ * a `block` Cancel sat 80.8px wide inside a 672px half-column instead of
405
+ * filling it, on every width and both themes. Restoring it needs the block
406
+ * class named, so this reaches only the buttons that asked to be full width.
407
+ */
408
+ .v-btn.v-btn--block.screen-btn-primary,
409
+ .v-btn.v-btn--block.screen-btn-ghost {
410
+ min-width: 100%;
411
+ }
412
+
413
+ /* Vuetify paints `variant="tonal"` with an overlay layer ABOVE the button's own
414
+ background, so without this the accent fill shows through a wash. */
415
+ .screen-btn-primary .v-btn__overlay,
416
+ .screen-btn-ghost .v-btn__overlay {
417
+ opacity: 0;
418
+ }
419
+
420
+ /*
421
+ * DISABLED. The two rules above set `background` and `color` unconditionally,
422
+ * and they out-specify the treatment Vuetify would otherwise give a disabled
423
+ * button - so a Submit that a `v-form` has genuinely gated OFF was painting
424
+ * the full accent fill at full opacity, identical to a live one. Measured on
425
+ * `OnlineFormFill` with its required field emptied: enabled and disabled came
426
+ * back byte-identical (`rgb(75, 116, 196)` / `rgb(255, 255, 255)` /
427
+ * `opacity: 1`) on BOTH themes. The gate held; nothing showed it.
428
+ *
429
+ * 0.5 is the design's own disabled step - `.app-btn:disabled` in
430
+ * `primitives.css` uses it - so the two button families dim by the same amount
431
+ * rather than inventing a second number here.
432
+ *
433
+ * Both the attribute and Vuetify's class are named: `:disabled` is what a
434
+ * plain `<button>` carries, `.v-btn--disabled` is what Vuetify adds, and a
435
+ * rule that matched only one of them would go quiet the moment the other path
436
+ * was used.
437
+ *
438
+ * `.screen-icon-btn` was MISSED when this rule was written, and it has exactly
439
+ * the same shape: `.v-btn.screen-icon-btn` is (0,2,0) and sets `background:
440
+ * var(--card)` and `color: var(--text2)`, so it out-specifies Vuetify the same
441
+ * way. Measured 2026-08-16 on `AreaMain`: enabled and disabled came back
442
+ * identical on background, colour, border, opacity and filter, on BOTH themes.
443
+ * Its `:disabled` call sites are the promo-code tier rows in the admin app,
444
+ * where the last remaining tier's delete button is gated off and looked live.
445
+ *
446
+ * `.screen-modal__close` and `.screen-modal` were checked at the same time and
447
+ * are NOT affected - both already read as distinct when disabled, because the
448
+ * close button's background is `transparent` and a disabled `v-card` dims its
449
+ * children rather than itself. Neither is listed here.
450
+ */
451
+ .v-btn.screen-btn-primary:disabled,
452
+ .v-btn.screen-btn-primary.v-btn--disabled,
453
+ .v-btn.screen-btn-ghost:disabled,
454
+ .v-btn.screen-btn-ghost.v-btn--disabled,
455
+ .v-btn.screen-icon-btn:disabled,
456
+ .v-btn.screen-icon-btn.v-btn--disabled {
457
+ opacity: 0.5;
458
+ }
459
+
460
+ /**
461
+ * The icon-only actions next to a primary button (download template, import,
462
+ * export). The design draws an icon button as a bordered square the height of
463
+ * the input next to it - the same button Phase 2 gave the top bar - not a
464
+ * tinted pill.
465
+ */
466
+ .v-btn.screen-icon-btn {
467
+ --v-btn-height: var(--btn-h);
468
+ height: var(--btn-h);
469
+ width: var(--btn-h);
470
+ min-width: var(--btn-h);
471
+ padding: 0;
472
+ border-radius: var(--r-inner);
473
+ border: 1px solid var(--border);
474
+ background: var(--card);
475
+ color: var(--text2);
476
+ }
477
+
478
+ .screen-icon-btn .v-btn__overlay {
479
+ opacity: 0;
480
+ }
481
+
482
+ .v-btn.screen-btn-ghost {
483
+ --v-btn-height: var(--btn-h);
484
+ min-width: 0;
485
+ border-radius: var(--r-inner);
486
+ padding: 0 16px;
487
+ font-family: var(--font-sans);
488
+ font-size: var(--fs-btn);
489
+ font-weight: var(--fw-btn);
490
+ letter-spacing: 0;
491
+ text-transform: none;
492
+ color: var(--text2);
493
+ /*
494
+ * THE EDGE IS THE GHOST. The handoff draws this button as "40px, 1px border,
495
+ * card fill" - the border is what separates it from the page, since the fill
496
+ * is the same `--card` as the surface behind it.
497
+ *
498
+ * `border-color` alone does not draw anything. It only tinted an edge that
499
+ * Vuetify's `variant` had already decided the width of: `outlined` gives
500
+ * `thin`, `flat` gives 0. So the same class rendered a bordered button at 48
501
+ * call sites and a borderless one at 13 - measured 1px vs 0px, both themes -
502
+ * and which you got depended on a `variant` a caller had no reason to think
503
+ * was load-bearing. Naming the width here makes the class draw the design's
504
+ * button whichever variant it is paired with.
505
+ */
506
+ border-width: 1px;
507
+ border-style: solid;
508
+ border-color: var(--border);
509
+ background: var(--card);
510
+ }
511
+
512
+ /* ------------------------------------------------------------------ */
513
+ /* 5. The settings accordion. */
514
+ /* ------------------------------------------------------------------ */
515
+
516
+ /**
517
+ * The design draws settings as a stack of bordered sections with one open at a
518
+ * time. The "one at a time" half is BEHAVIOUR and is left exactly as the panel
519
+ * has it (`multiple`, on the site settings screen); this is the drawn half:
520
+ * the border, the radius, the title type and the open section's surface.
521
+ */
522
+ .screen-accordion .v-expansion-panel {
523
+ font-family: var(--font-sans);
524
+ background: var(--card);
525
+ border: 1px solid var(--border);
526
+ border-radius: var(--r-card) !important;
527
+ box-shadow: none !important;
528
+ margin-bottom: 10px;
529
+ }
530
+
531
+ .screen-accordion .v-expansion-panel::after {
532
+ display: none;
533
+ }
534
+
535
+ .screen-accordion .v-expansion-panel-title {
536
+ font-family: var(--font-sans);
537
+ font-size: var(--fs-card-title);
538
+ font-weight: var(--fw-card-title);
539
+ color: var(--text);
540
+ min-height: 56px;
541
+ }
542
+
543
+ .screen-accordion .v-expansion-panel-title--active {
544
+ color: var(--text);
545
+ border-bottom: 1px solid var(--border);
546
+ }
547
+
548
+ .screen-accordion .v-expansion-panel-text {
549
+ font-size: var(--fs-cell);
550
+ color: var(--text2);
551
+ }
552
+
553
+ .screen-accordion .v-expansion-panel-title__overlay {
554
+ background: transparent;
555
+ }
556
+
557
+ /* ------------------------------------------------------------------ */
558
+ /* 6. The table card (moved here from TableMain, unchanged). */
559
+ /* ------------------------------------------------------------------ */
560
+
561
+ /**
562
+ * Phase 3 drew this inside `TableMain`'s own scoped block. It is moved here,
563
+ * rule for rule, because it is not TableMain's alone any more: `BillingMain`
564
+ * draws the same card from its own copy of that markup, and a second copy of
565
+ * the CSS is how the two drift apart. `TableMain` still puts the same classes
566
+ * on the same elements - only the stylesheet moved, so nothing it renders
567
+ * changes.
568
+ */
569
+
570
+ /* ------------------------------------------------------------------ */
571
+ /* The card. */
572
+ /* ------------------------------------------------------------------ */
573
+
574
+ /**
575
+ * THE WIDE-TABLE BLOWOUT.
576
+ *
577
+ * The card sits in a `v-col`, i.e. a flex item, and a flex item's automatic
578
+ * minimum size is its content's minimum - so a table with 8 columns set a
579
+ * ~686px floor the column could not shrink below, and the PAGE scrolled
580
+ * sideways at 360 and 768 instead of the table doing it. `min-width: 0` lets
581
+ * it shrink; the table then scrolls INSIDE the card, which is how the design
582
+ * draws it - the row is deliberately not restacked into key/value blocks,
583
+ * because a column carries meaning by its position. Same decision as
584
+ * `.app-table-scroll` in `primitives.css`.
585
+ *
586
+ * It is on BOTH of this component's own flex boxes - its root `v-row` (which
587
+ * is itself an item of the CALLER's row) and the column holding the card -
588
+ * because the content's minimum walks all the way up the chain, and stopping
589
+ * it at the inner column alone left the page still scrolling sideways.
590
+ */
591
+ .table-card__shrink {
592
+ min-width: 0;
593
+ }
594
+
595
+ .table-card .v-table__wrapper {
596
+ overflow-x: auto;
597
+ }
598
+
599
+ .table-card {
600
+ font-family: var(--font-sans);
601
+ background: var(--card);
602
+ border: 1px solid var(--border);
603
+ border-radius: var(--r-card);
604
+ box-shadow: var(--shadow);
605
+ overflow: hidden;
606
+ /*
607
+ * `.table-card__loading` below is `position: absolute`, and nothing in the
608
+ * chain above it - not `.app-card`, not `.table-card` - established a
609
+ * containing block, so the bar resolved against the VIEWPORT: measured
610
+ * 1440x2 pinned to `top: 0` of the window while its card sat at `y: 61.5`.
611
+ * Every `TableMain` screen in all 11 apps drew its busy bar across the top
612
+ * of the browser instead of the top of the card. This is the anchor it
613
+ * always needed. Safe by construction: the card is already
614
+ * `overflow: hidden`, so nothing inside it could have been anchored to an
615
+ * ancestor OUTSIDE it and still be visible.
616
+ */
617
+ position: relative;
618
+ }
619
+
620
+ /**
621
+ * The toolbar was `color="grey-lighten-4"` - a literal Vuetify grey, which is
622
+ * a near-white bar sitting on a #1c1e24 card in dark mode. It is the card's
623
+ * own surface now, with the design's rule under it.
624
+ */
625
+ .table-card__toolbar {
626
+ border-bottom: 1px solid var(--border);
627
+ }
628
+
629
+ /**
630
+ * The callers' filter row (`#extension`). It was a `v-toolbar` extension, i.e.
631
+ * a fixed-height second bar; it is now a plain row under the toolbar with the
632
+ * same rule under it, so it can size to whatever the caller puts in it.
633
+ */
634
+ .table-card__extension {
635
+ border-bottom: 1px solid var(--border);
636
+ }
637
+
638
+ .table-card__loading {
639
+ position: absolute;
640
+ z-index: 1;
641
+ }
642
+
643
+ .table-card__range {
644
+ font-size: 12px;
645
+ font-weight: 600;
646
+ color: var(--muted);
647
+ font-variant-numeric: tabular-nums;
648
+ }
649
+
650
+ /* ------------------------------------------------------------------ */
651
+ /* The header row and the rows under it. */
652
+ /* ------------------------------------------------------------------ */
653
+
654
+ /**
655
+ * `padding: 12px 20px`, not 10px - read off the prototype's own header row,
656
+ * which is `padding:12px 20px;font-size:11px;font-weight:800;letter-spacing:
657
+ * .07em;text-transform:uppercase;color:var(--muted);background:var(--thead)`.
658
+ */
659
+ .table-card thead th {
660
+ background: var(--thead) !important;
661
+ /*
662
+ * `--text2`, not the prototype's `--muted`. `--muted` on the header band
663
+ * measures 3.10:1 in light (3.24:1 where the band sits on a plain card) -
664
+ * below AA, and reported independently by two apps' QA runs. `--text2` is
665
+ * the design's SECONDARY text token: 9.22:1 light / 9.57:1 dark on the same
666
+ * band, while still sitting clearly below the primary ink the data rows use
667
+ * (16.52:1 / 13.77:1), so the header keeps reading as secondary to the data.
668
+ * The uppercase/800/11px treatment below is unchanged.
669
+ */
670
+ color: var(--text2) !important;
671
+ font-size: var(--fs-table-head) !important;
672
+ font-weight: var(--fw-table-head) !important;
673
+ letter-spacing: var(--ls-table-head);
674
+ text-transform: uppercase;
675
+ padding: 12px var(--cellpad-x) !important;
676
+ height: auto !important;
677
+ border-bottom: 1px solid var(--border) !important;
678
+ white-space: nowrap;
679
+ }
680
+
681
+ /*
682
+ * `--thead` is intentionally translucent. A fixed header, however, sits above
683
+ * scrolling rows, so using the token by itself lets the first row show through
684
+ * the header. Layer the same tint over the card surface to keep the rendered
685
+ * colour unchanged while making the sticky header fully opaque.
686
+ */
687
+ .table-card .v-table--fixed-header thead th {
688
+ background:
689
+ linear-gradient(var(--thead), var(--thead)),
690
+ var(--card) !important;
691
+ }
692
+
693
+ /**
694
+ * ROW HEIGHT IS THE LINE BOX, NOT THE PADDING.
695
+ *
696
+ * The padding was already the design's `15px 20px`, and the type was already
697
+ * `13.5px / 600` - measured, both correct. The row was still too tall, because
698
+ * Vuetify puts `line-height: 1.5` on a cell: a 20.25px line box around 13.5px
699
+ * type, which makes a text-only row 51.25px.
700
+ *
701
+ * The prototype sets NO line-height at all, so its rows draw at the font's own
702
+ * normal leading - so this sets `normal` rather than a number, which is the
703
+ * same thing said out loud. Measured: a text-only row is then exactly 49.0px,
704
+ * the design's figure, and a row carrying a chip or the design's 26-28px avatar
705
+ * grows past it on its own - which is where the design's taller rows come from.
706
+ * Height follows content, exactly as the prototype does it, and is pinned to
707
+ * nothing.
708
+ */
709
+ .table-card tbody td {
710
+ font-size: var(--fs-cell);
711
+ font-weight: var(--fw-cell);
712
+ color: var(--text);
713
+ padding: var(--cellpad) var(--cellpad-x) !important;
714
+ height: auto !important;
715
+ /* `!important` for the same reason the padding needs it: Vuetify's own rule
716
+ is five levels of descendant, which no two-class selector can outrank. */
717
+ line-height: normal !important;
718
+ font-variant-numeric: tabular-nums;
719
+ }
720
+
721
+ /**
722
+ * The design bolds the name column. Every table in the prototype that leads
723
+ * with a name draws it `font-weight:700` against the row's own 600 - it is what
724
+ * makes a row scannable, and it was missing from every table here.
725
+ *
726
+ * `:first-child` rather than a prop: the caller's own `headers` decide what the
727
+ * first column is, so no caller changes and no new API. A cell whose CONTENT
728
+ * sets its own weight - a chip, a link, an avatar block - still wins, because
729
+ * this only sets what the cell's children inherit.
730
+ */
731
+ .table-card tbody td:first-child {
732
+ font-weight: 700;
733
+ }
734
+
735
+ /**
736
+ * THE DESIGN'S FIXED COLUMN PROPORTIONS - opt-in, because they cannot be
737
+ * derived. The prototype lays every table out as a CSS grid with a per-table
738
+ * `grid-template-columns` (`0.9fr 1.5fr 1.7fr 0.8fr 1fr 1.1fr` on Feedbacks,
739
+ * `2fr 1fr 1fr` on Units, and so on). Those proportions are a design decision
740
+ * per screen; nothing in a caller's `headers` array says what they should be,
741
+ * so a shared component cannot invent them.
742
+ *
743
+ * What `TableMain` can do is honour them when a caller states them: a `weight`
744
+ * on a header becomes that column's share, and only then does the table switch
745
+ * to `table-layout: fixed` so the widths are obeyed rather than treated as
746
+ * hints. A caller that sets no weights gets exactly today's automatic layout.
747
+ */
748
+ .table-card .table-card__fixed table {
749
+ table-layout: fixed;
750
+ }
751
+
752
+ /* Fixed layout means a long value can no longer widen its column, so it has to
753
+ be told what to do instead of overflowing the cell. */
754
+ .table-card .table-card__fixed tbody td {
755
+ overflow: hidden;
756
+ text-overflow: ellipsis;
757
+ }
758
+
759
+ .table-card tbody tr:hover > td {
760
+ background: var(--hover);
761
+ }
762
+
763
+ /**
764
+ * The grouped list (`TableHygiene`) draws its own rows instead of a <table>,
765
+ * so it cannot inherit the two rules above. Its group header takes the header
766
+ * row's surface and its rows the card's - replacing the literal
767
+ * `grey-lighten-4` and `white` that painted the whole list near-white on a
768
+ * dark card.
769
+ */
770
+ .table-card__group {
771
+ background: var(--thead);
772
+ }
773
+
774
+ .table-card__row {
775
+ background: var(--card);
776
+ }
777
+
778
+ /* ------------------------------------------------------------------ */
779
+ /* Empty state, in-card search, buttons and in-card tabs. */
780
+ /* ------------------------------------------------------------------ */
781
+
782
+ .table-card__empty {
783
+ display: flex;
784
+ flex-direction: column;
785
+ align-items: center;
786
+ justify-content: center;
787
+ gap: 8px;
788
+ padding: 52px 16px;
789
+ font-size: var(--fs-cell);
790
+ color: var(--muted);
791
+ }
792
+
793
+ .table-card__empty .v-icon {
794
+ color: var(--muted);
795
+ }
796
+
797
+ /* The in-card variant: 34px, 10px radius, as drawn. */
798
+ .table-card__search .v-field {
799
+ border-radius: var(--r-inner);
800
+ font-size: var(--fs-cell);
801
+ }
802
+
803
+ .table-card__primary {
804
+ height: var(--btn-h);
805
+ border-radius: var(--r-inner);
806
+ padding: 0 16px;
807
+ font-size: var(--fs-btn);
808
+ font-weight: var(--fw-btn-strong);
809
+ letter-spacing: 0;
810
+ background: var(--accent-strong);
811
+ color: var(--on-accent-strong);
812
+ }
813
+
814
+ .table-card__ghost {
815
+ height: var(--btn-h);
816
+ border-radius: var(--r-inner);
817
+ padding: 0 16px;
818
+ font-size: var(--fs-btn);
819
+ font-weight: var(--fw-btn);
820
+ letter-spacing: 0;
821
+ color: var(--text2);
822
+ /*
823
+ * Same reason as `.screen-btn-ghost` (#1200): `border-color` alone draws
824
+ * nothing, it only tints an edge whose width the Vuetify `variant` decided.
825
+ * Measured: paired with `outlined` this rule gives 1px, paired with `flat`
826
+ * it gives 0px - and the fill is `--card`, the same colour as the surface
827
+ * behind it, so a borderless copy has no boundary at all.
828
+ *
829
+ * The one call site today (`TableMain.vue`, Scan QR Code) does use
830
+ * `outlined`, so nothing is visibly broken right now. This is the latent
831
+ * half of the same defect: the next caller who writes `flat` gets an
832
+ * invisible button and no error. Naming the width closes it.
833
+ */
834
+ border-width: 1px;
835
+ border-style: solid;
836
+ border-color: var(--border);
837
+ background: var(--card);
838
+ }
839
+
840
+ /**
841
+ * Tabs that a screen puts in this card's extension slot: 13px, the active one
842
+ * at 800 in `--accent-text` over a 2px accent underline. Tabs elsewhere in the
843
+ * product are their own screens' and are restyled with them.
844
+ */
845
+ .table-card .v-tab {
846
+ font-size: var(--fs-nav);
847
+ font-weight: var(--fw-cell);
848
+ letter-spacing: 0;
849
+ text-transform: none;
850
+ color: var(--muted);
851
+ min-width: 0;
852
+ }
853
+
854
+ .table-card .v-tab.v-tab--selected {
855
+ font-weight: var(--fw-table-head);
856
+ color: var(--accent-text);
857
+ }
858
+
859
+ .table-card .v-tab .v-tab__slider {
860
+ height: 2px;
861
+ background: var(--accent);
862
+ }
863
+
864
+ /* ------------------------------------------------------------------ */
865
+ /* Toast (`components/Snackbar.vue`, 163 caller files). */
866
+ /* */
867
+ /* The handoff draws no toast at all - it is one of the gaps. So this */
868
+ /* is deliberately NOT an invented surface: it is the design's own */
869
+ /* button type scale, inner radius and menu shadow applied to */
870
+ /* Vuetify's snackbar. Vuetify's own default is a 4px radius, a */
871
+ /* Material elevation and Roboto at 14px, none of which are ours. */
872
+ /* */
873
+ /* The FILL is the `color` prop, defaulted in the component to */
874
+ /* `primary-button` - never `primary`, which is a foreground token. */
875
+ /* ------------------------------------------------------------------ */
876
+ .app-toast .v-snackbar__wrapper {
877
+ border-radius: var(--r-inner);
878
+ box-shadow: var(--shadow-menu);
879
+ font-family: var(--font-sans);
880
+ }
881
+
882
+ .app-toast .v-snackbar__content {
883
+ font-size: var(--fs-btn);
884
+ font-weight: 600;
885
+ letter-spacing: 0;
886
+ }
887
+
888
+ /* Vuetify's text button is uppercase with wide tracking; the design has no
889
+ uppercase control anywhere. Named on `.v-btn` so it outweighs the default. */
890
+ .v-btn.app-toast__close {
891
+ border-radius: var(--r-inner-sm);
892
+ font-family: var(--font-sans);
893
+ font-size: var(--fs-btn);
894
+ font-weight: var(--fw-btn);
895
+ letter-spacing: 0;
896
+ text-transform: none;
897
+ }
898
+
899
+ /* ------------------------------------------------------------------ */
900
+ /* The "Back" link that sits above a page title. */
901
+ /* `SpecificAttr.vue` (17 caller files) and `Container/Standard.vue`. */
902
+ /* */
903
+ /* DESIGN GAP, STATED: the handoff draws no back control anywhere, so */
904
+ /* its SIZE is not a value the design gives. What the design DOES give */
905
+ /* is the rule for a link - `iService365 Redesign.dc.html:26`, */
906
+ /* `a{color:var(--accent-text);text-decoration:underline}` - so the */
907
+ /* colour and the underline are literal, and the underline is kept for */
908
+ /* that reason rather than dropped for looking dated. The size is */
909
+ /* taken from the only standalone inline link the handoff draws, the */
910
+ /* panel's "View All" at line 166: 12.5px / 700. */
911
+ /* */
912
+ /* It replaces `text-subtitle-1 font-weight-medium` - Vuetify's */
913
+ /* 16px/400 Roboto scale, which is neither the product font nor a size */
914
+ /* that appears anywhere in the design. */
915
+ /* ------------------------------------------------------------------ */
916
+ .screen-back {
917
+ display: inline-block;
918
+ font-family: var(--font-sans);
919
+ font-size: var(--fs-breadcrumb);
920
+ font-weight: var(--fw-btn);
921
+ letter-spacing: 0;
922
+ line-height: 1.4;
923
+ color: var(--accent-text);
924
+ text-decoration: underline;
925
+ cursor: pointer;
926
+ }
927
+
928
+ .screen-back:hover {
929
+ color: var(--accent);
930
+ }
931
+
932
+ /* ------------------------------------------------------------------ */
933
+ /* 9. The row kebab and the dropdown it opens. */
934
+ /* */
935
+ /* Both halves are LITERAL, and both were previously left as Vuetify */
936
+ /* draws them - `Dialog/UpdateMoreAction.vue` says so in a comment, */
937
+ /* because a menu teleports out of the measured container and could */
938
+ /* not be render-verified at the time. It can now (force-shot). */
939
+ /* */
940
+ /* The kebab: `iService365 Redesign.dc.html:494`, drawn identically at */
941
+ /* 494, 547, 625 and 660 - a 30px bordered square, 8px radius, */
942
+ /* transparent fill, `--muted` ink, 17px glyph, `--hover` on hover. */
943
+ /* 8px is not one of the radius tokens (the set is 7/9/10/12/14/16), */
944
+ /* so it is written literally rather than snapped to a near token. */
945
+ /* */
946
+ /* The panel: `iService365 Redesign.dc.html:79-90`, the profile */
947
+ /* dropdown - the only dropdown the handoff draws. 240px, `--card` on */
948
+ /* a 1px `--border`, 14px radius, `--shadow-menu`, 8px padding; rows */
949
+ /* are 13px/700 `--text2` at 10px 12px with a 10px radius, and a */
950
+ /* destructive row is `--err` ink with an `--err-bg` hover (line 89), */
951
+ /* which is ink and not a filled button. */
952
+ /* ------------------------------------------------------------------ */
953
+ .v-btn.screen-kebab {
954
+ --v-btn-height: 30px;
955
+ height: 30px;
956
+ width: 30px;
957
+ min-width: 30px;
958
+ padding: 0;
959
+ border: 1px solid var(--border);
960
+ border-radius: 8px;
961
+ background: transparent;
962
+ color: var(--muted);
963
+ }
964
+
965
+ .screen-kebab .v-btn__overlay {
966
+ opacity: 0;
967
+ }
968
+
969
+ .screen-kebab:hover {
970
+ background: var(--hover);
971
+ }
972
+
973
+ .screen-kebab .v-icon {
974
+ font-size: 17px;
975
+ }
976
+
977
+ .v-overlay__content > .v-list.screen-menu {
978
+ width: 240px;
979
+ padding: 8px;
980
+ border: 1px solid var(--border);
981
+ border-radius: var(--r-menu);
982
+ background: var(--card);
983
+ box-shadow: var(--shadow-menu);
984
+ }
985
+
986
+ /* Four classes, to beat Vuetify's own `.v-list .v-list-item--density-*` */
987
+ /* block on specificity rather than on load order - a three-class rule ties */
988
+ /* with it and loses, which is how the drawer's 10px gap never applied. */
989
+ .v-overlay__content > .v-list.screen-menu > .screen-menu__item.v-list-item {
990
+ min-height: 0;
991
+ padding: 10px 12px;
992
+ /* MEASURED, not assumed: without `!important` this read back 0px. Vuetify
993
+ puts the `rounded-0` UTILITY on every `v-list-item` inside a list with no
994
+ `rounded` prop, and utilities carry `!important`, so a four-class rule
995
+ still loses to it. Same trap `NavigationItem.vue` hit in batch Q. */
996
+ border-radius: var(--r-inner) !important;
997
+ color: var(--text2);
998
+ }
999
+
1000
+ .screen-menu__item .v-list-item-title {
1001
+ font-family: var(--font-sans);
1002
+ font-size: var(--fs-nav);
1003
+ font-weight: var(--fw-btn);
1004
+ letter-spacing: 0;
1005
+ line-height: 1.3;
1006
+ }
1007
+
1008
+ .screen-menu__item .v-list-item__prepend > .v-icon {
1009
+ font-size: 18px;
1010
+ color: var(--muted);
1011
+ opacity: 1;
1012
+ }
1013
+
1014
+ /* The design's row gap is 10px; Vuetify's spacer is wider, and the selector */
1015
+ /* below matches its own four-class shape for the same reason as above. */
1016
+ .screen-menu__item .v-list-item__prepend > .v-icon ~ .v-list-item__spacer {
1017
+ width: 10px;
1018
+ }
1019
+
1020
+ .screen-menu__item:hover > .v-list-item__overlay {
1021
+ opacity: 0;
1022
+ }
1023
+
1024
+ .screen-menu__item:hover {
1025
+ background: var(--hover);
1026
+ }
1027
+
1028
+ .v-overlay__content > .v-list.screen-menu > .screen-menu__item--danger.v-list-item {
1029
+ color: var(--err);
1030
+ }
1031
+
1032
+ .screen-menu__item--danger .v-list-item__prepend > .v-icon {
1033
+ color: var(--err);
1034
+ }
1035
+
1036
+ .screen-menu__item--danger:hover {
1037
+ background: var(--err-bg);
1038
+ }
1039
+
1040
+ /* ------------------------------------------------------------------ */
1041
+ /* SIGNATURE PAD - `SignaturePad.vue`. */
1042
+ /* */
1043
+ /* DESIGN GAP, STATED: the handoff draws no signature pad and contains */
1044
+ /* no dashed border anywhere, so everything below is the established */
1045
+ /* primitives and tokens, not a handoff literal. The one value taken */
1046
+ /* from the handoff is the caption pair at line 659 - 12.5px / 600. */
1047
+ /* */
1048
+ /* THE PAD IS PAPER AND STAYS LIGHT IN BOTH THEMES, DELIBERATELY. */
1049
+ /* The canvas ink is `#111` and `stop()` exports the drawing as a PNG */
1050
+ /* that callers render back on light surfaces and print. A `--card` */
1051
+ /* pad would be dark in the dark theme, so the signature would be */
1052
+ /* black-on-black on the pad AND the exported PNG would be a black */
1053
+ /* scrawl on transparency. The three literals here are the LIGHT theme */
1054
+ /* values of `--card`, `--border` and `--muted`, named so nobody has */
1055
+ /* to guess where they came from. */
1056
+ /* ------------------------------------------------------------------ */
1057
+ .screen-signpad {
1058
+ position: relative;
1059
+ overflow: hidden;
1060
+ /* Light `--muted` at 55% - NOT `--border`. A drop zone's dashed edge is an
1061
+ affordance and has to be seen; `--border` (rgb 232,232,225) on a white pad
1062
+ is all but invisible, which the first render showed. This lands on the same
1063
+ weight as the `#bbb` it replaces, from a token value rather than a hex. */
1064
+ border: 1.5px dashed rgba(139, 143, 152, 0.55);
1065
+ border-radius: var(--r-inner);
1066
+ background: rgb(255, 255, 255); /* light `--card` */
1067
+ }
1068
+
1069
+ .screen-signpad__canvas {
1070
+ display: block;
1071
+ width: 100%;
1072
+ height: 130px;
1073
+ cursor: crosshair;
1074
+ touch-action: none;
1075
+ }
1076
+
1077
+ .screen-signpad__hint {
1078
+ position: absolute;
1079
+ inset: 0;
1080
+ display: flex;
1081
+ align-items: center;
1082
+ justify-content: center;
1083
+ pointer-events: none;
1084
+ font-family: var(--font-sans);
1085
+ font-size: 12.5px;
1086
+ font-weight: 600;
1087
+ letter-spacing: 0;
1088
+ color: rgb(139, 143, 152); /* light `--muted` - the pad is paper */
1089
+ }
1090
+
1091
+ .screen-signpad__label {
1092
+ font-family: var(--font-sans);
1093
+ font-size: 12.5px;
1094
+ font-weight: 600;
1095
+ letter-spacing: 0;
1096
+ color: var(--muted);
1097
+ }