@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,1113 @@
1
+ /**
2
+ * PLATFORM REDESIGN - THE PAGE PRIMITIVES.
3
+ *
4
+ * Phases 1-5 tokenised what Vuetify draws. This file is the other half: the
5
+ * pieces the design draws that Vuetify has no component for, taken as literal
6
+ * CSS out of the handoff prototype
7
+ * (`design_handoff_platform_redesign/iService365 Redesign.dc.html`) rather
8
+ * than re-derived by eye. Every declaration below has a line in that file.
9
+ *
10
+ * The rule for a screen adopting these: MARKUP, not a theme override. A
11
+ * `.v-btn` restyled into a pill is still a `.v-btn` with Vuetify's ripple,
12
+ * overlay, min-width and uppercase tracking fighting it at a higher
13
+ * specificity; a `<button class="app-btn">` simply is the button in the
14
+ * design. Where Vuetify owns the behaviour (the data table, the menu, the
15
+ * dialog) it stays - only the chrome around it is replaced.
16
+ *
17
+ * Numbers, for review against the handoff:
18
+ * page title 22px/800/-0.01em · card radius 14px, 1px border, --shadow
19
+ * in-card toolbar 0 20px, tabs 14px 2px 12px, 2px accent underline
20
+ * fields 40px (34px compact), 10px radius, 13.5px/500
21
+ * primary button 40px, 10px radius, 13.5px/800 · ghost 38px, 13px/700
22
+ */
23
+
24
+ /* ------------------------------------------------------------------ */
25
+ /* The typeface. */
26
+ /* ------------------------------------------------------------------ */
27
+
28
+ /**
29
+ * Phase 1 loaded Manrope and deliberately did not apply it, so that the
30
+ * product would not sit half-migrated between two typefaces. The page
31
+ * scaffold is the migration, so the font goes on now - on Vuetify's own
32
+ * application root, which is every rendered page in all eleven apps.
33
+ *
34
+ * `.v-application` and not `body`: Vuetify sets `font-family` on that class
35
+ * itself, so a rule on `body` loses to it and the product would keep Roboto
36
+ * everywhere while the new primitives alone changed - the exact
37
+ * half-migrated look this was avoiding.
38
+ */
39
+ .v-application,
40
+ .v-overlay-container {
41
+ font-family: var(--font-sans);
42
+ }
43
+
44
+ /**
45
+ * ...and the places Vuetify hard-codes Roboto, which BEAT the rule above
46
+ * because they name the element rather than inherit into it. In
47
+ * vuetify 3.12 that is exactly 81 declarations, in three files:
48
+ *
49
+ * main.css `html` (1) and the typography utilities .text-h1 ...
50
+ * .text-overline (13), each repeated for the five responsive
51
+ * breakpoints .text-sm-* ... .text-xxl-* (65) = 79
52
+ * VBadge.css `.v-badge__badge`
53
+ * VKbd.css `.v-kbd`
54
+ *
55
+ * None of them carries `!important`, so specificity alone re-points them:
56
+ * `:root` (0,1,0) beats `html` (0,0,1), and a descendant of `.v-application`
57
+ * (0,2,0) beats a bare `.text-h1` (0,1,0). No `!important` here either -
58
+ * a screen that wants a different face must still be able to say so.
59
+ *
60
+ * This lives HERE, once, and not in each of the eleven apps: the utilities
61
+ * are Vuetify's, the token is this layer's, and a per-app copy would drift.
62
+ *
63
+ * The var() fallback keeps Vuetify's own stack if a build ever resolves a
64
+ * pre-redesign layer-common in which `--font-sans` does not exist.
65
+ */
66
+ :root {
67
+ font-family: var(--font-sans, Roboto, sans-serif);
68
+ }
69
+
70
+ .v-application
71
+ :is(
72
+ [class*="text-h"],
73
+ [class*="text-subtitle"],
74
+ [class*="text-body"],
75
+ [class*="text-button"],
76
+ [class*="text-caption"],
77
+ [class*="text-overline"],
78
+ .v-badge__badge,
79
+ .v-kbd
80
+ ):not(.material-symbols-outlined),
81
+ .v-overlay-container
82
+ :is(
83
+ [class*="text-h"],
84
+ [class*="text-subtitle"],
85
+ [class*="text-body"],
86
+ [class*="text-button"],
87
+ [class*="text-caption"],
88
+ [class*="text-overline"],
89
+ .v-badge__badge,
90
+ .v-kbd
91
+ ):not(.material-symbols-outlined) {
92
+ font-family: var(--font-sans, Roboto, sans-serif);
93
+ }
94
+
95
+ /* ------------------------------------------------------------------ */
96
+ /* The icon face. */
97
+ /* ------------------------------------------------------------------ */
98
+
99
+ /**
100
+ * The handoff draws every icon with Google's `Material Symbols Outlined`
101
+ * variable font, requested in `nuxt.config.ts` alongside Manrope. The icon
102
+ * set in `plugins/vuetify.ts` turns each `mdi-*` name into that font's
103
+ * ligature and puts this class on the element; this rule is what makes the
104
+ * ligature resolve.
105
+ *
106
+ * One class, no `!important`. `.v-icon` already gives us `font-feature-
107
+ * settings: "liga"`, `letter-spacing: normal`, `line-height: 1` and the 1em
108
+ * box, and Vuetify never sets `font-family` on it - the old family arrived
109
+ * through `.mdi:before`, which we no longer emit. Only `text-transform` has
110
+ * to be stated: `.v-btn` uppercases its contents, and an uppercased "close"
111
+ * matches no ligature, so the icon would render as the word.
112
+ */
113
+ .material-symbols-outlined {
114
+ font-family: "Material Symbols Outlined";
115
+ font-style: normal;
116
+ font-variation-settings:
117
+ "FILL" 0,
118
+ "GRAD" 0,
119
+ "opsz" 24;
120
+ text-transform: none;
121
+ white-space: nowrap;
122
+ word-wrap: normal;
123
+ direction: ltr;
124
+ -webkit-font-smoothing: antialiased;
125
+ }
126
+
127
+ /**
128
+ * The icon face, re-asserted at (0,3,0).
129
+ *
130
+ * A `<v-icon class="text-h6">` carries BOTH the icon class and a typography
131
+ * utility, so the re-point above (a descendant of `.v-application`, (0,2,0))
132
+ * would otherwise beat the single-class rule (0,1,0) and the ligature would
133
+ * fall back to Manrope - drawing the literal word "local_shipping" instead of
134
+ * a truck. The `:not()` guard above stops that here; this block stops it
135
+ * anywhere an app ships its own copy of the re-point, whatever the stylesheet
136
+ * order. Repeating the class is how the weight is raised - no `!important`,
137
+ * same precedent as the re-point itself, so a screen can still override.
138
+ */
139
+ .v-application .material-symbols-outlined.material-symbols-outlined,
140
+ .v-overlay-container .material-symbols-outlined.material-symbols-outlined {
141
+ font-family: "Material Symbols Outlined";
142
+ }
143
+
144
+ /* mdi's spin modifier, kept working now the glyph is not a `:before`.
145
+ `@keyframes mdi-spin` still comes from `@mdi/font`, which stays loaded so
146
+ that any icon name without a mapping keeps drawing its old glyph. */
147
+ .material-symbols-outlined.mdi-spin {
148
+ animation: mdi-spin 2s infinite linear;
149
+ }
150
+
151
+ /* ------------------------------------------------------------------ */
152
+ /* PageHeader - the row every screen opens with. */
153
+ /* ------------------------------------------------------------------ */
154
+
155
+ .app-page-head {
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: space-between;
159
+ gap: 16px;
160
+ margin-bottom: 18px;
161
+ flex-wrap: wrap;
162
+ }
163
+
164
+ .app-page-head__title {
165
+ margin: 0;
166
+ font-size: var(--fs-h1);
167
+ font-weight: var(--fw-h1);
168
+ letter-spacing: var(--ls-h1);
169
+ line-height: 1.25;
170
+ color: var(--text);
171
+ }
172
+
173
+ .app-page-head__sub {
174
+ display: flex;
175
+ align-items: center;
176
+ gap: 5px;
177
+ margin-top: 4px;
178
+ font-size: 12.5px;
179
+ font-weight: 600;
180
+ color: var(--muted);
181
+ }
182
+
183
+ .app-page-head__actions {
184
+ display: flex;
185
+ align-items: center;
186
+ gap: 10px;
187
+ flex-wrap: wrap;
188
+ }
189
+
190
+ /* ------------------------------------------------------------------ */
191
+ /* AppCard - the surface everything else sits on. */
192
+ /* ------------------------------------------------------------------ */
193
+
194
+ .app-card {
195
+ background: var(--card);
196
+ border: 1px solid var(--border);
197
+ border-radius: var(--r-card);
198
+ box-shadow: var(--shadow);
199
+ overflow: hidden;
200
+ }
201
+
202
+ .app-card--flush {
203
+ overflow: visible;
204
+ }
205
+
206
+ /* ------------------------------------------------------------------ */
207
+ /* CardToolbar - the in-card top row. */
208
+ /* ------------------------------------------------------------------ */
209
+
210
+ /**
211
+ * Two shapes in the handoff, one component. With tabs the row is `0 20px` and
212
+ * the tabs supply their own vertical padding, so the underline meets the
213
+ * card's rule; without tabs (a refresh button on the left) it is `10px 14px`.
214
+ */
215
+ .app-toolbar {
216
+ display: flex;
217
+ align-items: center;
218
+ /* One row at desktop widths, as drawn. On a phone the tabs and the
219
+ search/pager group drop onto their own lines rather than overflow. */
220
+ flex-wrap: wrap;
221
+ gap: 22px;
222
+ padding: 0 var(--cellpad-x);
223
+ border-bottom: 1px solid var(--border);
224
+ min-height: 48px;
225
+ }
226
+
227
+ .app-toolbar--plain {
228
+ gap: 8px;
229
+ padding: 10px 14px;
230
+ }
231
+
232
+ .app-toolbar__spacer {
233
+ flex: 1;
234
+ }
235
+
236
+ .app-toolbar__right {
237
+ display: flex;
238
+ align-items: center;
239
+ gap: 8px;
240
+ }
241
+
242
+ /* The list toolbar's own search box. `--search` alone stretches (it is written
243
+ for the filter row), which would push the pager off the end of the row. */
244
+ .app-toolbar__search.app-field--search {
245
+ flex: 0 0 auto;
246
+ width: 200px;
247
+ min-width: 0;
248
+ }
249
+
250
+ .app-toolbar__count {
251
+ font-size: 12.5px;
252
+ font-weight: 600;
253
+ color: var(--muted);
254
+ font-variant-numeric: tabular-nums;
255
+ white-space: nowrap;
256
+ }
257
+
258
+ .app-tabs {
259
+ display: flex;
260
+ align-items: center;
261
+ gap: 22px;
262
+ min-width: 0;
263
+ overflow-x: auto;
264
+ scrollbar-width: none;
265
+ }
266
+
267
+ .app-tabs::-webkit-scrollbar {
268
+ display: none;
269
+ }
270
+
271
+ .app-tab {
272
+ padding: 14px 2px 12px 2px;
273
+ border: none;
274
+ background: transparent;
275
+ border-bottom: 2px solid transparent;
276
+ font-family: inherit;
277
+ font-size: var(--fs-nav);
278
+ font-weight: 600;
279
+ color: var(--muted);
280
+ white-space: nowrap;
281
+ cursor: pointer;
282
+ /* A tab is a <button> on the screens that switch a list in place and a link
283
+ on the ones that route (Invitations), so the underline has to be ours. */
284
+ text-decoration: none;
285
+ }
286
+
287
+ .app-tab:hover {
288
+ color: var(--text2);
289
+ }
290
+
291
+ .app-tab--active {
292
+ font-weight: 800;
293
+ color: var(--accent-text);
294
+ border-bottom-color: var(--accent);
295
+ }
296
+
297
+ /* ------------------------------------------------------------------ */
298
+ /* The table, drawn as a grid. */
299
+ /* ------------------------------------------------------------------ */
300
+
301
+ .app-thead {
302
+ display: grid;
303
+ gap: 12px;
304
+ padding: 12px var(--cellpad-x);
305
+ font-size: var(--fs-table-head);
306
+ font-weight: var(--fw-table-head);
307
+ letter-spacing: var(--ls-table-head);
308
+ text-transform: uppercase;
309
+ color: var(--muted);
310
+ background: var(--thead);
311
+ }
312
+
313
+ .app-trow {
314
+ display: grid;
315
+ gap: 12px;
316
+ align-items: center;
317
+ padding: var(--cellpad) var(--cellpad-x);
318
+ border-top: 1px solid var(--border);
319
+ font-size: 13px;
320
+ font-weight: 600;
321
+ color: var(--text);
322
+ }
323
+
324
+ .app-trow:hover {
325
+ background: var(--hover);
326
+ }
327
+
328
+ .app-cell--strong {
329
+ font-weight: 700;
330
+ overflow: hidden;
331
+ text-overflow: ellipsis;
332
+ white-space: nowrap;
333
+ }
334
+
335
+ .app-cell--muted {
336
+ color: var(--text2);
337
+ overflow: hidden;
338
+ text-overflow: ellipsis;
339
+ white-space: nowrap;
340
+ }
341
+
342
+ .app-cell--num {
343
+ color: var(--text2);
344
+ font-variant-numeric: tabular-nums;
345
+ }
346
+
347
+ .app-empty {
348
+ display: flex;
349
+ flex-direction: column;
350
+ align-items: center;
351
+ gap: 8px;
352
+ padding: 52px var(--cellpad-x);
353
+ border-top: 1px solid var(--border);
354
+ font-size: 13px;
355
+ font-weight: 600;
356
+ color: var(--muted);
357
+ text-align: center;
358
+ }
359
+
360
+ /* ------------------------------------------------------------------ */
361
+ /* AppButton. */
362
+ /* ------------------------------------------------------------------ */
363
+
364
+ .app-btn {
365
+ display: inline-flex;
366
+ align-items: center;
367
+ justify-content: center;
368
+ gap: 6px;
369
+ height: var(--btn-h);
370
+ padding: 0 16px;
371
+ border: none;
372
+ border-radius: var(--r-inner);
373
+ /* `--accent` at 3.23:1 cannot carry a white label - see Phase 1. The fill,
374
+ and only the fill, uses the 18%-darker `--accent-strong`; the underline,
375
+ the links and every soft background stay on `--accent`. */
376
+ background: var(--accent-strong);
377
+ color: var(--on-accent-strong);
378
+ font-family: inherit;
379
+ font-size: var(--fs-btn);
380
+ font-weight: var(--fw-btn-strong);
381
+ line-height: 1;
382
+ white-space: nowrap;
383
+ cursor: pointer;
384
+ }
385
+
386
+ .app-btn:hover {
387
+ filter: brightness(1.06);
388
+ }
389
+
390
+ /*
391
+ * THE `type="button"` TIE, AND WHY THIS BLOCK EXISTS.
392
+ *
393
+ * Vuetify ships `[type="button"] { color: inherit }`. That is an ATTRIBUTE
394
+ * selector at (0,1,0) - an exact tie with every `.app-btn` rule above, which is
395
+ * also (0,1,0). A tie is broken by INJECTION ORDER, so in a document where
396
+ * Vuetify's stylesheet lands after this one, the background from the rules above
397
+ * still applies and the `color` does not: the button keeps its accent fill and
398
+ * takes the page ink instead of its own.
399
+ *
400
+ * `AppButton` renders `<button :type="type">` with `type` defaulting to
401
+ * "button", so EVERY variant carries the attribute the tie needs.
402
+ *
403
+ * MEASURED, 2026-08-16. Injecting `[type="button"] { color: inherit }` last, on
404
+ * a real primary in the harness:
405
+ *
406
+ * before fill rgb(75,116,196) ink rgb(255,255,255) 4.57:1
407
+ * after fill rgb(75,116,196) ink rgb(25,27,31) 3.77:1
408
+ *
409
+ * `rgb(25,27,31)` is `--text` on the light theme - the button had stopped
410
+ * choosing its own ink at all.
411
+ *
412
+ * Naming the ELEMENT makes each of these (0,1,1), which beats an attribute
413
+ * selector WHATEVER the order. No `!important`: a screen that wants a different
414
+ * label colour must still be able to say so.
415
+ *
416
+ * This is an ADDITIONAL block rather than an edit to the rules above, and
417
+ * deliberately so: `AppButton` renders a `NuxtLink` when `to` is given, which is
418
+ * an `<a>` with no `type` attribute. Putting `button` on the original selectors
419
+ * would have taken the colour away from that path entirely. The values below are
420
+ * copied verbatim from the rules above; if one changes, change both.
421
+ */
422
+ button.app-btn {
423
+ color: var(--on-accent-strong);
424
+ }
425
+
426
+ button.app-btn--ghost {
427
+ color: var(--text2);
428
+ }
429
+
430
+ button.app-btn--icon {
431
+ color: var(--text2);
432
+ }
433
+
434
+ button.app-btn--row {
435
+ color: var(--muted);
436
+ }
437
+
438
+ button.app-btn--ok {
439
+ color: var(--on-ok);
440
+ }
441
+
442
+ button.app-btn--err {
443
+ color: var(--err);
444
+ }
445
+
446
+ .app-btn:disabled {
447
+ opacity: 0.5;
448
+ cursor: default;
449
+ filter: none;
450
+ }
451
+
452
+ /* The design draws the ghost at the SAME size as the primary it stands next to
453
+ - Bulk Upload beside Add Unit, Scan QR Code beside Add Visitor are both
454
+ `height:40px; padding:0 16px; font-size:13.5px`, and only the fill, the
455
+ border and the weight differ. 38px / 0 14px / 13px was a smaller button than
456
+ the handoff asks for, and the 2px mismatch shows on every screen that draws
457
+ the pair. Height, padding and type therefore come from `.app-btn`. */
458
+ .app-btn--ghost {
459
+ border: 1px solid var(--border);
460
+ background: var(--card);
461
+ color: var(--text2);
462
+ font-weight: var(--fw-btn);
463
+ }
464
+
465
+ .app-btn--ghost:hover {
466
+ background: var(--hover);
467
+ filter: none;
468
+ }
469
+
470
+ .app-btn--icon {
471
+ width: 32px;
472
+ height: 32px;
473
+ padding: 0;
474
+ border: none;
475
+ border-radius: 8px;
476
+ background: transparent;
477
+ color: var(--text2);
478
+ }
479
+
480
+ .app-btn--icon:hover {
481
+ background: var(--hover);
482
+ filter: none;
483
+ }
484
+
485
+ /* The 30px bordered row action - the `more_vert` at the end of a member row. */
486
+ .app-btn--row {
487
+ width: 30px;
488
+ height: 30px;
489
+ padding: 0;
490
+ border: 1px solid var(--border);
491
+ border-radius: 8px;
492
+ background: transparent;
493
+ color: var(--muted);
494
+ }
495
+
496
+ .app-btn--row:hover {
497
+ background: var(--hover);
498
+ filter: none;
499
+ }
500
+
501
+ /* ------------------------------------------------------------------ */
502
+ /* The check-in / check-out pair. */
503
+ /* ------------------------------------------------------------------ */
504
+
505
+ /**
506
+ * NEW DESIGN, approved by the owner on 2026-08-14. The handoff draws no
507
+ * check-in and no check-out button, so there is nothing to match here; the
508
+ * gatehouse screens had been carrying Vuetify's `color="success"` and a
509
+ * hardcoded `color="red"` instead.
510
+ *
511
+ * Both are `.app-btn` first - the same 40px box, the same 10px radius, the
512
+ * same 13.5px/800 - so only the fill, the ink and the edge differ and the pair
513
+ * sits level with every other button in the product. The 16px icon at an 8px
514
+ * gap is the only metric either adds.
515
+ *
516
+ * CHECK IN is SOLID. It is the affirmative action a guard hits hundreds of
517
+ * times a shift and should be the heaviest thing on its row.
518
+ *
519
+ * CHECK OUT is TONAL, and that is measured rather than preferred: white on
520
+ * `--err` reads 2.99:1 in the dark theme, which is why batch C built a solid
521
+ * danger button, measured it and reverted it. `--err-bg` fill + `--err` ink +
522
+ * a 24%/28% `--err` edge is the construction the status chips already use, it
523
+ * passes in both themes, and it keeps a routine action from reading as a
524
+ * destructive one.
525
+ */
526
+ .app-btn--ok,
527
+ .app-btn--err {
528
+ gap: 8px;
529
+ }
530
+
531
+ /* The label is `--on-ok`, not a literal white - see tokens.css. White on the
532
+ DARK theme's `--ok` measures 2.14:1; `--on-ok` is the on-fill pair the theme
533
+ already carries, so the green stays exactly the green the design names. */
534
+ .app-btn--ok {
535
+ border: 1px solid var(--ok);
536
+ background: var(--ok);
537
+ color: var(--on-ok);
538
+ }
539
+
540
+ .app-btn--err {
541
+ border: 1px solid var(--err-border);
542
+ background: var(--err-bg);
543
+ color: var(--err);
544
+ }
545
+
546
+ /* A tint cannot be hovered with `brightness()` - above 1 it fades in the light
547
+ theme, which is the wrong direction. Layering the theme's own `--hover` over
548
+ the fill darkens in light and lightens in dark, with no new token. */
549
+ .app-btn--err:hover {
550
+ background: linear-gradient(var(--hover), var(--hover)), var(--err-bg);
551
+ filter: none;
552
+ }
553
+
554
+ /* ------------------------------------------------------------------ */
555
+ /* AppField / AppSelect / AppDateField. */
556
+ /* ------------------------------------------------------------------ */
557
+
558
+ /**
559
+ * Flat and compact, with the label OUTSIDE the box as a small muted word -
560
+ * not Vuetify's outlined field with a label that floats through the border.
561
+ * A native `<input>`/`<select>`, so the browser keeps the keyboard, the date
562
+ * picker and the accessibility for free.
563
+ */
564
+ .app-field {
565
+ display: flex;
566
+ align-items: center;
567
+ gap: 8px;
568
+ height: var(--input-h);
569
+ padding: 0 12px;
570
+ border: 1px solid var(--border);
571
+ border-radius: var(--r-inner);
572
+ background: var(--card);
573
+ font-size: var(--fs-nav);
574
+ font-weight: 600;
575
+ color: var(--text);
576
+ min-width: 0;
577
+ }
578
+
579
+ .app-field--compact {
580
+ height: var(--input-h-compact);
581
+ }
582
+
583
+ .app-field:focus-within {
584
+ border-color: var(--accent);
585
+ }
586
+
587
+ .app-field__label {
588
+ font-size: 12px;
589
+ font-weight: 600;
590
+ color: var(--muted);
591
+ white-space: nowrap;
592
+ }
593
+
594
+ .app-field__icon {
595
+ color: var(--muted);
596
+ flex: 0 0 auto;
597
+ }
598
+
599
+ .app-field__input {
600
+ flex: 1;
601
+ min-width: 0;
602
+ height: 100%;
603
+ border: none;
604
+ outline: none;
605
+ background: transparent;
606
+ font-family: inherit;
607
+ font-size: var(--fs-cell);
608
+ font-weight: 500;
609
+ color: var(--text);
610
+ }
611
+
612
+ .app-field__input::placeholder {
613
+ color: var(--muted);
614
+ opacity: 1;
615
+ }
616
+
617
+ /*
618
+ * A native `<select>`'s option list is painted by the BROWSER, not by the page.
619
+ * The control above sets `color`, and an `<option>` inherits it - but it gets
620
+ * no background of its own, so the browser paints its own popup behind the
621
+ * theme's ink. In dark mode that popup is white on Windows/Chrome, and the
622
+ * theme's near-white ink on it measures 1.14:1: the list reads as blank, with
623
+ * only the highlighted row legible. QA reported exactly that on two apps.
624
+ *
625
+ * Giving the option the card's own fill and ink is what makes the open list
626
+ * belong to the theme. `--card`/`--text` and not a literal, so it follows the
627
+ * `v-theme--*` subtree the select is actually in - a dialog that forces a
628
+ * theme included. Declared once here rather than per component, so every
629
+ * native select this layer renders is covered, in every consuming app.
630
+ *
631
+ * The FRAME around the list is browser chrome and is not ours to paint; a
632
+ * light strip may remain around the options in some browsers.
633
+ */
634
+ select option {
635
+ background-color: var(--card);
636
+ color: var(--text);
637
+ }
638
+
639
+ /* The `trailing` slot's control - same muted weight as the leading icon, so a
640
+ reveal toggle reads as field chrome rather than as a second action. */
641
+ .app-field__trailing {
642
+ display: grid;
643
+ place-items: center;
644
+ flex: 0 0 auto;
645
+ border: none;
646
+ background: transparent;
647
+ padding: 0;
648
+ color: var(--muted);
649
+ cursor: pointer;
650
+ }
651
+
652
+ .app-field__trailing:hover {
653
+ color: var(--text2);
654
+ }
655
+
656
+ /*
657
+ * AppSelect wears the same field chrome as a button and opens a themed
658
+ * popover. Multi values are `StatusChip pill tone-neutral`, the soft pill the
659
+ * design already uses and the one whose contrast is measured.
660
+ */
661
+ .app-select {
662
+ cursor: pointer;
663
+ overflow: hidden;
664
+ }
665
+
666
+ .app-select--disabled {
667
+ cursor: default;
668
+ opacity: 0.55;
669
+ }
670
+
671
+ .app-select--single {
672
+ min-width: 180px;
673
+ font-family: inherit;
674
+ text-align: left;
675
+ }
676
+
677
+ .app-select__single-value {
678
+ flex: 1;
679
+ min-width: 0;
680
+ overflow: hidden;
681
+ font-size: var(--fs-nav);
682
+ font-weight: 600;
683
+ color: var(--text);
684
+ text-overflow: ellipsis;
685
+ white-space: nowrap;
686
+ }
687
+
688
+ /*
689
+ * The SAME step for the typed field, which never had one. `AppField` and
690
+ * `AppDateField` both take a `disabled` prop and pass it to the native
691
+ * `<input>` - and `AppField`'s prop doc says disabled "also greys the box" -
692
+ * but `.app-field` pins `background: var(--card)` and `.app-field__input` pins
693
+ * `color: var(--text)`, which overrides the greying a browser gives a disabled
694
+ * input for free. Measured 2026-08-16 on `AccessManagement`: enabled and
695
+ * disabled came back identical on background, colour, border, opacity and
696
+ * filter, on BOTH themes. The field was genuinely dead and read as live.
697
+ *
698
+ * Keyed off the input's own `:disabled` rather than a new modifier class, so
699
+ * it covers `AppField` and `AppDateField` without two templates having to
700
+ * remember to add it. 0.55 is
701
+ * `.app-select--disabled`'s number, not a second one.
702
+ */
703
+ .app-field:has(.app-field__input:disabled) {
704
+ cursor: default;
705
+ opacity: 0.55;
706
+ }
707
+
708
+ .app-select__values {
709
+ display: flex;
710
+ align-items: center;
711
+ gap: 6px;
712
+ flex: 1;
713
+ min-width: 0;
714
+ overflow: hidden;
715
+ }
716
+
717
+ .app-select__placeholder {
718
+ font-size: var(--fs-cell);
719
+ font-weight: 500;
720
+ color: var(--muted);
721
+ white-space: nowrap;
722
+ }
723
+
724
+ /* The x that removes one pill, and the one that empties the field. */
725
+ .app-select__x,
726
+ .app-select__clear {
727
+ display: inline-flex;
728
+ align-items: center;
729
+ color: inherit;
730
+ opacity: 0.7;
731
+ background: none;
732
+ border: none;
733
+ padding: 0;
734
+ cursor: pointer;
735
+ }
736
+
737
+ .app-select__x:hover,
738
+ .app-select__clear:hover {
739
+ opacity: 1;
740
+ }
741
+
742
+ .app-select__clear {
743
+ color: var(--muted);
744
+ flex: 0 0 auto;
745
+ }
746
+
747
+ .app-select__menu {
748
+ min-width: 200px;
749
+ max-height: 280px;
750
+ overflow-y: auto;
751
+ padding: 6px;
752
+ border: 1px solid var(--border);
753
+ border-radius: var(--r-inner-lg);
754
+ background: var(--card);
755
+ box-shadow: var(--shadow-menu);
756
+ }
757
+
758
+ .app-select__option {
759
+ display: flex;
760
+ align-items: center;
761
+ gap: 8px;
762
+ width: 100%;
763
+ padding: 7px 8px;
764
+ border: none;
765
+ border-radius: var(--r-inner-sm);
766
+ background: none;
767
+ font-family: var(--font-sans);
768
+ font-size: var(--fs-cell);
769
+ font-weight: 500;
770
+ color: var(--text);
771
+ text-align: left;
772
+ cursor: pointer;
773
+ }
774
+
775
+ .app-select__option:hover {
776
+ background: var(--hover);
777
+ }
778
+
779
+ .app-select__option--selected {
780
+ background: var(--accent-soft);
781
+ color: var(--accent-text);
782
+ font-weight: 600;
783
+ }
784
+
785
+ .app-select__option--selected:hover {
786
+ background: var(--accent-soft);
787
+ }
788
+
789
+ .app-select__box {
790
+ color: var(--muted);
791
+ flex: 0 0 auto;
792
+ }
793
+
794
+ .app-select__box--on {
795
+ color: var(--accent);
796
+ }
797
+
798
+ .app-select__option-text {
799
+ flex: 1;
800
+ min-width: 0;
801
+ overflow: hidden;
802
+ text-overflow: ellipsis;
803
+ white-space: nowrap;
804
+ }
805
+
806
+ .app-field--search {
807
+ flex: 1;
808
+ min-width: 220px;
809
+ }
810
+
811
+ .app-filter-row {
812
+ display: flex;
813
+ gap: var(--grid-gap);
814
+ margin-bottom: 14px;
815
+ flex-wrap: wrap;
816
+ }
817
+
818
+ @media (max-width: 599.98px) {
819
+ .app-filter-row > * {
820
+ flex: 1 1 100%;
821
+ min-width: 0;
822
+ }
823
+ }
824
+
825
+ /* ------------------------------------------------------------------ */
826
+ /* SegmentedControl - Today | Week | Month. */
827
+ /* ------------------------------------------------------------------ */
828
+
829
+ /**
830
+ * The handoff draws this as a 3px-padded bordered tray holding 8px-radius
831
+ * segments; the SELECTED one is an ACCENT FILL, not a grey one. Vuetify's
832
+ * `v-btn-toggle` cannot be talked into that shape (its own height scale, its
833
+ * dividers, its overlay layer), so the tray is written as markup.
834
+ *
835
+ * The fill is `--accent-strong`, not `--accent`: white on `--accent` measures
836
+ * 3.23:1, the same Phase-1 rule every other filled label follows.
837
+ */
838
+ .app-seg {
839
+ display: flex;
840
+ gap: 2px;
841
+ padding: 3px;
842
+ border: 1px solid var(--border);
843
+ border-radius: var(--r-inner);
844
+ background: var(--card);
845
+ }
846
+
847
+ .app-seg__item {
848
+ padding: 6px 12px;
849
+ border: none;
850
+ border-radius: 8px;
851
+ background: transparent;
852
+ color: var(--muted);
853
+ font-family: inherit;
854
+ font-size: 12.5px;
855
+ font-weight: 700;
856
+ line-height: 1.2;
857
+ white-space: nowrap;
858
+ cursor: pointer;
859
+ }
860
+
861
+ .app-seg__item:hover {
862
+ background: var(--hover);
863
+ }
864
+
865
+ .app-seg__item--on,
866
+ .app-seg__item--on:hover {
867
+ background: var(--accent-strong);
868
+ color: var(--on-accent-strong);
869
+ }
870
+
871
+ /* ------------------------------------------------------------------ */
872
+ /* Switch - the settings toggle. */
873
+ /* ------------------------------------------------------------------ */
874
+
875
+ /**
876
+ * THE DESIGN'S TOGGLE, WRITTEN ONCE.
877
+ *
878
+ * The handoff draws it on the Settings screen's ANPR Camera section and then
879
+ * says, in the prototype's own words, that every other module's
880
+ * "toggles, form fields and tables use the same styling as the ANPR Camera
881
+ * section" - so this IS the spec for the HID panels, not an interpretation:
882
+ *
883
+ * track 38x22, `border-radius:999px`; ON `var(--accent)`, OFF `var(--hover)`
884
+ * thumb 18x18 circle at `top:2px`; ON `left:18px` `#ffffff`,
885
+ * OFF `left:2px` `var(--muted)`
886
+ * label 13.5px/700; the OFF label is `var(--text2)`
887
+ *
888
+ * `--toggle-w` / `--toggle-h` were put in `tokens.css` in Phase 1 for exactly
889
+ * this and had no rule to serve until now.
890
+ *
891
+ * Vuetify slides `.v-selection-control__input` and leaves the thumb static
892
+ * inside it; the design slides the THUMB inside a fixed track. So the input's
893
+ * transform is reset and the thumb is positioned directly - otherwise the two
894
+ * movements compound and the knob leaves the track.
895
+ *
896
+ * Three HID panels each carried their own copy of this with hardcoded hexes
897
+ * (`#8b9297` / `#2eaf55` / `#e0e0e0`), which is why a disabled toggle read as
898
+ * light grey on the dark theme's dark card. One rule, both themes, no hex.
899
+ */
900
+ .app-switch .v-selection-control__wrapper {
901
+ position: relative;
902
+ flex: 0 0 auto;
903
+ width: var(--toggle-w);
904
+ height: var(--toggle-h);
905
+ }
906
+
907
+ .app-switch .v-switch__track {
908
+ position: absolute;
909
+ inset: 0;
910
+ width: var(--toggle-w);
911
+ height: var(--toggle-h);
912
+ border-radius: var(--r-pill);
913
+ background: var(--hover);
914
+ opacity: 1;
915
+ transition: background var(--motion);
916
+ }
917
+
918
+ .app-switch .v-selection-control--dirty .v-switch__track {
919
+ background: var(--accent);
920
+ }
921
+
922
+ .app-switch .v-selection-control__input {
923
+ position: absolute;
924
+ inset: 0;
925
+ width: var(--toggle-w);
926
+ height: var(--toggle-h);
927
+ /*
928
+ * Vuetify's own slide. The design moves the thumb, not this box - but the
929
+ * reset never landed: Vuetify ships it as
930
+ * `.v-locale--is-ltr .v-switch .v-selection-control__input` (THREE classes)
931
+ * and, when checked, `.v-locale--is-ltr .v-switch .v-selection-control--dirty
932
+ * .v-selection-control__input` (FOUR), both of which outrank this two-class
933
+ * rule. So every `.app-switch` in the layer drew its thumb 10px out: measured
934
+ * OFF thumb at `track.x - 8` (design: +2) and ON at `track.x + 28`
935
+ * (design: +18). `!important` rather than stacking four `.app-switch`
936
+ * selectors to out-specify it.
937
+ */
938
+ transform: none !important;
939
+ }
940
+
941
+ /* The 40px hover/ripple disc is bigger than the whole control in this shape. */
942
+ .app-switch .v-selection-control__input::before,
943
+ .app-switch .v-selection-control__input .v-ripple__container {
944
+ display: none;
945
+ }
946
+
947
+ .app-switch .v-switch__thumb {
948
+ position: absolute;
949
+ top: 2px;
950
+ left: 2px;
951
+ width: 18px;
952
+ height: 18px;
953
+ border-radius: 50%;
954
+ background: var(--muted);
955
+ box-shadow: none;
956
+ transition: left var(--motion), background var(--motion);
957
+ }
958
+
959
+ .app-switch .v-selection-control--dirty .v-switch__thumb {
960
+ left: 18px;
961
+ background: #ffffff;
962
+ }
963
+
964
+ .app-switch .v-label {
965
+ padding-inline-start: 12px;
966
+ color: var(--text2);
967
+ font-family: var(--font-sans);
968
+ font-size: 13.5px;
969
+ font-weight: 700;
970
+ opacity: 1;
971
+ }
972
+
973
+ .app-switch .v-selection-control--dirty .v-label {
974
+ color: var(--text);
975
+ }
976
+
977
+ .app-switch.v-input--disabled {
978
+ opacity: 0.55;
979
+ }
980
+
981
+ /* ------------------------------------------------------------------ */
982
+ /* KpiTile - the dashboard's headline figure. */
983
+ /* ------------------------------------------------------------------ */
984
+
985
+ /**
986
+ * Three rows, in this order, straight from the handoff:
987
+ * 1. a 34px soft icon square and the uppercase label, side by side
988
+ * 2. the 30px tabular number with its percentage pill BESIDE it
989
+ * 3. the sub-line
990
+ * The product had the icon alone on row 1 and the pill stranded at the
991
+ * bottom, which is why the tile read as a different component.
992
+ */
993
+ .app-kpi {
994
+ padding: 18px;
995
+ }
996
+
997
+ .app-kpi__head {
998
+ display: flex;
999
+ align-items: center;
1000
+ gap: 10px;
1001
+ margin-bottom: 14px;
1002
+ }
1003
+
1004
+ .app-kpi__icon {
1005
+ display: flex;
1006
+ align-items: center;
1007
+ justify-content: center;
1008
+ width: 34px;
1009
+ height: 34px;
1010
+ flex: 0 0 34px;
1011
+ border-radius: var(--r-inner);
1012
+ }
1013
+
1014
+ .app-kpi__label {
1015
+ font-size: var(--fs-table-head);
1016
+ font-weight: var(--fw-table-head);
1017
+ letter-spacing: var(--ls-table-head);
1018
+ text-transform: uppercase;
1019
+ color: var(--muted);
1020
+ min-width: 0;
1021
+ }
1022
+
1023
+ .app-kpi__action {
1024
+ margin-left: auto;
1025
+ flex: 0 0 auto;
1026
+ }
1027
+
1028
+ .app-kpi__figure {
1029
+ display: flex;
1030
+ align-items: center;
1031
+ gap: 10px;
1032
+ flex-wrap: wrap;
1033
+ }
1034
+
1035
+ .app-kpi__value {
1036
+ font-size: var(--fs-kpi);
1037
+ font-weight: var(--fw-kpi);
1038
+ letter-spacing: var(--ls-kpi);
1039
+ line-height: 1.1;
1040
+ color: var(--text);
1041
+ font-variant-numeric: tabular-nums;
1042
+ }
1043
+
1044
+ .app-kpi__chip {
1045
+ display: inline-flex;
1046
+ align-items: center;
1047
+ padding: 3px 8px;
1048
+ border-radius: 999px;
1049
+ font-size: 11.5px;
1050
+ font-weight: 800;
1051
+ line-height: 1.2;
1052
+ background: var(--ok-bg);
1053
+ color: var(--ok);
1054
+ }
1055
+
1056
+ .app-kpi__chip--down {
1057
+ background: var(--err-bg);
1058
+ color: var(--err);
1059
+ }
1060
+
1061
+ .app-kpi__sub {
1062
+ margin-top: 6px;
1063
+ font-size: 12.5px;
1064
+ font-weight: 600;
1065
+ color: var(--muted);
1066
+ }
1067
+
1068
+ /* ------------------------------------------------------------------ */
1069
+ /* Responsive - the prototype has no reference, this follows Phase 1. */
1070
+ /* ------------------------------------------------------------------ */
1071
+
1072
+ @media (max-width: 767.98px) {
1073
+ .app-page-head {
1074
+ align-items: flex-start;
1075
+ }
1076
+
1077
+ /* The grid table keeps its columns and scrolls sideways inside the card,
1078
+ deliberately not restacked into key/value blocks: the columns carry
1079
+ meaning by position. Same decision as Phase 1's tables. */
1080
+ .app-table-scroll {
1081
+ overflow-x: auto;
1082
+ }
1083
+
1084
+ .app-table-scroll > * {
1085
+ min-width: 720px;
1086
+ }
1087
+ }
1088
+
1089
+ /* ------------------------------------------------------------------ */
1090
+ /* Field label (`components/InputLabel.vue`, 125 caller files). */
1091
+ /* */
1092
+ /* Every form label in the product rendered Vuetify's `text-subtitle-2`:*/
1093
+ /* 14px/500 at 0.0071em tracking, declaring `font-family: Roboto`. */
1094
+ /* The handoff's own label - the Create Role modal - is 12.5px/700 with*/
1095
+ /* a 6px gap under it and the asterisk on `--err`. */
1096
+ /* */
1097
+ /* `margin-bottom` is deliberately NOT `!important`, so the ~40 callers*/
1098
+ /* that already pass a Vuetify spacing utility (`mb-1`, `mb-2`) keep */
1099
+ /* winning - those classes carry `!important` - and only the labels */
1100
+ /* that state no spacing take the design's 6px. */
1101
+ /* ------------------------------------------------------------------ */
1102
+ .app-input-label {
1103
+ font-family: var(--font-sans);
1104
+ font-size: 12.5px;
1105
+ font-weight: 700;
1106
+ letter-spacing: 0;
1107
+ color: var(--text);
1108
+ margin-bottom: 6px;
1109
+ }
1110
+
1111
+ .app-input-label__req {
1112
+ color: var(--err);
1113
+ }