permission_forge 0.14.1

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 (406) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1138 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +356 -0
  5. data/app/controllers/concerns/permission_forge/acting_admin.rb +17 -0
  6. data/app/controllers/concerns/permission_forge/api/crud_actions.rb +84 -0
  7. data/app/controllers/concerns/permission_forge/api/nested_resource_lookup.rb +18 -0
  8. data/app/controllers/concerns/permission_forge/api/pagination.rb +50 -0
  9. data/app/controllers/concerns/permission_forge/api/response_renderer.rb +65 -0
  10. data/app/controllers/concerns/permission_forge/api/serializer_resolver.rb +30 -0
  11. data/app/controllers/concerns/permission_forge/api_authentication.rb +40 -0
  12. data/app/controllers/concerns/permission_forge/assignment_authorization.rb +97 -0
  13. data/app/controllers/concerns/permission_forge/audit_log_gate.rb +55 -0
  14. data/app/controllers/concerns/permission_forge/basic_authentication.rb +77 -0
  15. data/app/controllers/concerns/permission_forge/cache_confirmation.rb +32 -0
  16. data/app/controllers/concerns/permission_forge/forgery_protection.rb +29 -0
  17. data/app/controllers/concerns/permission_forge/html_format.rb +40 -0
  18. data/app/controllers/concerns/permission_forge/param_shapes.rb +87 -0
  19. data/app/controllers/concerns/permission_forge/tenant_required.rb +42 -0
  20. data/app/controllers/permission_forge/api/audit_events_controller.rb +33 -0
  21. data/app/controllers/permission_forge/api/base_controller.rb +220 -0
  22. data/app/controllers/permission_forge/api/dashboard_controller.rb +30 -0
  23. data/app/controllers/permission_forge/api/permission_set_group_users_controller.rb +51 -0
  24. data/app/controllers/permission_forge/api/permission_set_groups_controller.rb +71 -0
  25. data/app/controllers/permission_forge/api/permission_set_rules_controller.rb +37 -0
  26. data/app/controllers/permission_forge/api/permission_set_users_controller.rb +105 -0
  27. data/app/controllers/permission_forge/api/permission_sets_controller.rb +87 -0
  28. data/app/controllers/permission_forge/api/resource_actions_controller.rb +58 -0
  29. data/app/controllers/permission_forge/api/resource_fields_controller.rb +30 -0
  30. data/app/controllers/permission_forge/api/resources_controller.rb +56 -0
  31. data/app/controllers/permission_forge/application_controller.rb +198 -0
  32. data/app/controllers/permission_forge/assets_controller.rb +97 -0
  33. data/app/controllers/permission_forge/assignments_controller.rb +266 -0
  34. data/app/controllers/permission_forge/audit_events_controller.rb +94 -0
  35. data/app/controllers/permission_forge/bulk_assignments_controller.rb +108 -0
  36. data/app/controllers/permission_forge/field_lookups_controller.rb +38 -0
  37. data/app/controllers/permission_forge/home_controller.rb +24 -0
  38. data/app/controllers/permission_forge/maintenance_controller.rb +13 -0
  39. data/app/controllers/permission_forge/people_controller.rb +190 -0
  40. data/app/controllers/permission_forge/permission_set_editor.rb +71 -0
  41. data/app/controllers/permission_forge/permission_set_group_users_controller.rb +57 -0
  42. data/app/controllers/permission_forge/permission_set_groups_controller.rb +111 -0
  43. data/app/controllers/permission_forge/permission_set_rules_controller.rb +97 -0
  44. data/app/controllers/permission_forge/permission_sets_controller.rb +227 -0
  45. data/app/controllers/permission_forge/preferences_controller.rb +26 -0
  46. data/app/controllers/permission_forge/resource_actions_controller.rb +106 -0
  47. data/app/controllers/permission_forge/resource_fields_controller.rb +129 -0
  48. data/app/controllers/permission_forge/resources_controller.rb +189 -0
  49. data/app/controllers/permission_forge/settings_controller.rb +33 -0
  50. data/app/frontend/permission_forge/brand/apple-touch-icon.png +0 -0
  51. data/app/frontend/permission_forge/brand/chevron.svg +1 -0
  52. data/app/frontend/permission_forge/brand/favicon.ico +0 -0
  53. data/app/frontend/permission_forge/brand/favicon.svg +1 -0
  54. data/app/frontend/permission_forge/brand/logo-horizontal-dark.svg +1 -0
  55. data/app/frontend/permission_forge/brand/logo-horizontal-light.svg +1 -0
  56. data/app/frontend/permission_forge/brand/mark-small.svg +1 -0
  57. data/app/frontend/permission_forge/brand/mark.svg +1 -0
  58. data/app/frontend/permission_forge/pf_admin.css +766 -0
  59. data/app/frontend/permission_forge/pf_admin.js +482 -0
  60. data/app/helpers/permission_forge/application_helper.rb +167 -0
  61. data/app/helpers/permission_forge/audit_helper.rb +230 -0
  62. data/app/helpers/permission_forge/audit_sentences_helper.rb +84 -0
  63. data/app/helpers/permission_forge/badges_helper.rb +134 -0
  64. data/app/helpers/permission_forge/changes_helper.rb +117 -0
  65. data/app/helpers/permission_forge/people_helper.rb +118 -0
  66. data/app/helpers/permission_forge/sentences_helper.rb +175 -0
  67. data/app/helpers/permission_forge/settings_helper.rb +152 -0
  68. data/app/helpers/permission_forge/ui_helper.rb +184 -0
  69. data/app/models/permission_forge/audit_event.rb +159 -0
  70. data/app/models/permission_forge/permission_set.rb +100 -0
  71. data/app/models/permission_forge/permission_set_group.rb +83 -0
  72. data/app/models/permission_forge/permission_set_group_user.rb +43 -0
  73. data/app/models/permission_forge/permission_set_rule.rb +99 -0
  74. data/app/models/permission_forge/permission_set_user.rb +158 -0
  75. data/app/models/permission_forge/resource.rb +100 -0
  76. data/app/models/permission_forge/resource_action.rb +58 -0
  77. data/app/models/permission_forge/resource_field.rb +69 -0
  78. data/app/views/layouts/permission_forge/application.html.erb +30 -0
  79. data/app/views/layouts/permission_forge/fragment.html.erb +2 -0
  80. data/app/views/permission_forge/assignments/_person_options.html.erb +26 -0
  81. data/app/views/permission_forge/assignments/confirm_destroy.html.erb +24 -0
  82. data/app/views/permission_forge/assignments/new.html.erb +99 -0
  83. data/app/views/permission_forge/audit_events/index.html.erb +161 -0
  84. data/app/views/permission_forge/audit_events/show.html.erb +146 -0
  85. data/app/views/permission_forge/bulk_assignments/confirm_revoke.html.erb +21 -0
  86. data/app/views/permission_forge/bulk_assignments/new.html.erb +69 -0
  87. data/app/views/permission_forge/field_lookups/show.html.erb +32 -0
  88. data/app/views/permission_forge/home/_audit_card.html.erb +19 -0
  89. data/app/views/permission_forge/home/_ending_card.html.erb +32 -0
  90. data/app/views/permission_forge/home/_expired_card.html.erb +34 -0
  91. data/app/views/permission_forge/home/_privacy_card.html.erb +67 -0
  92. data/app/views/permission_forge/home/_rules_card.html.erb +23 -0
  93. data/app/views/permission_forge/home/_stale_card.html.erb +39 -0
  94. data/app/views/permission_forge/home/_tidy_card.html.erb +18 -0
  95. data/app/views/permission_forge/home/index.html.erb +69 -0
  96. data/app/views/permission_forge/people/_access_tab.html.erb +58 -0
  97. data/app/views/permission_forge/people/_audit_tab.html.erb +93 -0
  98. data/app/views/permission_forge/people/_effective_field_source.html.erb +11 -0
  99. data/app/views/permission_forge/people/_effective_tab.html.erb +118 -0
  100. data/app/views/permission_forge/people/_effective_why.html.erb +48 -0
  101. data/app/views/permission_forge/people/_why_item.html.erb +19 -0
  102. data/app/views/permission_forge/people/confirm_remove_all.html.erb +27 -0
  103. data/app/views/permission_forge/people/consent_preview.html.erb +39 -0
  104. data/app/views/permission_forge/people/erasure_preview.html.erb +28 -0
  105. data/app/views/permission_forge/people/index.html.erb +78 -0
  106. data/app/views/permission_forge/people/show.html.erb +54 -0
  107. data/app/views/permission_forge/permission_set_groups/confirm_destroy.html.erb +12 -0
  108. data/app/views/permission_forge/permission_set_groups/index.html.erb +55 -0
  109. data/app/views/permission_forge/permission_set_groups/show.html.erb +100 -0
  110. data/app/views/permission_forge/permission_set_rules/_builder.html.erb +145 -0
  111. data/app/views/permission_forge/permission_set_rules/_value_input.html.erb +42 -0
  112. data/app/views/permission_forge/permission_set_rules/confirm_destroy.html.erb +13 -0
  113. data/app/views/permission_forge/permission_set_rules/edit.html.erb +3 -0
  114. data/app/views/permission_forge/permission_set_rules/new.html.erb +3 -0
  115. data/app/views/permission_forge/permission_sets/_assignments_tab.html.erb +74 -0
  116. data/app/views/permission_forge/permission_sets/_audit_tab.html.erb +80 -0
  117. data/app/views/permission_forge/permission_sets/_conditions_tab.html.erb +83 -0
  118. data/app/views/permission_forge/permission_sets/_header.html.erb +36 -0
  119. data/app/views/permission_forge/permission_sets/_records_summary.html.erb +16 -0
  120. data/app/views/permission_forge/permission_sets/_resource_summary.html.erb +11 -0
  121. data/app/views/permission_forge/permission_sets/_resources_tab.html.erb +49 -0
  122. data/app/views/permission_forge/permission_sets/confirm_destroy.html.erb +23 -0
  123. data/app/views/permission_forge/permission_sets/confirm_switch_mode.html.erb +15 -0
  124. data/app/views/permission_forge/permission_sets/index.html.erb +83 -0
  125. data/app/views/permission_forge/permission_sets/new.html.erb +63 -0
  126. data/app/views/permission_forge/permission_sets/review.html.erb +26 -0
  127. data/app/views/permission_forge/permission_sets/show.html.erb +50 -0
  128. data/app/views/permission_forge/resource_actions/_across_sets.html.erb +24 -0
  129. data/app/views/permission_forge/resource_actions/_add_select.html.erb +12 -0
  130. data/app/views/permission_forge/resource_actions/_effect.html.erb +25 -0
  131. data/app/views/permission_forge/resource_actions/_endpoints.html.erb +16 -0
  132. data/app/views/permission_forge/resource_actions/_form.html.erb +44 -0
  133. data/app/views/permission_forge/resource_actions/_managed_note.html.erb +11 -0
  134. data/app/views/permission_forge/resource_actions/confirm_destroy.html.erb +13 -0
  135. data/app/views/permission_forge/resource_actions/new.html.erb +31 -0
  136. data/app/views/permission_forge/resource_actions/show.html.erb +39 -0
  137. data/app/views/permission_forge/resource_fields/_explain.html.erb +24 -0
  138. data/app/views/permission_forge/resource_fields/show.html.erb +186 -0
  139. data/app/views/permission_forge/resources/_actions_tab.html.erb +88 -0
  140. data/app/views/permission_forge/resources/_fields_tab.html.erb +89 -0
  141. data/app/views/permission_forge/resources/_rules_tab.html.erb +55 -0
  142. data/app/views/permission_forge/resources/_sync.html.erb +57 -0
  143. data/app/views/permission_forge/resources/confirm_destroy.html.erb +17 -0
  144. data/app/views/permission_forge/resources/new.html.erb +57 -0
  145. data/app/views/permission_forge/resources/show.html.erb +117 -0
  146. data/app/views/permission_forge/settings/_ladder.html.erb +15 -0
  147. data/app/views/permission_forge/settings/_row.html.erb +38 -0
  148. data/app/views/permission_forge/settings/show.html.erb +67 -0
  149. data/app/views/permission_forge/shared/_across_sets.html.erb +31 -0
  150. data/app/views/permission_forge/shared/_audit_events.html.erb +36 -0
  151. data/app/views/permission_forge/shared/_audit_footer.html.erb +16 -0
  152. data/app/views/permission_forge/shared/_audit_off_note.html.erb +15 -0
  153. data/app/views/permission_forge/shared/_audit_pagination.html.erb +19 -0
  154. data/app/views/permission_forge/shared/_audit_refused_note.html.erb +7 -0
  155. data/app/views/permission_forge/shared/_audit_timeline.html.erb +29 -0
  156. data/app/views/permission_forge/shared/_crud_cell.html.erb +18 -0
  157. data/app/views/permission_forge/shared/_dialog_frame.html.erb +11 -0
  158. data/app/views/permission_forge/shared/_expiry_picker.html.erb +27 -0
  159. data/app/views/permission_forge/shared/_head_extra.html.erb +3 -0
  160. data/app/views/permission_forge/shared/_help_banner.html.erb +10 -0
  161. data/app/views/permission_forge/shared/_level_control.html.erb +18 -0
  162. data/app/views/permission_forge/shared/_mode_banners.html.erb +22 -0
  163. data/app/views/permission_forge/shared/_nav.html.erb +12 -0
  164. data/app/views/permission_forge/shared/_preferences.html.erb +24 -0
  165. data/app/views/permission_forge/shared/_reason_field.html.erb +20 -0
  166. data/app/views/permission_forge/shared/_savebar.html.erb +13 -0
  167. data/app/views/permission_forge/shared/_sheet_frame.html.erb +22 -0
  168. data/app/views/permission_forge/shared/_sidebar.html.erb +18 -0
  169. data/app/views/permission_forge/shared/_tech_details.html.erb +20 -0
  170. data/app/views/permission_forge/shared/_toasts.html.erb +27 -0
  171. data/app/views/permission_forge/shared/_topbar.html.erb +15 -0
  172. data/app/views/permission_forge/shared/_user_select.html.erb +16 -0
  173. data/app/views/permission_forge/shared/audit_refused.html.erb +7 -0
  174. data/config/locales/permission_forge.en.yml +2115 -0
  175. data/config/locales/permission_forge.mk.yml +2166 -0
  176. data/config/locales/permission_forge.sq.yml +2164 -0
  177. data/config/routes.rb +143 -0
  178. data/lib/generators/permission_forge/install/install_generator.rb +105 -0
  179. data/lib/generators/permission_forge/install/templates/initializer.rb.tt +148 -0
  180. data/lib/generators/permission_forge/install/templates/migration.rb.tt +198 -0
  181. data/lib/generators/permission_forge/rls/rls_generator.rb +57 -0
  182. data/lib/generators/permission_forge/rls/templates/enable_permission_forge_rls.rb.tt +15 -0
  183. data/lib/generators/permission_forge/upgrade/templates/add_admin_ui_columns.rb.tt +8 -0
  184. data/lib/generators/permission_forge/upgrade/templates/add_audit_events.rb.tt +98 -0
  185. data/lib/generators/permission_forge/upgrade/templates/add_resource_fields_unique_index.rb.tt +69 -0
  186. data/lib/generators/permission_forge/upgrade/upgrade_generator.rb +66 -0
  187. data/lib/permission_forge/admin/action_across_sets.rb +76 -0
  188. data/lib/permission_forge/admin/action_rows.rb +156 -0
  189. data/lib/permission_forge/admin/attention_items.rb +207 -0
  190. data/lib/permission_forge/admin/audit_event_detail.rb +75 -0
  191. data/lib/permission_forge/admin/audit_log.rb +193 -0
  192. data/lib/permission_forge/admin/audit_log_days.rb +41 -0
  193. data/lib/permission_forge/admin/audit_lookups.rb +105 -0
  194. data/lib/permission_forge/admin/audit_name_history.rb +85 -0
  195. data/lib/permission_forge/admin/audit_trail.rb +169 -0
  196. data/lib/permission_forge/admin/bulk_people_matcher.rb +109 -0
  197. data/lib/permission_forge/admin/change_set.rb +123 -0
  198. data/lib/permission_forge/admin/change_set_parts/actions.rb +27 -0
  199. data/lib/permission_forge/admin/change_set_parts/base.rb +58 -0
  200. data/lib/permission_forge/admin/change_set_parts/context.rb +50 -0
  201. data/lib/permission_forge/admin/change_set_parts/field_edit.rb +67 -0
  202. data/lib/permission_forge/admin/change_set_parts/field_levels.rb +71 -0
  203. data/lib/permission_forge/admin/change_set_parts/new_actions.rb +91 -0
  204. data/lib/permission_forge/admin/change_set_parts/new_fields.rb +63 -0
  205. data/lib/permission_forge/admin/change_set_parts/resource_edit.rb +55 -0
  206. data/lib/permission_forge/admin/change_set_parts/resource_flags.rb +19 -0
  207. data/lib/permission_forge/admin/change_set_parts/set_attrs.rb +45 -0
  208. data/lib/permission_forge/admin/change_set_parts/values.rb +20 -0
  209. data/lib/permission_forge/admin/column_fields.rb +48 -0
  210. data/lib/permission_forge/admin/conditions_form.rb +78 -0
  211. data/lib/permission_forge/admin/controller_catalog.rb +166 -0
  212. data/lib/permission_forge/admin/csv.rb +41 -0
  213. data/lib/permission_forge/admin/editable_lock.rb +157 -0
  214. data/lib/permission_forge/admin/effective_access.rb +158 -0
  215. data/lib/permission_forge/admin/effective_fields.rb +176 -0
  216. data/lib/permission_forge/admin/expiry_presets.rb +51 -0
  217. data/lib/permission_forge/admin/exports/document.rb +89 -0
  218. data/lib/permission_forge/admin/exports/events.rb +51 -0
  219. data/lib/permission_forge/admin/exports/holdings.rb +118 -0
  220. data/lib/permission_forge/admin/exports.rb +37 -0
  221. data/lib/permission_forge/admin/field_across_sets.rb +51 -0
  222. data/lib/permission_forge/admin/field_detail.rb +112 -0
  223. data/lib/permission_forge/admin/field_level.rb +74 -0
  224. data/lib/permission_forge/admin/field_rows.rb +137 -0
  225. data/lib/permission_forge/admin/last_changes.rb +94 -0
  226. data/lib/permission_forge/admin/model_catalog.rb +216 -0
  227. data/lib/permission_forge/admin/people_candidates.rb +108 -0
  228. data/lib/permission_forge/admin/people_directory.rb +158 -0
  229. data/lib/permission_forge/admin/permission_set_factory.rb +48 -0
  230. data/lib/permission_forge/admin/person_label.rb +95 -0
  231. data/lib/permission_forge/admin/record_id.rb +19 -0
  232. data/lib/permission_forge/admin/rule_builder.rb +58 -0
  233. data/lib/permission_forge/admin/rule_form.rb +175 -0
  234. data/lib/permission_forge/admin/rule_form_input.rb +74 -0
  235. data/lib/permission_forge/admin/sensitive_columns.rb +68 -0
  236. data/lib/permission_forge/admin/settings_catalog.rb +261 -0
  237. data/lib/permission_forge/admin/settings_values.rb +130 -0
  238. data/lib/permission_forge/api/serializers/audit_event_serializer.rb +27 -0
  239. data/lib/permission_forge/api/serializers/base_serializer.rb +36 -0
  240. data/lib/permission_forge/api/serializers/permission_set_group_serializer.rb +12 -0
  241. data/lib/permission_forge/api/serializers/permission_set_group_user_serializer.rb +11 -0
  242. data/lib/permission_forge/api/serializers/permission_set_rule_serializer.rb +25 -0
  243. data/lib/permission_forge/api/serializers/permission_set_serializer.rb +13 -0
  244. data/lib/permission_forge/api/serializers/permission_set_user_serializer.rb +11 -0
  245. data/lib/permission_forge/api/serializers/resource_action_serializer.rb +13 -0
  246. data/lib/permission_forge/api/serializers/resource_field_serializer.rb +14 -0
  247. data/lib/permission_forge/api/serializers/resource_serializer.rb +15 -0
  248. data/lib/permission_forge/audit/actor.rb +144 -0
  249. data/lib/permission_forge/audit/actor_resolver.rb +103 -0
  250. data/lib/permission_forge/audit/audited_views.rb +208 -0
  251. data/lib/permission_forge/audit/boot_warnings.rb +58 -0
  252. data/lib/permission_forge/audit/column_values.rb +56 -0
  253. data/lib/permission_forge/audit/context.rb +98 -0
  254. data/lib/permission_forge/audit/detached.rb +62 -0
  255. data/lib/permission_forge/audit/event_catalog.rb +110 -0
  256. data/lib/permission_forge/audit/event_query.rb +192 -0
  257. data/lib/permission_forge/audit/failure.rb +47 -0
  258. data/lib/permission_forge/audit/field_access.rb +186 -0
  259. data/lib/permission_forge/audit/gate.rb +167 -0
  260. data/lib/permission_forge/audit/job_context.rb +32 -0
  261. data/lib/permission_forge/audit/log_access.rb +36 -0
  262. data/lib/permission_forge/audit/recorder.rb +168 -0
  263. data/lib/permission_forge/audit/refusals.rb +61 -0
  264. data/lib/permission_forge/audit/request_context.rb +152 -0
  265. data/lib/permission_forge/audit/runtime.rb +178 -0
  266. data/lib/permission_forge/audit/runtime_emission.rb +135 -0
  267. data/lib/permission_forge/audit/settings_observer.rb +70 -0
  268. data/lib/permission_forge/audit/source.rb +54 -0
  269. data/lib/permission_forge/audit/subscribers/base.rb +69 -0
  270. data/lib/permission_forge/audit/subscribers/legacy_hooks.rb +97 -0
  271. data/lib/permission_forge/audit/subscribers/log.rb +45 -0
  272. data/lib/permission_forge/audit/subscribers.rb +58 -0
  273. data/lib/permission_forge/audit.rb +302 -0
  274. data/lib/permission_forge/boot_checks.rb +184 -0
  275. data/lib/permission_forge/cache/backlog.rb +142 -0
  276. data/lib/permission_forge/cache/base.rb +200 -0
  277. data/lib/permission_forge/cache/bulk_invalidation.rb +131 -0
  278. data/lib/permission_forge/cache/failure.rb +171 -0
  279. data/lib/permission_forge/cache/handled_errors.rb +82 -0
  280. data/lib/permission_forge/cache/holder_invalidation.rb +85 -0
  281. data/lib/permission_forge/cache/invalidation.rb +180 -0
  282. data/lib/permission_forge/cache/memory_cache.rb +77 -0
  283. data/lib/permission_forge/cache/null_cache.rb +40 -0
  284. data/lib/permission_forge/cache/rails_cache.rb +231 -0
  285. data/lib/permission_forge/cache/rebuild.rb +149 -0
  286. data/lib/permission_forge/cache/settlement.rb +63 -0
  287. data/lib/permission_forge/cache/user_keys.rb +206 -0
  288. data/lib/permission_forge/cache/variant_removal.rb +55 -0
  289. data/lib/permission_forge/cache/withheld_notice.rb +60 -0
  290. data/lib/permission_forge/concerns/audited_collection.rb +34 -0
  291. data/lib/permission_forge/concerns/auto_permission_scoping.rb +158 -0
  292. data/lib/permission_forge/concerns/controller_action_guard.rb +106 -0
  293. data/lib/permission_forge/concerns/field_mask_checker.rb +45 -0
  294. data/lib/permission_forge/concerns/has_active_scope.rb +12 -0
  295. data/lib/permission_forge/concerns/has_audit_trail.rb +59 -0
  296. data/lib/permission_forge/concerns/has_permission_set_users.rb +115 -0
  297. data/lib/permission_forge/concerns/has_tenant_scoping.rb +86 -0
  298. data/lib/permission_forge/concerns/has_uuid.rb +24 -0
  299. data/lib/permission_forge/concerns/invalidates_in_batch.rb +207 -0
  300. data/lib/permission_forge/concerns/invalidates_managed_actions.rb +80 -0
  301. data/lib/permission_forge/concerns/masked_attributes/alias_access.rb +280 -0
  302. data/lib/permission_forge/concerns/masked_attributes/store_access.rb +100 -0
  303. data/lib/permission_forge/concerns/masked_attributes/validation_reads.rb +153 -0
  304. data/lib/permission_forge/concerns/masked_attributes/virtual_readers.rb +242 -0
  305. data/lib/permission_forge/concerns/masked_attributes.rb +576 -0
  306. data/lib/permission_forge/concerns/permission_cache_accessor.rb +416 -0
  307. data/lib/permission_forge/concerns/permission_scoped_findable.rb +337 -0
  308. data/lib/permission_forge/concerns/permissioned_user.rb +78 -0
  309. data/lib/permission_forge/concerns/permissions_memo.rb +160 -0
  310. data/lib/permission_forge/concerns/permissions_per_context.rb +288 -0
  311. data/lib/permission_forge/concerns/permissions_regenerator.rb +43 -0
  312. data/lib/permission_forge/concerns/records_audit_events.rb +286 -0
  313. data/lib/permission_forge/concerns/resource_permission_checker.rb +116 -0
  314. data/lib/permission_forge/concerns/validates_column_lengths.rb +69 -0
  315. data/lib/permission_forge/concerns/validates_dependency.rb +54 -0
  316. data/lib/permission_forge/conditional_sets.rb +361 -0
  317. data/lib/permission_forge/configuration/admin_ui_settings.rb +108 -0
  318. data/lib/permission_forge/configuration/audit_settings.rb +184 -0
  319. data/lib/permission_forge/configuration/cache_settings.rb +76 -0
  320. data/lib/permission_forge/configuration/controller_action_settings.rb +112 -0
  321. data/lib/permission_forge/configuration/gdpr_settings.rb +70 -0
  322. data/lib/permission_forge/configuration/masked_attributes_settings.rb +83 -0
  323. data/lib/permission_forge/configuration/permits_attributes_settings.rb +26 -0
  324. data/lib/permission_forge/configuration/presets.rb +139 -0
  325. data/lib/permission_forge/configuration/query_scoping_settings.rb +107 -0
  326. data/lib/permission_forge/configuration/strategy_values.rb +57 -0
  327. data/lib/permission_forge/configuration/virtual_attributes_settings.rb +65 -0
  328. data/lib/permission_forge/configuration.rb +624 -0
  329. data/lib/permission_forge/controller_actions/authorizer.rb +171 -0
  330. data/lib/permission_forge/controller_actions/managed_index.rb +213 -0
  331. data/lib/permission_forge/controller_actions/mapping.rb +167 -0
  332. data/lib/permission_forge/controller_actions.rb +122 -0
  333. data/lib/permission_forge/deprecation.rb +86 -0
  334. data/lib/permission_forge/engine.rb +113 -0
  335. data/lib/permission_forge/execution_state.rb +318 -0
  336. data/lib/permission_forge/invalidation_batch.rb +206 -0
  337. data/lib/permission_forge/masked_models.rb +226 -0
  338. data/lib/permission_forge/masking_sync.rb +271 -0
  339. data/lib/permission_forge/model_bindings.rb +90 -0
  340. data/lib/permission_forge/open_transaction.rb +303 -0
  341. data/lib/permission_forge/operators/constants.rb +41 -0
  342. data/lib/permission_forge/operators/registry.rb +300 -0
  343. data/lib/permission_forge/operators/rule_column.rb +120 -0
  344. data/lib/permission_forge/operators/validations.rb +111 -0
  345. data/lib/permission_forge/operators/value_casters/number.rb +68 -0
  346. data/lib/permission_forge/operators/value_casters/temporal.rb +69 -0
  347. data/lib/permission_forge/operators/value_casters.rb +80 -0
  348. data/lib/permission_forge/operators/value_typing.rb +115 -0
  349. data/lib/permission_forge/query_scoping_warnings.rb +68 -0
  350. data/lib/permission_forge/rake_audit.rb +65 -0
  351. data/lib/permission_forge/resource_chain.rb +190 -0
  352. data/lib/permission_forge/rls.rb +209 -0
  353. data/lib/permission_forge/services/attribute_access_guard.rb +138 -0
  354. data/lib/permission_forge/services/audit_event_purger.rb +150 -0
  355. data/lib/permission_forge/services/bulk_permission_set_assignment.rb +108 -0
  356. data/lib/permission_forge/services/concerns/across_tenants.rb +69 -0
  357. data/lib/permission_forge/services/concerns/audited_operation.rb +43 -0
  358. data/lib/permission_forge/services/concerns/clonable.rb +29 -0
  359. data/lib/permission_forge/services/consent_withdrawal_handler.rb +135 -0
  360. data/lib/permission_forge/services/dashboard_stats.rb +43 -0
  361. data/lib/permission_forge/services/data_subject_erasure.rb +306 -0
  362. data/lib/permission_forge/services/expired_assignment_cleaner.rb +152 -0
  363. data/lib/permission_forge/services/extra_field_defaults.rb +71 -0
  364. data/lib/permission_forge/services/field_permission_merger.rb +83 -0
  365. data/lib/permission_forge/services/filters_response_attributes.rb +144 -0
  366. data/lib/permission_forge/services/gdpr_field_auditor/text_report.rb +63 -0
  367. data/lib/permission_forge/services/gdpr_field_auditor.rb +223 -0
  368. data/lib/permission_forge/services/host_key_guard.rb +110 -0
  369. data/lib/permission_forge/services/mask_value.rb +93 -0
  370. data/lib/permission_forge/services/permission_cache_clearer.rb +174 -0
  371. data/lib/permission_forge/services/permission_hash_builder.rb +305 -0
  372. data/lib/permission_forge/services/permission_rule_applier.rb +192 -0
  373. data/lib/permission_forge/services/permission_set_cloner.rb +89 -0
  374. data/lib/permission_forge/services/permission_set_condition_evaluator.rb +102 -0
  375. data/lib/permission_forge/services/permission_set_group_cloner.rb +52 -0
  376. data/lib/permission_forge/services/permits_attributes/nested_records.rb +106 -0
  377. data/lib/permission_forge/services/permits_attributes.rb +270 -0
  378. data/lib/permission_forge/services/resource_field_generator.rb +93 -0
  379. data/lib/permission_forge/services/resource_with_fields_builder.rb +47 -0
  380. data/lib/permission_forge/services/rls_operator_sql.rb +131 -0
  381. data/lib/permission_forge/services/rls_policy_generator.rb +365 -0
  382. data/lib/permission_forge/services/security_audit/action_check.rb +157 -0
  383. data/lib/permission_forge/services/security_audit/assignment_check.rb +120 -0
  384. data/lib/permission_forge/services/security_audit/audit_trail_check.rb +166 -0
  385. data/lib/permission_forge/services/security_audit/base_check.rb +57 -0
  386. data/lib/permission_forge/services/security_audit/config_check.rb +212 -0
  387. data/lib/permission_forge/services/security_audit/integrity_check.rb +105 -0
  388. data/lib/permission_forge/services/security_audit/personal_data_check.rb +79 -0
  389. data/lib/permission_forge/services/security_audit/privilege_check.rb +67 -0
  390. data/lib/permission_forge/services/security_audit/rule_check.rb +141 -0
  391. data/lib/permission_forge/services/security_audit/text_report.rb +46 -0
  392. data/lib/permission_forge/services/security_audit/user_model_check.rb +67 -0
  393. data/lib/permission_forge/services/security_audit.rb +75 -0
  394. data/lib/permission_forge/services/stale_assignment_auditor.rb +68 -0
  395. data/lib/permission_forge/services/unscoped_query_detector.rb +132 -0
  396. data/lib/permission_forge/version.rb +3 -0
  397. data/lib/permission_forge/virtual_fields/discovery.rb +65 -0
  398. data/lib/permission_forge/virtual_fields.rb +102 -0
  399. data/lib/permission_forge.rb +464 -0
  400. data/lib/tasks/permission_forge.rake +31 -0
  401. data/lib/tasks/permission_forge_audit_events.rake +28 -0
  402. data/lib/tasks/permission_forge_cache.rake +26 -0
  403. data/lib/tasks/permission_forge_expiration.rake +30 -0
  404. data/lib/tasks/permission_forge_gdpr.rake +69 -0
  405. data/lib/tasks/permission_forge_rls.rake +38 -0
  406. metadata +515 -0
@@ -0,0 +1,766 @@
1
+ /*
2
+ * PermissionForge admin UI.
3
+ *
4
+ * Self-contained: served by the engine itself (no host asset pipeline), every
5
+ * selector is scoped to .pf-root or prefixed pf-, and nothing styles bare
6
+ * elements outside the engine's own document. Host apps can re-theme by
7
+ * overriding the --pf-* custom properties on .pf-root.
8
+ */
9
+
10
+ /* ------------------------------------------------------------------ tokens */
11
+ .pf-root,
12
+ .pf-root[data-theme="light"] {
13
+ --pf-font: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
14
+ --pf-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
15
+
16
+ --pf-radius-xs: 4px;
17
+ --pf-radius-sm: 6px;
18
+ --pf-radius: 8px;
19
+ --pf-radius-lg: 12px;
20
+
21
+ --pf-space-1: 4px;
22
+ --pf-space-2: 8px;
23
+ --pf-space-3: 12px;
24
+ --pf-space-4: 16px;
25
+ --pf-space-5: 24px;
26
+ --pf-space-6: 32px;
27
+
28
+ --pf-bg: #f7f7f5;
29
+ --pf-surface: #ffffff;
30
+ --pf-surface-2: #f1f1ee;
31
+ --pf-border: #e4e4df;
32
+ --pf-border-strong: #cdcdc6;
33
+ --pf-text: #1b1c1f;
34
+ --pf-text-2: #50535a;
35
+ --pf-text-3: #6b6e75;
36
+
37
+ --pf-accent: #2f54c8;
38
+ --pf-accent-hover: #2645a8;
39
+ --pf-on-accent: #ffffff;
40
+ --pf-accent-soft: #ebf0fc;
41
+ --pf-accent-ink: #2645a8;
42
+
43
+ --pf-grant: #1a7444;
44
+ --pf-grant-soft: #e7f4ec;
45
+ --pf-grant-border: #b5dcc4;
46
+
47
+ --pf-block: #b42318;
48
+ --pf-block-soft: #fdecea;
49
+ --pf-block-border: #f2c2bc;
50
+
51
+ --pf-mask: #6941c6;
52
+ --pf-mask-soft: #f1ecfc;
53
+ --pf-mask-border: #d5c9f4;
54
+
55
+ --pf-restrict: #a8470a;
56
+ --pf-restrict-soft: #fdf0e6;
57
+ --pf-restrict-border: #f0c7a5;
58
+
59
+ --pf-warn: #8a5300;
60
+ --pf-warn-soft: #fdf5e3;
61
+ --pf-warn-border: #efd8a4;
62
+
63
+ --pf-overlay: rgba(20, 22, 26, .40);
64
+ --pf-shadow: 0 1px 2px rgba(16, 18, 22, .06), 0 8px 24px rgba(16, 18, 22, .08);
65
+
66
+ color-scheme: light;
67
+ }
68
+
69
+ .pf-root[data-theme="dark"] {
70
+ --pf-bg: #111214;
71
+ --pf-surface: #18191c;
72
+ --pf-surface-2: #212327;
73
+ --pf-border: #2c2e33;
74
+ --pf-border-strong: #3c3f45;
75
+ --pf-text: #ececee;
76
+ --pf-text-2: #b6b8be;
77
+ --pf-text-3: #8f939a;
78
+ --pf-accent: #7c9cff;
79
+ --pf-accent-hover: #9bb3ff;
80
+ --pf-on-accent: #0c1224;
81
+ --pf-accent-soft: #1c2442;
82
+ --pf-accent-ink: #a9bdff;
83
+ --pf-grant: #5cc995;
84
+ --pf-grant-soft: #13291e;
85
+ --pf-grant-border: #25553b;
86
+ --pf-block: #ff8a80;
87
+ --pf-block-soft: #2e1614;
88
+ --pf-block-border: #5f2b27;
89
+ --pf-mask: #b9a3fb;
90
+ --pf-mask-soft: #231c3b;
91
+ --pf-mask-border: #44377b;
92
+ --pf-restrict: #f6a85f;
93
+ --pf-restrict-soft: #2c1d10;
94
+ --pf-restrict-border: #5f3b1a;
95
+ --pf-warn: #eab64e;
96
+ --pf-warn-soft: #2a2210;
97
+ --pf-warn-border: #56441a;
98
+ --pf-overlay: rgba(0, 0, 0, .6);
99
+ --pf-shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 8px 24px rgba(0, 0, 0, .4);
100
+ color-scheme: dark;
101
+ }
102
+
103
+ @media (prefers-color-scheme: dark) {
104
+ .pf-root[data-theme="auto"] {
105
+ --pf-bg: #111214;
106
+ --pf-surface: #18191c;
107
+ --pf-surface-2: #212327;
108
+ --pf-border: #2c2e33;
109
+ --pf-border-strong: #3c3f45;
110
+ --pf-text: #ececee;
111
+ --pf-text-2: #b6b8be;
112
+ --pf-text-3: #8f939a;
113
+ --pf-accent: #7c9cff;
114
+ --pf-accent-hover: #9bb3ff;
115
+ --pf-on-accent: #0c1224;
116
+ --pf-accent-soft: #1c2442;
117
+ --pf-accent-ink: #a9bdff;
118
+ --pf-grant: #5cc995;
119
+ --pf-grant-soft: #13291e;
120
+ --pf-grant-border: #25553b;
121
+ --pf-block: #ff8a80;
122
+ --pf-block-soft: #2e1614;
123
+ --pf-block-border: #5f2b27;
124
+ --pf-mask: #b9a3fb;
125
+ --pf-mask-soft: #231c3b;
126
+ --pf-mask-border: #44377b;
127
+ --pf-restrict: #f6a85f;
128
+ --pf-restrict-soft: #2c1d10;
129
+ --pf-restrict-border: #5f3b1a;
130
+ --pf-warn: #eab64e;
131
+ --pf-warn-soft: #2a2210;
132
+ --pf-warn-border: #56441a;
133
+ --pf-overlay: rgba(0, 0, 0, .6);
134
+ --pf-shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 8px 24px rgba(0, 0, 0, .4);
135
+ color-scheme: dark;
136
+ }
137
+ }
138
+
139
+ /* ------------------------------------------------------------- base reset */
140
+ .pf-root { margin: 0; background: var(--pf-bg); color: var(--pf-text); font-family: var(--pf-font); font-size: 14px; line-height: 1.5; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
141
+ .pf-root body { margin: 0; background: var(--pf-bg); }
142
+ .pf-root *, .pf-root *::before, .pf-root *::after { box-sizing: border-box; }
143
+ .pf-root :where(h1, h2, h3, h4, p, ul, ol, dl, dd, figure, pre, fieldset) { margin: 0; }
144
+ .pf-root :where(ul, ol) { padding: 0; list-style: none; }
145
+ .pf-root :where(fieldset) { border: 0; padding: 0; min-width: 0; }
146
+ .pf-root :where(legend) { padding: 0; }
147
+ .pf-root :where(input, select, textarea, button) { font: inherit; color: inherit; letter-spacing: inherit; }
148
+ .pf-root :where(button) { cursor: pointer; }
149
+ .pf-root :where(button, input, select):disabled { cursor: not-allowed; }
150
+ .pf-root :focus-visible { outline: 2px solid var(--pf-accent); outline-offset: 2px; }
151
+ .pf-root :where(a) { color: var(--pf-accent-ink); text-decoration: none; }
152
+ .pf-root :where(a):hover { color: var(--pf-accent); text-decoration: underline; }
153
+ .pf-root :where(code, kbd, pre), .pf-mono { font-family: var(--pf-mono); }
154
+ .pf-root :where(img, svg) { display: block; max-width: 100%; }
155
+ .pf-root [hidden] { display: none !important; }
156
+ .pf-root :where(table) { border-collapse: collapse; }
157
+ @media (prefers-reduced-motion: reduce) {
158
+ .pf-root *, .pf-root *::before, .pf-root *::after { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
159
+ }
160
+
161
+ .pf-sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
162
+ .pf-skip { position: absolute; left: 8px; top: -40px; z-index: 100; padding: 8px 12px; background: var(--pf-text); color: var(--pf-surface) !important; border-radius: var(--pf-radius-sm); }
163
+ .pf-skip:focus { top: 8px; }
164
+
165
+ /* ----------------------------------------------------------------- utility */
166
+ .pf-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
167
+ .pf-row--top { align-items: flex-start; }
168
+ .pf-row--end { align-items: flex-end; }
169
+ .pf-row--between { justify-content: space-between; }
170
+ .pf-row--right { justify-content: flex-end; }
171
+ .pf-row--nowrap { flex-wrap: nowrap; }
172
+ .pf-row--tight { gap: 4px; }
173
+ .pf-row--loose { gap: 16px; }
174
+ .pf-stack { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
175
+ .pf-stack--tight { gap: 4px; }
176
+ .pf-stack--loose { gap: 16px; }
177
+ .pf-stack--xl { gap: 20px; }
178
+ .pf-grow { flex: 1 1 auto; min-width: 0; }
179
+ .pf-gap-6 { gap: 6px; }
180
+ .pf-gap-12 { gap: 12px; }
181
+ .pf-mt-4 { margin-top: 4px; }
182
+ .pf-mt-8 { margin-top: 8px; }
183
+ .pf-mt-12 { margin-top: 12px; }
184
+ .pf-mt-16 { margin-top: 16px; }
185
+ .pf-mt-24 { margin-top: 24px; }
186
+ .pf-ml-auto { margin-left: auto; }
187
+ .pf-text-2 { color: var(--pf-text-2); }
188
+ .pf-text-3 { color: var(--pf-text-3); }
189
+ .pf-text-warn { color: var(--pf-warn); }
190
+ .pf-text-block { color: var(--pf-block); }
191
+ .pf-text-grant { color: var(--pf-grant); }
192
+ .pf-text-mask { color: var(--pf-mask); }
193
+ .pf-text-restrict { color: var(--pf-restrict); }
194
+ .pf-text-accent { color: var(--pf-accent-ink); }
195
+ .pf-fs-11 { font-size: 11px; }
196
+ .pf-fs-12 { font-size: 12px; }
197
+ .pf-fs-13 { font-size: 13px; }
198
+ .pf-fs-15 { font-size: 15px; }
199
+ .pf-fw-500 { font-weight: 500; }
200
+ .pf-fw-600 { font-weight: 600; }
201
+ .pf-nowrap { white-space: nowrap; }
202
+ .pf-ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
203
+ .pf-strike { text-decoration: line-through; }
204
+ .pf-tabular { font-variant-numeric: tabular-nums; }
205
+ .pf-balance { text-wrap: balance; }
206
+ .pf-mono-key { font-family: var(--pf-mono); font-size: 12px; color: var(--pf-text-3); }
207
+ .pf-eyebrow { font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--pf-text-3); }
208
+ .pf-divider { height: 1px; background: var(--pf-border); border: 0; margin: 0; }
209
+ .pf-hint { font-size: 12px; color: var(--pf-text-3); }
210
+ .pf-dot { display: inline-block; width: 7px; height: 7px; border-radius: 999px; background: var(--pf-warn); flex: 0 0 7px; }
211
+ .pf-dot--grant { background: var(--pf-grant); }
212
+
213
+ /* ------------------------------------------------------------------ layout */
214
+ .pf-app { min-height: 100vh; display: flex; flex-wrap: wrap; align-content: flex-start; background: var(--pf-bg); }
215
+ .pf-sidebar { flex: 0 0 248px; align-self: stretch; border-right: 1px solid var(--pf-border); background: var(--pf-surface); }
216
+ .pf-sidebar__inner { position: sticky; top: 0; height: 100vh; overflow: auto; display: flex; flex-direction: column; gap: 2px; padding: 20px 12px; }
217
+ .pf-brand { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; padding: 4px 8px 18px; }
218
+ .pf-brand__home { display: inline-block; }
219
+ .pf-back-link { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 500; color: var(--pf-text-2) !important; text-decoration: none !important; border-radius: var(--pf-radius-sm); }
220
+ .pf-back-link:hover { color: var(--pf-text) !important; text-decoration: underline !important; }
221
+ .pf-brand__logo { height: 26px; width: auto; }
222
+ .pf-brand__logo--dark { display: none; }
223
+ .pf-root[data-theme="dark"] .pf-brand__logo--light { display: none; }
224
+ .pf-root[data-theme="dark"] .pf-brand__logo--dark { display: block; }
225
+ @media (prefers-color-scheme: dark) {
226
+ .pf-root[data-theme="auto"] .pf-brand__logo--light { display: none; }
227
+ .pf-root[data-theme="auto"] .pf-brand__logo--dark { display: block; }
228
+ }
229
+ .pf-nav { display: flex; flex-direction: column; gap: 2px; }
230
+ .pf-nav__link { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 10px; border-radius: var(--pf-radius-sm); color: var(--pf-text-2) !important; font-weight: 500; text-decoration: none !important; }
231
+ .pf-nav__link:hover { background: var(--pf-surface-2); color: var(--pf-text) !important; }
232
+ .pf-nav__link[aria-current="page"] { background: var(--pf-surface-2); color: var(--pf-text) !important; font-weight: 600; }
233
+ .pf-nav__label { flex: 1; min-width: 0; }
234
+ .pf-nav__badge { min-width: 22px; padding: 0 7px; border-radius: 999px; background: var(--pf-warn-soft); color: var(--pf-warn); border: 1px solid var(--pf-warn-border); font-size: 12px; font-weight: 600; text-align: center; line-height: 18px; }
235
+ .pf-sidebar__footer { margin-top: auto; display: flex; flex-direction: column; gap: 12px; padding: 16px 8px 0; }
236
+ .pf-sidebar__label { font-size: 12px; color: var(--pf-text-3); }
237
+ .pf-main { flex: 1 1 640px; min-width: 0; padding: 32px clamp(16px, 4vw, 48px) 96px; }
238
+ .pf-main__inner { max-width: 1120px; margin: 0 auto; display: flex; flex-direction: column; gap: 20px; }
239
+ .pf-topbar { display: none; }
240
+
241
+ @media (max-width: 1023px) {
242
+ .pf-sidebar { flex: 1 1 100%; border-right: 0; border-bottom: 1px solid var(--pf-border); }
243
+ .pf-sidebar__inner { position: static; height: auto; }
244
+ .pf-sidebar__footer { margin-top: 12px; flex-direction: row; flex-wrap: wrap; align-items: center; }
245
+ }
246
+ @media (max-width: 640px) {
247
+ .pf-sidebar { display: none; }
248
+ .pf-topbar { display: block; flex: 1 1 100%; background: var(--pf-surface); border-bottom: 1px solid var(--pf-border); }
249
+ .pf-topbar > summary { list-style: none; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; cursor: pointer; }
250
+ .pf-topbar > summary::-webkit-details-marker { display: none; }
251
+ .pf-topbar__menu { padding: 4px 12px 16px; display: flex; flex-direction: column; gap: 12px; }
252
+ .pf-main { padding: 20px 16px 72px; }
253
+ }
254
+
255
+ /* -------------------------------------------------------------- page header */
256
+ .pf-page-header { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: flex-end; gap: 16px; }
257
+ .pf-page-header__text { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1 1 320px; }
258
+ .pf-h1 { font-size: 24px; line-height: 1.25; font-weight: 600; letter-spacing: -0.01em; overflow-wrap: anywhere; }
259
+ .pf-h2 { font-size: 15px; font-weight: 600; line-height: 1.35; }
260
+ .pf-h3 { font-size: 15px; font-weight: 600; }
261
+ .pf-drawer-title { font-size: 18px; font-weight: 600; line-height: 1.3; }
262
+ .pf-subtitle { font-size: 14px; color: var(--pf-text-2); }
263
+ .pf-breadcrumb { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 13px; color: var(--pf-text-3); }
264
+ .pf-breadcrumb a { color: var(--pf-text-2); }
265
+ .pf-impact { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--pf-text-2); }
266
+
267
+ /* ---------------------------------------------------------------- buttons */
268
+ .pf-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 14px; border-radius: var(--pf-radius-sm); border: 1px solid var(--pf-border-strong); background: var(--pf-surface); color: var(--pf-text) !important; font-size: 14px; font-weight: 500; line-height: 1.3; text-decoration: none !important; white-space: nowrap; cursor: pointer; }
269
+ .pf-btn:hover { background: var(--pf-surface-2); color: var(--pf-text) !important; }
270
+ .pf-btn--sm { padding: 7px 12px; font-size: 13px; }
271
+ .pf-btn--xs { padding: 5px 10px; font-size: 13px; }
272
+ .pf-btn--primary { background: var(--pf-accent); border-color: var(--pf-accent); color: var(--pf-on-accent) !important; }
273
+ .pf-btn--primary:hover { background: var(--pf-accent-hover); border-color: var(--pf-accent-hover); color: var(--pf-on-accent) !important; }
274
+ .pf-btn--ghost { background: transparent; border-color: transparent; color: var(--pf-text-2) !important; padding: 5px 10px; }
275
+ .pf-btn--ghost:hover { background: var(--pf-surface-2); }
276
+ .pf-btn--danger { border-color: var(--pf-block-border); color: var(--pf-block) !important; }
277
+ .pf-btn--danger:hover { background: var(--pf-block-soft); color: var(--pf-block) !important; }
278
+ .pf-btn--ghost-danger { background: transparent; border-color: transparent; color: var(--pf-block) !important; padding: 5px 10px; }
279
+ .pf-btn--ghost-danger:hover { background: var(--pf-block-soft); }
280
+ .pf-btn--danger-solid { background: var(--pf-block); border-color: var(--pf-block); color: #fff !important; font-weight: 600; }
281
+ .pf-btn--danger-solid:hover { background: var(--pf-block); color: #fff !important; filter: brightness(.94); }
282
+ .pf-root[data-theme="dark"] .pf-btn--danger-solid { color: #1b0b09 !important; }
283
+ @media (prefers-color-scheme: dark) { .pf-root[data-theme="auto"] .pf-btn--danger-solid { color: #1b0b09 !important; } }
284
+ .pf-btn--link { padding: 0; border: 0; background: none; color: var(--pf-accent-ink) !important; text-decoration: underline !important; font-weight: 500; }
285
+ .pf-btn--link:hover { background: none; color: var(--pf-accent) !important; }
286
+ .pf-btn--inverse { background: var(--pf-surface); color: var(--pf-text) !important; border-color: var(--pf-surface); font-weight: 600; }
287
+ .pf-btn--on-dark { background: transparent; color: var(--pf-surface) !important; border-color: currentColor; }
288
+ .pf-btn--on-dark:hover { background: rgba(127, 127, 127, .2); color: var(--pf-surface) !important; }
289
+ .pf-btn[disabled], .pf-btn[aria-disabled="true"] { opacity: .5; pointer-events: none; }
290
+ .pf-btn--fixed { min-width: 88px; }
291
+ .pf-inline-form { display: inline; margin: 0; }
292
+ .pf-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
293
+
294
+ /* ----------------------------------------------------------------- inputs */
295
+ .pf-input, .pf-select, .pf-textarea { display: block; width: 100%; padding: 8px 10px; border: 1px solid var(--pf-border-strong); border-radius: var(--pf-radius-sm); background: var(--pf-surface); color: var(--pf-text); font-size: 14px; line-height: 1.4; }
296
+ /* Selects: one look on every browser/OS. The chevron is served by the engine
297
+ (same origin), so strict CSP img-src 'self' allows it. */
298
+ .pf-root .pf-select { -webkit-appearance: none; appearance: none; padding-right: 34px; background-color: var(--pf-surface); background-image: url("chevron.svg"); background-repeat: no-repeat; background-position: right 10px center; background-size: 16px 16px; cursor: pointer; text-overflow: ellipsis; box-shadow: 0 1px 1px rgba(16, 18, 22, .03); }
299
+ .pf-root .pf-select:hover { border-color: var(--pf-text-3); }
300
+ .pf-root .pf-select:focus { outline: none; border-color: var(--pf-accent); box-shadow: 0 0 0 3px var(--pf-accent-soft); }
301
+ .pf-root .pf-select:disabled { opacity: .6; cursor: not-allowed; background-color: var(--pf-surface-2); }
302
+ .pf-root .pf-select::-ms-expand { display: none; }
303
+ .pf-root .pf-select.pf-select--sm { padding-right: 30px; background-position: right 8px center; }
304
+ .pf-root .pf-select.pf-select--rel { background-color: var(--pf-accent-soft); }
305
+ .pf-root .pf-input:hover, .pf-root .pf-textarea:hover { border-color: var(--pf-text-3); }
306
+ .pf-root .pf-input:focus, .pf-root .pf-textarea:focus { outline: none; border-color: var(--pf-accent); box-shadow: 0 0 0 3px var(--pf-accent-soft); }
307
+ .pf-input--sm, .pf-select--sm { padding: 6px 10px; font-size: 13px; }
308
+ .pf-input--lg { padding: 10px 12px; font-size: 15px; }
309
+ .pf-input--auto, .pf-select--auto { display: inline-block; width: auto; max-width: 100%; }
310
+ .pf-textarea { min-height: 72px; resize: vertical; }
311
+ .pf-textarea--mono { font-family: var(--pf-mono); font-size: 13px; max-width: 360px; }
312
+ .pf-input[aria-invalid="true"], .pf-select[aria-invalid="true"], .pf-textarea[aria-invalid="true"] { border-color: var(--pf-block); }
313
+ .pf-input::placeholder, .pf-textarea::placeholder { color: var(--pf-text-3); }
314
+ .pf-label { display: block; font-size: 13px; font-weight: 600; color: var(--pf-text); margin-bottom: 6px; }
315
+ .pf-form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); gap: 12px 16px; align-items: end; }
316
+ .pf-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
317
+ .pf-field__error, .pf-error { font-size: 13px; color: var(--pf-block); }
318
+ .pf-error::before { content: "✕ "; }
319
+ .pf-checkbox { display: inline-flex; align-items: flex-start; gap: 8px; cursor: pointer; }
320
+ .pf-checkbox input { margin: 3px 0 0; accent-color: var(--pf-accent); width: 16px; height: 16px; flex: 0 0 16px; }
321
+ .pf-search { position: relative; }
322
+ .pf-title-input { width: 100%; font-size: 24px; line-height: 1.25; font-weight: 600; letter-spacing: -0.01em; border: 1px solid transparent; border-radius: var(--pf-radius-sm); background: transparent; margin-left: -8px; padding: 4px 8px; }
323
+ .pf-title-input:hover { border-color: var(--pf-border); }
324
+ .pf-title-input:focus { border-color: var(--pf-border-strong); background: var(--pf-surface); }
325
+ .pf-desc-input { width: 100%; font-size: 14px; color: var(--pf-text-2); border: 1px solid transparent; border-radius: var(--pf-radius-sm); background: transparent; margin-left: -8px; padding: 3px 8px; }
326
+ .pf-desc-input:hover { border-color: var(--pf-border); }
327
+ .pf-desc-input:focus { border-color: var(--pf-border-strong); background: var(--pf-surface); color: var(--pf-text); }
328
+
329
+ /* switch: <label class="pf-switch"><input type="checkbox" role="switch"><span class="pf-switch__track"></span>Text</label> */
330
+ .pf-switch { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border: 1px solid var(--pf-border-strong); border-radius: var(--pf-radius-sm); background: var(--pf-surface); font-size: 13px; font-weight: 500; cursor: pointer; white-space: nowrap; }
331
+ .pf-switch--bare { border: 0; padding: 0; background: none; }
332
+ .pf-switch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
333
+ .pf-switch__track { position: relative; width: 34px; height: 18px; flex: 0 0 34px; border-radius: 999px; background: var(--pf-border-strong); }
334
+ .pf-switch__track::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 999px; background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, .2); transition: left .12s ease; }
335
+ .pf-switch input:checked + .pf-switch__track { background: var(--pf-grant); }
336
+ .pf-switch input:checked + .pf-switch__track::after { left: 18px; }
337
+ .pf-switch input:focus-visible + .pf-switch__track { outline: 2px solid var(--pf-accent); outline-offset: 2px; }
338
+ .pf-switch input:disabled + .pf-switch__track { opacity: .5; }
339
+ .pf-switch__on { display: none; }
340
+ .pf-switch input:checked ~ .pf-switch__on { display: inline; }
341
+ .pf-switch input:checked ~ .pf-switch__off { display: none; }
342
+
343
+ /* ------------------------------------------------------------------- cards */
344
+ .pf-card { background: var(--pf-surface); border: 1px solid var(--pf-border); border-radius: var(--pf-radius-lg); overflow: hidden; min-width: 0; }
345
+ .pf-card--visible { overflow: visible; }
346
+ .pf-card__header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; padding: 16px 18px; }
347
+ .pf-card__body { padding: 16px 18px; }
348
+ .pf-card__body--flush { padding: 0; }
349
+ .pf-card__desc { font-size: 13px; color: var(--pf-text-2); }
350
+ .pf-card__footer { padding: 12px 18px; border-top: 1px solid var(--pf-border); display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
351
+ .pf-card__footer--muted { background: var(--pf-surface-2); font-size: 13px; color: var(--pf-text-2); }
352
+ .pf-card__row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; padding: 10px 18px; border-top: 1px solid var(--pf-border); }
353
+ .pf-card__row--top { align-items: flex-start; }
354
+ .pf-card__row--link { color: inherit !important; text-decoration: none !important; }
355
+ .pf-card__row--link:hover { background: var(--pf-surface-2); }
356
+ .pf-panel { border: 1px solid var(--pf-border); border-radius: var(--pf-radius); padding: 12px 14px; background: var(--pf-surface); }
357
+ .pf-panel--muted { background: var(--pf-surface-2); }
358
+ .pf-panel--mask { background: var(--pf-mask-soft); border-color: var(--pf-mask-border); }
359
+ .pf-panel--warn { background: var(--pf-warn-soft); border-color: var(--pf-warn-border); }
360
+ .pf-panel--grant { background: var(--pf-grant-soft); border-color: var(--pf-grant-border); color: var(--pf-grant); }
361
+ .pf-panel--block { background: var(--pf-block-soft); border-color: var(--pf-block-border); color: var(--pf-block); }
362
+ .pf-masonry { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-start; }
363
+ .pf-masonry__col { flex: 1 1 min(100%, 460px); min-width: 0; display: flex; flex-direction: column; gap: 16px; }
364
+ .pf-grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); gap: 16px; align-items: start; }
365
+ .pf-grid-3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr)); gap: 12px; align-items: start; }
366
+ .pf-scroll-x { overflow-x: auto; }
367
+ .pf-scroll-y { max-height: 260px; overflow-y: auto; }
368
+ .pf-count-pill { display: inline-flex; align-items: center; min-width: 22px; padding: 0 8px; border-radius: 999px; font-size: 12px; font-weight: 600; line-height: 20px; justify-content: center; background: var(--pf-surface-2); color: var(--pf-text-2); border: 1px solid var(--pf-border); }
369
+ .pf-count-pill--warn { background: var(--pf-warn-soft); color: var(--pf-warn); border-color: var(--pf-warn-border); }
370
+
371
+ /* ------------------------------------------------------------------ badges */
372
+ .pf-badge { display: inline-flex; align-items: center; gap: 4px; padding: 0 7px; border-radius: 999px; font-size: 11px; font-weight: 600; line-height: 18px; border: 1px solid var(--pf-border); background: var(--pf-surface-2); color: var(--pf-text-2); white-space: nowrap; vertical-align: middle; }
373
+ .pf-root a.pf-badge:hover { text-decoration: none; filter: brightness(.97); }
374
+ .pf-badge--muting { background: var(--pf-restrict-soft); color: var(--pf-restrict); border-color: var(--pf-restrict-border); }
375
+ .pf-badge--conditional { background: var(--pf-accent-soft); color: var(--pf-accent-ink); border-color: var(--pf-border); }
376
+ .pf-badge--warn, .pf-badge--sensitive, .pf-badge--failopen { background: var(--pf-warn-soft); color: var(--pf-warn); border-color: var(--pf-warn-border); }
377
+ .pf-badge--column-missing { background: transparent; color: var(--pf-block); border-color: var(--pf-block-border); }
378
+ .pf-badge--unconfigured { background: transparent; color: var(--pf-warn); border: 1px dashed var(--pf-warn-border); }
379
+ .pf-badge--changed { background: var(--pf-accent-soft); color: var(--pf-accent-ink); border-color: var(--pf-accent-soft); }
380
+ .pf-badge--secure, .pf-badge--grant { background: var(--pf-grant-soft); color: var(--pf-grant); border-color: var(--pf-grant-border); }
381
+ .pf-badge--block { background: var(--pf-block-soft); color: var(--pf-block); border-color: var(--pf-block-border); }
382
+ .pf-badge--mask { background: var(--pf-mask-soft); color: var(--pf-mask); border-color: var(--pf-mask-border); }
383
+ .pf-badge--off { background: transparent; border-color: transparent; color: var(--pf-text-3); font-weight: 500; padding: 0 2px; }
384
+ .pf-badge--square { border-radius: var(--pf-radius-xs); }
385
+ .pf-badge--wrap { white-space: normal; }
386
+ .pf-badge--inverse { background: var(--pf-text); color: var(--pf-surface); border-color: var(--pf-text); font-weight: 700; letter-spacing: .05em; text-transform: uppercase; }
387
+ .pf-lock { width: 11px; height: 11px; flex: 0 0 11px; }
388
+
389
+ /* Permission Set chip */
390
+ .pf-chip { display: inline-flex; align-items: center; gap: 4px; max-width: 100%; padding: 0 8px; border-radius: 999px; font-size: 12px; font-weight: 500; line-height: 20px; border: 1px solid var(--pf-border); background: var(--pf-surface-2); color: var(--pf-text) !important; white-space: nowrap; text-decoration: none !important; vertical-align: middle; }
391
+ .pf-chip > span:first-child { overflow: hidden; text-overflow: ellipsis; }
392
+ .pf-root a.pf-chip:hover { border-color: var(--pf-border-strong); }
393
+ .pf-chip--lg { padding: 1px 10px; font-size: 13px; font-weight: 600; line-height: 22px; }
394
+ .pf-chip--muting { background: var(--pf-restrict-soft); color: var(--pf-restrict) !important; border-color: var(--pf-restrict-border); }
395
+ .pf-chip--inactive, .pf-chip--expired { background: transparent; color: var(--pf-text-3) !important; border-color: var(--pf-border-strong); }
396
+ .pf-chip--expired > span:first-child { text-decoration: line-through; }
397
+ .pf-chip__suffix { font-weight: 500; text-decoration: none; }
398
+ .pf-chips { display: flex; flex-wrap: wrap; gap: 4px 6px; min-width: 0; }
399
+
400
+ /* --------------------------------------------------------------- filters */
401
+ .pf-filters { display: flex; flex-wrap: wrap; gap: 6px; }
402
+ .pf-filter { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; font-size: 13px; font-weight: 500; border: 1px solid var(--pf-border-strong); background: var(--pf-surface); color: var(--pf-text-2) !important; text-decoration: none !important; white-space: nowrap; }
403
+ .pf-filter:hover { background: var(--pf-surface-2); }
404
+ .pf-filter[aria-pressed="true"], .pf-filter[aria-current="true"] { background: var(--pf-text); border-color: var(--pf-text); color: var(--pf-surface) !important; }
405
+ .pf-filter__count { opacity: .75; }
406
+
407
+ /* ------------------------------------------------------------------- tabs */
408
+ .pf-tabs { display: flex; flex-wrap: wrap; gap: 4px 20px; border-bottom: 1px solid var(--pf-border); }
409
+ .pf-tab { padding: 8px 2px; margin-bottom: -1px; border-bottom: 2px solid transparent; color: var(--pf-text-2) !important; font-weight: 600; text-decoration: none !important; white-space: nowrap; }
410
+ .pf-tab:hover { color: var(--pf-text) !important; }
411
+ .pf-tab[aria-selected="true"] { color: var(--pf-text) !important; border-bottom-color: var(--pf-text); }
412
+
413
+ /* ----------------------------------------------------------- data tables */
414
+ .pf-table { width: 100%; min-width: 0; }
415
+ .pf-table__head, .pf-table__row { display: grid; gap: 8px 12px; align-items: center; padding: 10px 18px; }
416
+ .pf-table__head { font-size: 12px; font-weight: 600; color: var(--pf-text-3); border-bottom: 1px solid var(--pf-border); padding-top: 12px; padding-bottom: 8px; }
417
+ .pf-table__row { border-top: 1px solid var(--pf-border); color: inherit !important; text-decoration: none !important; }
418
+ .pf-table__head + .pf-table__row { border-top: 0; }
419
+ .pf-root a.pf-table__row:hover, .pf-table__row--link:hover, .pf-table__row--link:focus-within { background: var(--pf-surface-2); }
420
+ /* The row is one target: the link in its first cell is stretched over it. */
421
+ .pf-table__row--link { position: relative; }
422
+ .pf-root .pf-table__row--link a.pf-cell-person, .pf-root .pf-table__row--link a.pf-table__link { color: inherit; text-decoration: none; }
423
+ .pf-table__row--link a.pf-cell-person::after, .pf-table__row--link a.pf-table__link::after { content: ''; position: absolute; inset: 0; }
424
+ .pf-table--people .pf-table__head, .pf-table--people .pf-table__row { grid-template-columns: minmax(0, 1.2fr) minmax(0, 2fr) minmax(110px, .8fr); }
425
+ .pf-table--sets { min-width: 560px; }
426
+ .pf-table--sets .pf-table__head, .pf-table--sets .pf-table__row { grid-template-columns: minmax(0, 2fr) 56px 76px minmax(0, .8fr) auto; }
427
+ .pf-table--assignments { min-width: 640px; }
428
+ .pf-table--assignments .pf-table__head, .pf-table--assignments .pf-table__row { grid-template-columns: 24px minmax(0, 1.5fr) minmax(0, 1.2fr) minmax(0, 1fr) auto; }
429
+ .pf-table--audit { min-width: 640px; }
430
+ .pf-table--audit .pf-table__head, .pf-table--audit .pf-table__row { grid-template-columns: 110px minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr); }
431
+ .pf-cell-person { display: flex; align-items: center; gap: 10px; min-width: 0; }
432
+ .pf-avatar { display: inline-grid; place-items: center; width: 30px; height: 30px; flex: 0 0 30px; border-radius: 999px; background: var(--pf-surface-2); border: 1px solid var(--pf-border); font-size: 11px; font-weight: 600; color: var(--pf-text-2); }
433
+ .pf-avatar--sm { width: 28px; height: 28px; flex-basis: 28px; }
434
+ .pf-avatar--lg { width: 48px; height: 48px; flex-basis: 48px; font-size: 16px; }
435
+ .pf-person-name { font-weight: 600; }
436
+ .pf-person-email { font-size: 12px; color: var(--pf-text-3); }
437
+
438
+ /* ------------------------------------------------------------- matrices */
439
+ .pf-matrix { min-width: 620px; }
440
+ .pf-matrix__head, .pf-matrix__row { display: grid; grid-template-columns: minmax(130px, 1.3fr) repeat(4, minmax(72px, 1fr)) minmax(120px, 1.2fr) 28px; gap: 8px; align-items: center; padding: 10px 16px; }
441
+ .pf-matrix__head { font-size: 12px; font-weight: 600; color: var(--pf-text-3); border-bottom: 1px solid var(--pf-border); }
442
+ .pf-matrix__row { border-top: 1px solid var(--pf-border); }
443
+ .pf-matrix__head + .pf-matrix__row { border-top: 0; }
444
+ .pf-matrix__resource { display: flex; flex-direction: column; min-width: 0; }
445
+ .pf-matrix__resource a { font-weight: 600; color: var(--pf-text) !important; }
446
+ .pf-matrix__chev { color: var(--pf-text-3) !important; font-size: 16px; text-align: center; text-decoration: none !important; }
447
+ .pf-effective { min-width: 540px; }
448
+ .pf-effective .pf-matrix__head, .pf-effective .pf-matrix__row { grid-template-columns: minmax(120px, 1.3fr) repeat(4, minmax(66px, 1fr)) minmax(100px, 1.2fr); }
449
+ .pf-effective__group { border-top: 1px solid var(--pf-border); }
450
+ .pf-effective__group:first-of-type { border-top: 0; }
451
+ .pf-effective__group > summary { list-style: none; cursor: pointer; }
452
+ .pf-effective__group > summary::-webkit-details-marker { display: none; }
453
+ .pf-effective__group > summary .pf-matrix__row { border-top: 0; }
454
+ .pf-disclosure { display: inline-block; color: var(--pf-text-3); transition: transform .12s ease; }
455
+ .pf-root details[open] > summary .pf-disclosure, .pf-disclosure--open { transform: rotate(90deg); }
456
+ .pf-effective__detail { padding: 4px 14px 14px 32px; display: flex; flex-direction: column; gap: 6px; }
457
+ .pf-effective__field { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; padding: 6px 8px; border-radius: var(--pf-radius-sm); color: inherit !important; text-decoration: none !important; }
458
+ .pf-root a.pf-effective__field:hover { background: var(--pf-surface-2); }
459
+
460
+ /* Access toggle cell: <label class="pf-cell"><input type=checkbox><span class="pf-cell__on">✓ Allowed</span><span class="pf-cell__off">– No</span></label> */
461
+ .pf-cell { position: relative; display: flex; align-items: center; justify-content: center; gap: 4px; min-height: 34px; padding: 4px 6px; border-radius: var(--pf-radius-sm); border: 1px solid var(--pf-border-strong); background: transparent; color: var(--pf-text-2); font-size: 13px; font-weight: 500; text-align: center; cursor: pointer; text-decoration: none !important; user-select: none; }
462
+ .pf-cell--lg { min-height: 36px; }
463
+ .pf-cell--sm { min-height: 32px; cursor: default; }
464
+ .pf-cell input { position: absolute; opacity: 0; inset: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
465
+ .pf-cell__glyph { font-weight: 700; }
466
+ .pf-cell__on { display: none; }
467
+ .pf-cell:has(input:checked) { background: var(--pf-grant-soft); color: var(--pf-grant); border-color: var(--pf-grant-border); }
468
+ .pf-cell:has(input:checked) .pf-cell__on { display: inline-flex; gap: 4px; }
469
+ .pf-cell:has(input:checked) .pf-cell__off { display: none; }
470
+ .pf-cell:has(input:focus-visible) { outline: 2px solid var(--pf-accent); outline-offset: 2px; }
471
+ .pf-cell--muting:has(input:checked) { background: var(--pf-block-soft); color: var(--pf-block); border-color: var(--pf-block-border); }
472
+ .pf-cell--muting { color: var(--pf-text-3); border-color: var(--pf-border); }
473
+ .pf-cell__off { display: inline-flex; gap: 4px; }
474
+ .pf-cell:has(input:disabled) { opacity: .6; cursor: not-allowed; border-color: var(--pf-border); color: var(--pf-text-3); }
475
+ .pf-cell:has(input:disabled) input { cursor: not-allowed; }
476
+ .pf-cell__needs { display: none; }
477
+ .pf-cell[data-pf-needs="true"] .pf-cell__off { display: none; }
478
+ .pf-cell[data-pf-needs="true"] .pf-cell__needs { display: inline-flex; gap: 4px; }
479
+ /* read-only state cells (person page, across-sets) */
480
+ .pf-state { display: inline-flex; align-items: center; justify-content: center; gap: 4px; min-height: 32px; padding: 4px 6px; border-radius: var(--pf-radius-sm); border: 1px solid var(--pf-border); color: var(--pf-text-3); font-size: 13px; font-weight: 500; text-decoration: none !important; }
481
+ .pf-root a.pf-state:hover { border-color: var(--pf-border-strong); }
482
+ .pf-state--granted { background: var(--pf-grant-soft); color: var(--pf-grant) !important; border-color: var(--pf-grant-border); }
483
+ .pf-state--blocked { background: var(--pf-block-soft); color: var(--pf-block) !important; border-color: var(--pf-block-border); }
484
+ .pf-state--none { color: var(--pf-text-3) !important; }
485
+ .pf-state[aria-current="true"], .pf-effective__field[aria-current="true"], .pf-action-pill[aria-current="true"] { box-shadow: 0 0 0 2px var(--pf-accent); }
486
+ .pf-state .pf-cell__glyph { font-weight: 700; }
487
+ .pf-state--records { flex-direction: column; align-items: flex-start; justify-content: center; gap: 0; min-width: 0; width: 100%; overflow: hidden; text-align: left; }
488
+
489
+ /* 4-level field control */
490
+ .pf-levels { display: inline-flex; flex-wrap: wrap; gap: 2px; padding: 2px; border-radius: var(--pf-radius-sm); background: var(--pf-surface-2); border: 1px solid var(--pf-border); }
491
+ .pf-levels--lg .pf-level__label { padding: 6px 12px; }
492
+ .pf-level { position: relative; display: inline-flex; }
493
+ .pf-level input { position: absolute; opacity: 0; width: 1px; height: 1px; }
494
+ .pf-level__label { display: inline-block; padding: 4px 9px; border-radius: var(--pf-radius-xs); font-size: 13px; font-weight: 500; color: var(--pf-text-2); cursor: pointer; white-space: nowrap; }
495
+ .pf-level__label:hover { background: var(--pf-surface); }
496
+ .pf-level input:checked + .pf-level__label { background: var(--pf-surface); font-weight: 600; box-shadow: 0 0 0 1px var(--pf-border-strong), 0 1px 2px rgba(0, 0, 0, .06); color: var(--pf-text); }
497
+ .pf-level input:focus-visible + .pf-level__label { outline: 2px solid var(--pf-accent); outline-offset: 1px; }
498
+ .pf-level input:disabled + .pf-level__label { opacity: .45; cursor: not-allowed; }
499
+ .pf-level--hidden input:checked + .pf-level__label, .pf-level--leave input:checked + .pf-level__label { color: var(--pf-text-2); }
500
+ .pf-level--masked input:checked + .pf-level__label, .pf-level--mask input:checked + .pf-level__label, .pf-level--mask .pf-level__label { color: var(--pf-mask); }
501
+ .pf-level--edit input:checked + .pf-level__label { color: var(--pf-grant); }
502
+ .pf-level--hide .pf-level__label { color: var(--pf-block); }
503
+ .pf-level-badge { display: inline-flex; align-items: center; gap: 4px; padding: 1px 8px; border-radius: 5px; font-size: 12px; font-weight: 600; border: 1px solid var(--pf-border); white-space: nowrap; }
504
+ .pf-level-badge--hidden { color: var(--pf-text-3); background: transparent; }
505
+ .pf-level-badge--masked, .pf-level-badge--mask { color: var(--pf-mask); background: var(--pf-mask-soft); border-color: var(--pf-mask-border); }
506
+ .pf-level-badge--read { color: var(--pf-text); background: var(--pf-surface-2); }
507
+ .pf-level-badge--edit { color: var(--pf-grant); background: var(--pf-grant-soft); border-color: var(--pf-grant-border); }
508
+ .pf-level-badge--hide, .pf-level-badge--muted { color: var(--pf-block); background: var(--pf-block-soft); border-color: var(--pf-block-border); }
509
+ .pf-level-badge--leave, .pf-level-badge--none { color: var(--pf-text-3); background: transparent; }
510
+
511
+ /* Fields table */
512
+ .pf-fields__row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 8px 12px; border-top: 1px solid var(--pf-border); }
513
+ .pf-fields__row--head { border-top: 0; font-size: 12px; font-weight: 600; color: var(--pf-text-3); }
514
+ .pf-fields__name { display: flex; flex-direction: column; flex: 1 1 220px; min-width: 0; text-align: left; color: inherit !important; text-decoration: none !important; }
515
+ .pf-root a.pf-fields__name:hover .pf-fields__label { text-decoration: underline; }
516
+ .pf-fields__label { font-weight: 500; }
517
+ .pf-mask-preview { font-size: 12px; color: var(--pf-mask); font-family: var(--pf-mono); white-space: nowrap; }
518
+ .pf-bulk-bar { position: sticky; top: 8px; z-index: 5; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 10px 14px; border-radius: var(--pf-radius); background: var(--pf-text); color: var(--pf-surface); box-shadow: var(--pf-shadow); }
519
+ .pf-bulk-bar .pf-btn { background: transparent; color: var(--pf-surface) !important; border-color: rgba(127, 127, 127, .55); }
520
+ .pf-bulk-bar .pf-btn:hover { background: rgba(127, 127, 127, .25); }
521
+ .pf-root .pf-bulk-bar a { color: var(--pf-surface); }
522
+
523
+ /* rule sentence */
524
+ .pf-sentence { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; line-height: 1.9; color: var(--pf-text-2); }
525
+ .pf-token { display: inline-flex; align-items: center; gap: 4px; padding: 1px 8px; border-radius: 5px; background: var(--pf-surface-2); border: 1px solid var(--pf-border); color: var(--pf-text); font-weight: 500; line-height: 1.5; }
526
+ .pf-token--value { background: var(--pf-accent-soft); color: var(--pf-accent-ink); font-weight: 600; border-color: var(--pf-accent-soft); }
527
+ .pf-token--rel { background: var(--pf-accent-soft); color: var(--pf-accent-ink); border-color: var(--pf-border); }
528
+ .pf-rule { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border-radius: var(--pf-radius); border: 1px solid var(--pf-border); background: var(--pf-surface); }
529
+ .pf-rule--off { background: var(--pf-surface-2); }
530
+ .pf-rule__meta { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 12px; color: var(--pf-text-3); }
531
+ .pf-builder-path { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
532
+ .pf-builder-path .pf-select { width: auto; }
533
+ .pf-select--rel { background: var(--pf-accent-soft); color: var(--pf-accent-ink); border-color: var(--pf-border); }
534
+ .pf-condition { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 15px; line-height: 2; }
535
+ .pf-list-chips { display: flex; flex-wrap: wrap; gap: 6px; }
536
+ .pf-list-chip { position: relative; display: inline-flex; }
537
+ .pf-list-chip input { position: absolute; opacity: 0; width: 1px; height: 1px; }
538
+ .pf-list-chip span { display: inline-block; padding: 3px 10px; border-radius: 999px; border: 1px solid var(--pf-border-strong); font-size: 13px; cursor: pointer; }
539
+ .pf-list-chip input:checked + span { background: var(--pf-accent-soft); color: var(--pf-accent-ink); border-color: var(--pf-accent); font-weight: 600; }
540
+ .pf-list-chip input:focus-visible + span { outline: 2px solid var(--pf-accent); outline-offset: 2px; }
541
+
542
+ /* radio cards (new set type, start from, expiry, join type) */
543
+ .pf-choices { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); gap: 10px; }
544
+ .pf-choices--expiry { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 6px; }
545
+ .pf-choice { position: relative; display: flex; }
546
+ .pf-choice > input { position: absolute; opacity: 0; width: 1px; height: 1px; }
547
+ .pf-choice__box { flex: 1; display: flex; flex-direction: column; gap: 4px; padding: 12px 14px; border-radius: var(--pf-radius); border: 1px solid var(--pf-border-strong); background: var(--pf-surface); cursor: pointer; }
548
+ .pf-choices--expiry .pf-choice__box { padding: 7px 10px; border-radius: var(--pf-radius-sm); gap: 0; }
549
+ .pf-choice__title { font-weight: 600; font-size: 14px; display: flex; align-items: center; gap: 6px; }
550
+ .pf-choices--expiry .pf-choice__title { font-size: 13px; }
551
+ .pf-choice__sub { font-size: 12px; color: var(--pf-text-3); }
552
+ .pf-choice__desc { font-size: 13px; color: var(--pf-text-2); }
553
+ .pf-choice > input:checked + .pf-choice__box { border-color: var(--pf-accent); background: var(--pf-accent-soft); color: var(--pf-accent-ink); box-shadow: 0 0 0 1px var(--pf-accent); }
554
+ .pf-choice > input:checked + .pf-choice__box .pf-choice__sub { color: var(--pf-accent-ink); }
555
+ .pf-choice--muting > input:checked + .pf-choice__box { border-color: var(--pf-restrict); background: var(--pf-restrict-soft); color: var(--pf-text); box-shadow: 0 0 0 1px var(--pf-restrict); }
556
+ .pf-choice > input:focus-visible + .pf-choice__box { outline: 2px solid var(--pf-accent); outline-offset: 2px; }
557
+ .pf-choice > input:disabled + .pf-choice__box { opacity: .55; cursor: not-allowed; }
558
+ .pf-glyph-grant { color: var(--pf-grant); font-weight: 700; }
559
+ .pf-glyph-restrict { color: var(--pf-restrict); font-weight: 700; }
560
+ .pf-custom-date { display: none; }
561
+ .pf-expiry:has(input[value="custom"]:checked) .pf-custom-date { display: block; }
562
+
563
+ /* person picker / listbox rows */
564
+ .pf-listbox { display: flex; flex-direction: column; border: 1px solid var(--pf-border); border-radius: var(--pf-radius); overflow: hidden; }
565
+ .pf-listbox__scroll { max-height: 300px; overflow-y: auto; }
566
+ .pf-listbox--short .pf-listbox__scroll { max-height: 240px; }
567
+ .pf-option { position: relative; display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-top: 1px solid var(--pf-border); color: inherit !important; text-decoration: none !important; cursor: pointer; }
568
+ .pf-option:first-child { border-top: 0; }
569
+ .pf-option:hover { background: var(--pf-surface-2); }
570
+ .pf-option > input[type="radio"], .pf-option > input[type="checkbox"] { margin: 0; accent-color: var(--pf-accent); width: 16px; height: 16px; flex: 0 0 16px; }
571
+ .pf-option:has(input:checked) { background: var(--pf-accent-soft); }
572
+ .pf-option:has(input:disabled) { opacity: .6; cursor: not-allowed; }
573
+ .pf-option__main { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
574
+ .pf-option__aside { font-size: 12px; color: var(--pf-text-3); white-space: nowrap; }
575
+ .pf-listbox__footer { padding: 8px 12px; background: var(--pf-surface-2); border-top: 1px solid var(--pf-border); font-size: 12px; color: var(--pf-text-2); }
576
+ .pf-picked { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid var(--pf-border); border-radius: var(--pf-radius); background: var(--pf-surface-2); }
577
+
578
+ /* home search toolbar */
579
+ .pf-toolbar { display: flex; flex-wrap: wrap; align-items: stretch; gap: 8px; }
580
+ .pf-search-field { position: relative; flex: 1 1 320px; min-width: 0; }
581
+ .pf-search-field__icon { position: absolute; left: 12px; top: 50%; width: 16px; height: 16px; transform: translateY(-50%); color: var(--pf-text-3); pointer-events: none; }
582
+ .pf-root .pf-search-field__input { padding: 9px 12px 9px 36px; font-size: 14px; }
583
+ .pf-kbd { font-family: var(--pf-mono); font-size: 11px; padding: 1px 6px; border-radius: 4px; border: 1px solid var(--pf-border-strong); color: var(--pf-text-3); white-space: nowrap; }
584
+ .pf-all-good { display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center; padding: 40px 24px; }
585
+ .pf-all-good__icon { display: grid; place-items: center; width: 44px; height: 44px; border-radius: 999px; background: var(--pf-grant-soft); color: var(--pf-grant); border: 1px solid var(--pf-grant-border); font-size: 20px; font-weight: 700; }
586
+ .pf-steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); gap: 12px; }
587
+ .pf-step { display: flex; flex-direction: column; gap: 6px; padding: 14px; border-radius: var(--pf-radius); background: var(--pf-surface-2); border: 1px solid var(--pf-border); }
588
+ .pf-step__num { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 999px; background: var(--pf-text); color: var(--pf-surface); font-size: 12px; font-weight: 700; }
589
+ .pf-dryrun { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border-radius: var(--pf-radius); border: 1px dashed var(--pf-border-strong); background: var(--pf-surface-2); }
590
+
591
+ /* ---------------------------------------------------------------- banners */
592
+ .pf-banner { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 10px 14px; padding: 12px 16px; border-radius: var(--pf-radius-lg); border: 1px solid var(--pf-border); background: var(--pf-surface-2); font-size: 13px; }
593
+ .pf-banner__glyph { font-size: 18px; line-height: 1.2; }
594
+ .pf-banner__body { flex: 1 1 260px; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
595
+ .pf-banner__title { font-weight: 600; font-size: 14px; }
596
+ .pf-banner--help { background: var(--pf-accent-soft); }
597
+ .pf-banner--muting { background: var(--pf-restrict-soft); border-color: var(--pf-restrict-border); }
598
+ .pf-banner--muting .pf-banner__glyph, .pf-banner--muting .pf-banner__title { color: var(--pf-restrict); }
599
+ .pf-banner--warn { background: var(--pf-warn-soft); border-color: var(--pf-warn-border); }
600
+ .pf-banner--warn .pf-banner__glyph { color: var(--pf-warn); }
601
+ .pf-banner--block { background: var(--pf-block-soft); border-color: var(--pf-block-border); }
602
+ .pf-banner--block .pf-banner__glyph, .pf-banner--block .pf-banner__title { color: var(--pf-block); }
603
+ .pf-banner--grant { background: var(--pf-grant-soft); border-color: var(--pf-grant-border); }
604
+
605
+ /* ------------------------------------------------------------ why panel */
606
+ .pf-split { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-start; }
607
+ .pf-split__main { flex: 1 1 560px; min-width: 0; }
608
+ .pf-split__side { flex: 1 1 280px; min-width: 0; position: sticky; top: 24px; }
609
+ .pf-why { display: flex; flex-direction: column; gap: 10px; padding: 16px 18px; }
610
+ .pf-why__reason { display: flex; gap: 8px; align-items: flex-start; font-size: 13px; }
611
+ .pf-why__reason > span:first-child { font-weight: 700; flex: 0 0 auto; }
612
+ .pf-why-item { display: none; }
613
+ .pf-why-item:target, .pf-why-item[data-pf-active="true"] { display: flex; }
614
+ .pf-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 12px; color: var(--pf-text-2); }
615
+
616
+ /* ------------------------------------------------------------- settings */
617
+ .pf-setting { display: flex; flex-wrap: wrap; gap: 10px 20px; padding: 12px 16px; border-top: 1px solid var(--pf-border); scroll-margin-top: 80px; }
618
+ .pf-setting:target, .pf-setting[data-pf-focus="true"] { background: var(--pf-accent-soft); }
619
+ .pf-setting__about { flex: 2 1 300px; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
620
+ .pf-setting__value { flex: 1 1 240px; display: flex; flex-direction: column; align-items: flex-start; gap: 6px; min-width: 0; }
621
+ .pf-setting__how { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
622
+ .pf-setting__how[open] { flex: 1 1 100%; }
623
+ .pf-setting__how > summary { list-style: none; }
624
+ .pf-setting__how > summary::-webkit-details-marker { display: none; }
625
+ .pf-setting__panel { flex: 1 1 100%; display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border-radius: var(--pf-radius); background: var(--pf-surface-2); border: 1px solid var(--pf-border); }
626
+ .pf-setting-section > summary { list-style: none; display: flex; align-items: center; gap: 10px; padding: 14px 16px; cursor: pointer; }
627
+ .pf-setting-section > summary::-webkit-details-marker { display: none; }
628
+ .pf-setting-section + .pf-setting-section { border-top: 1px solid var(--pf-border); }
629
+ .pf-pill { display: inline-flex; align-items: center; gap: 4px; padding: 1px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; border: 1px solid var(--pf-border); background: var(--pf-surface-2); color: var(--pf-text-2); white-space: nowrap; }
630
+ .pf-pill--on { background: var(--pf-grant-soft); color: var(--pf-grant); border-color: var(--pf-grant-border); }
631
+ .pf-pill--opt { font-family: var(--pf-mono); font-weight: 500; background: transparent; }
632
+ .pf-pill--current { background: var(--pf-text); color: var(--pf-surface); border-color: var(--pf-text); }
633
+ .pf-code { margin: 0; padding: 12px 14px; border-radius: var(--pf-radius); background: var(--pf-surface); border: 1px solid var(--pf-border); font-family: var(--pf-mono); font-size: 12.5px; line-height: 1.6; overflow-x: auto; white-space: pre; }
634
+ .pf-ladder { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); gap: 10px; padding: 0 16px 14px; }
635
+ .pf-ladder__tile { display: flex; flex-direction: column; gap: 4px; padding: 10px 12px; border-radius: var(--pf-radius); border: 1px solid var(--pf-border); background: var(--pf-surface-2); }
636
+ .pf-ladder__tile--on { background: var(--pf-grant-soft); border-color: var(--pf-grant-border); color: var(--pf-grant); }
637
+ .pf-map { display: flex; flex-direction: column; gap: 2px; font-family: var(--pf-mono); font-size: 12px; }
638
+
639
+ /* ----------------------------------------------------- footer elements */
640
+ .pf-audit-footer { font-size: 12px; color: var(--pf-text-3); padding-top: 12px; border-top: 1px solid var(--pf-border); }
641
+ /* A link inside a line of text is told apart by more than its colour. */
642
+ .pf-audit-footer a, .pf-hint a { text-decoration: underline; }
643
+ .pf-tech > summary { font-size: 12px; color: var(--pf-text-3); cursor: pointer; width: max-content; }
644
+ .pf-tech__body { display: flex; flex-direction: column; gap: 8px; padding-top: 10px; font-size: 12px; color: var(--pf-text-2); }
645
+ .pf-copy { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
646
+ .pf-uuid { font-family: var(--pf-mono); font-size: 12px; user-select: all; overflow-wrap: anywhere; }
647
+
648
+ /* unsaved-changes bar */
649
+ .pf-savebar { position: sticky; bottom: 16px; z-index: 10; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 16px; padding: 12px 16px; border-radius: var(--pf-radius-lg); background: var(--pf-text); color: var(--pf-surface); box-shadow: var(--pf-shadow); }
650
+ .pf-savebar__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 280px; }
651
+ .pf-savebar__count { font-weight: 600; }
652
+ .pf-savebar__note { font-size: 12px; opacity: .8; }
653
+ .pf-savebar[data-pf-dirty="false"] .pf-savebar__count--dirty, .pf-savebar[data-pf-dirty="true"] .pf-savebar__count--clean { display: none; }
654
+
655
+ /* --------------------------------------------------------------- menus */
656
+ .pf-menu { position: relative; display: inline-block; }
657
+ .pf-menu > summary { list-style: none; }
658
+ .pf-menu > summary::-webkit-details-marker { display: none; }
659
+ .pf-menu__panel { position: absolute; right: 0; top: calc(100% + 6px); z-index: 30; width: min(340px, 90vw); display: flex; flex-direction: column; padding: 6px; border-radius: var(--pf-radius-lg); background: var(--pf-surface); border: 1px solid var(--pf-border); box-shadow: var(--pf-shadow); }
660
+ .pf-menu__item { display: flex; flex-direction: column; gap: 2px; width: 100%; padding: 8px 10px; border-radius: var(--pf-radius-sm); border: 0; background: none; text-align: left; color: var(--pf-text) !important; text-decoration: none !important; }
661
+ .pf-menu__item:hover { background: var(--pf-surface-2); }
662
+ .pf-menu__item--danger .pf-menu__title { color: var(--pf-block); }
663
+ .pf-menu__title { font-weight: 600; font-size: 14px; }
664
+ .pf-menu__desc { font-size: 12px; color: var(--pf-text-3); }
665
+ .pf-menu__sep { height: 1px; margin: 6px 4px; background: var(--pf-border); }
666
+ .pf-menu__label { padding: 4px 10px; font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--pf-text-3); }
667
+
668
+ /* segmented control (language) */
669
+ .pf-segmented { display: flex; gap: 2px; padding: 2px; border-radius: var(--pf-radius-sm); background: var(--pf-surface-2); border: 1px solid var(--pf-border); }
670
+ .pf-segmented__item { flex: 1; border: 0; background: none; cursor: pointer; padding: 4px 6px; border-radius: 4px; font-size: 12px; font-weight: 600; text-align: center; color: var(--pf-text-2) !important; text-decoration: none !important; }
671
+ .pf-segmented__item[aria-current="true"] { background: var(--pf-surface); color: var(--pf-text) !important; box-shadow: 0 0 0 1px var(--pf-border-strong); }
672
+
673
+ /* ------------------------------------------------------ dialogs & toasts */
674
+ .pf-dialog { width: min(500px, calc(100% - 32px)); max-height: calc(100vh - 48px); padding: 0; border: 1px solid var(--pf-border); border-radius: var(--pf-radius-lg); background: var(--pf-surface); color: var(--pf-text); box-shadow: var(--pf-shadow); }
675
+ .pf-dialog::backdrop { background: var(--pf-overlay); }
676
+ .pf-dialog--wide { width: min(620px, calc(100% - 32px)); }
677
+ .pf-drawer { margin: 0 0 0 auto; width: min(620px, 100%); max-width: 100%; height: 100vh; max-height: 100vh; padding: 0; border: 0; border-left: 1px solid var(--pf-border); border-radius: 0; background: var(--pf-surface); color: var(--pf-text); box-shadow: var(--pf-shadow); }
678
+ .pf-drawer::backdrop { background: var(--pf-overlay); }
679
+ .pf-modal-page { max-width: 640px; }
680
+ .pf-sheet { display: flex; flex-direction: column; min-height: 100%; }
681
+ .pf-sheet__header { position: sticky; top: 0; z-index: 2; display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 18px 22px; background: var(--pf-surface); border-bottom: 1px solid var(--pf-border); }
682
+ .pf-sheet__body { flex: 1; display: flex; flex-direction: column; gap: 18px; padding: 18px 22px; }
683
+ .pf-sheet__footer { position: sticky; bottom: 0; z-index: 2; display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; padding: 14px 22px; background: var(--pf-surface); border-top: 1px solid var(--pf-border); }
684
+ .pf-sheet__footer-note { flex: 1 1 220px; font-size: 13px; color: var(--pf-text-2); }
685
+ .pf-sheet--page { border: 1px solid var(--pf-border); border-radius: var(--pf-radius-lg); background: var(--pf-surface); overflow: hidden; }
686
+ .pf-sheet--page .pf-sheet__header, .pf-sheet--page .pf-sheet__footer { position: static; }
687
+ .pf-close { display: inline-grid; place-items: center; width: 32px; height: 32px; flex: 0 0 32px; border-radius: var(--pf-radius-sm); border: 0; background: none; color: var(--pf-text-2) !important; font-size: 20px; line-height: 1; text-decoration: none !important; }
688
+ .pf-close:hover { background: var(--pf-surface-2); }
689
+ .pf-confirm { display: flex; flex-direction: column; gap: 14px; padding: 22px; }
690
+ .pf-confirm__title { font-size: 17px; font-weight: 600; text-wrap: balance; }
691
+ .pf-confirm__list { max-height: 200px; overflow-y: auto; padding: 10px 12px; border-radius: var(--pf-radius); background: var(--pf-surface-2); font-size: 13px; display: flex; flex-direction: column; gap: 4px; }
692
+ .pf-confirm--danger { border-top: 3px solid var(--pf-block); }
693
+ .pf-toasts { position: fixed; left: 50%; bottom: 24px; z-index: 60; transform: translateX(-50%); display: flex; flex-direction: column; gap: 8px; width: max-content; max-width: min(640px, calc(100% - 32px)); }
694
+ .pf-toast { display: flex; align-items: center; gap: 12px; padding: 10px 12px 10px 16px; border-radius: var(--pf-radius); background: var(--pf-text); color: var(--pf-surface); box-shadow: var(--pf-shadow); font-size: 13px; }
695
+ .pf-toast--alert { background: var(--pf-block); color: #fff; }
696
+ .pf-toast__text { flex: 1; min-width: 0; }
697
+ .pf-toast .pf-btn { padding: 3px 10px; font-size: 13px; background: transparent; color: inherit !important; border-color: currentColor; }
698
+ .pf-toast .pf-close { color: inherit !important; width: 26px; height: 26px; flex-basis: 26px; font-size: 18px; }
699
+ .pf-toast .pf-close:hover { background: rgba(127, 127, 127, .25); }
700
+
701
+ /* -------------------------------------------------------- misc screens */
702
+ .pf-person-header { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; }
703
+ .pf-assignment { display: flex; flex-wrap: wrap; gap: 10px 18px; padding: 14px 18px; border-top: 1px solid var(--pf-border); }
704
+ .pf-assignment:first-child { border-top: 0; }
705
+ .pf-assignment__main { flex: 1 1 280px; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
706
+ .pf-assignment__when { flex: 0 1 200px; min-width: 170px; display: flex; flex-direction: column; gap: 2px; font-size: 13px; }
707
+ .pf-assignment__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
708
+ .pf-group-card { display: flex; flex-direction: column; gap: 10px; padding: 16px 18px; border-radius: var(--pf-radius-lg); background: var(--pf-surface); border: 1px solid var(--pf-border); color: inherit !important; text-decoration: none !important; }
709
+ .pf-root a.pf-group-card:hover { border-color: var(--pf-border-strong); }
710
+ .pf-grid-groups { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr)); gap: 16px; }
711
+ .pf-sync { border: 1px solid var(--pf-warn-border); border-radius: var(--pf-radius-lg); background: var(--pf-warn-soft); }
712
+ .pf-sync > summary { list-style: none; cursor: pointer; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; padding: 12px 16px; color: var(--pf-warn); font-weight: 600; font-size: 13px; }
713
+ .pf-sync > summary::-webkit-details-marker { display: none; }
714
+ .pf-sync__body { padding: 0 16px 16px; }
715
+ .pf-crud-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
716
+ .pf-crud-grid__col { display: flex; flex-direction: column; gap: 6px; }
717
+ .pf-scope-option { display: flex; gap: 8px; align-items: flex-start; }
718
+ .pf-empty { display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center; padding: 32px 24px; color: var(--pf-text-2); }
719
+ .pf-pagination { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; }
720
+ .pf-change-list { display: flex; flex-direction: column; gap: 6px; font-size: 13px; }
721
+ .pf-change-list li { display: flex; gap: 8px; align-items: baseline; }
722
+ .pf-change-list li::before { content: "•"; color: var(--pf-text-3); }
723
+ .pf-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 16px; font-size: 13px; }
724
+ .pf-kv dt { color: var(--pf-text-3); }
725
+ .pf-kv dd { min-width: 0; overflow-wrap: anywhere; }
726
+
727
+ /* --------------------------------------------------------------- audit log */
728
+ .pf-audit-filters__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr)); gap: 12px 16px; align-items: end; }
729
+ .pf-audit-filters__zone, .pf-audit-filters__views { grid-column: 1 / -1; }
730
+ .pf-audit-errors { display: flex; flex-direction: column; gap: 2px; }
731
+ .pf-audit-list { display: flex; flex-direction: column; }
732
+ .pf-audit-event { display: grid; grid-template-columns: 120px minmax(0, 1fr) auto; gap: 6px 16px; align-items: start; padding: 12px 18px; border-top: 1px solid var(--pf-border); }
733
+ .pf-audit-event__when { display: flex; flex-direction: column; gap: 2px; font-size: 12px; font-variant-numeric: tabular-nums; }
734
+ .pf-audit-event__main { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
735
+ .pf-root .pf-audit-event__sentence { color: var(--pf-text); font-size: 14px; font-weight: 500; text-decoration: none; overflow-wrap: anywhere; }
736
+ .pf-root .pf-audit-event__sentence:hover { text-decoration: underline; }
737
+ .pf-audit-event__meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; font-size: 12px; color: var(--pf-text-3); min-width: 0; }
738
+ .pf-audit-event__meta > * { min-width: 0; overflow-wrap: anywhere; }
739
+ .pf-audit-event__reason { color: var(--pf-text-2); font-style: italic; overflow-wrap: anywhere; }
740
+ .pf-audit-code { font-family: var(--pf-mono); font-size: 12px; overflow-wrap: anywhere; }
741
+ .pf-audit-table { width: 100%; border-collapse: collapse; font-size: 13px; }
742
+ .pf-audit-table th, .pf-audit-table td { padding: 8px 18px; border-top: 1px solid var(--pf-border); text-align: left; vertical-align: top; overflow-wrap: anywhere; }
743
+ .pf-audit-table thead th { font-size: 12px; font-weight: 600; color: var(--pf-text-3); border-top: 0; }
744
+ .pf-audit-table tbody th { font-weight: 500; color: var(--pf-text-2); width: 28%; }
745
+ .pf-audit-table__before { color: var(--pf-text-3); text-decoration: line-through; text-decoration-color: var(--pf-border-strong); }
746
+ .pf-audit-table__after { color: var(--pf-text); font-weight: 500; }
747
+ @media (max-width: 640px) {
748
+ .pf-audit-event { grid-template-columns: minmax(0, 1fr); padding: 12px 16px; }
749
+ .pf-audit-event__when { flex-direction: row; gap: 6px; }
750
+ .pf-audit-event__open { justify-self: start; }
751
+ .pf-audit-table th, .pf-audit-table td { padding: 8px 12px; }
752
+ .pf-kv { grid-template-columns: minmax(0, 1fr); gap: 2px; }
753
+ .pf-kv dd { margin-bottom: 8px; }
754
+ }
755
+
756
+ /* ------------------------------------------------- reason for a change */
757
+ .pf-confirm-form { display: flex; flex-direction: column; gap: 12px; }
758
+ .pf-reason { font-size: 13px; }
759
+ .pf-reason__summary { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; color: var(--pf-text-2); list-style: none; }
760
+ .pf-reason__summary::-webkit-details-marker { display: none; }
761
+ .pf-reason__summary:hover { color: var(--pf-text); }
762
+ .pf-reason__body { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; max-width: 420px; }
763
+ .pf-reason--compact .pf-reason__summary { font-size: 12px; }
764
+ .pf-reason--compact .pf-reason__body { margin-top: 6px; min-width: 200px; }
765
+ .pf-reason-form { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
766
+ .pf-reason-form .pf-reason--compact[open] { flex-basis: 100%; }