plexora 0.0.1__py3-none-any.whl

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 (248) hide show
  1. plexora/__init__.py +129 -0
  2. plexora/__main__.py +15 -0
  3. plexora/_url.py +105 -0
  4. plexora/api/__init__.py +55 -0
  5. plexora/api/dataset.py +398 -0
  6. plexora/api/http.py +19 -0
  7. plexora/api/plugin.py +541 -0
  8. plexora/api/store.py +221 -0
  9. plexora/cli.py +777 -0
  10. plexora/client/dist/354_bundle.js +2 -0
  11. plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
  12. plexora/client/dist/418_bundle.js +2 -0
  13. plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
  14. plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
  15. plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
  16. plexora/client/dist/770_bundle.js +1 -0
  17. plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
  18. plexora/client/dist/vendor_bundle.js +2 -0
  19. plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
  20. plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
  21. plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
  22. plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
  23. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
  24. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
  25. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
  26. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
  27. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
  28. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
  29. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
  30. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
  31. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
  32. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
  33. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
  34. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
  35. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
  36. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
  37. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
  38. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
  39. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
  40. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
  41. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
  42. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
  43. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
  44. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
  45. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
  46. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
  47. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
  48. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
  49. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
  50. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
  51. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
  52. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
  53. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
  54. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
  55. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
  56. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
  57. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
  58. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
  59. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
  60. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
  61. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
  62. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
  63. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
  64. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
  65. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
  66. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
  67. plexora/client/src/css/import.css +474 -0
  68. plexora/client/src/css/main.css +903 -0
  69. plexora/client/src/css/openProject.css +363 -0
  70. plexora/client/src/css/quickView.css +166 -0
  71. plexora/client/src/css/tokens.css +51 -0
  72. plexora/client/src/css/viewer.css +1655 -0
  73. plexora/client/src/img/apple-touch-icon.png +0 -0
  74. plexora/client/src/img/favicon.ico +0 -0
  75. plexora/client/src/img/logo.ai +8892 -33
  76. plexora/client/src/img/logo.svg +1 -0
  77. plexora/client/src/img/logo_with_text.ai +9011 -34
  78. plexora/client/src/img/logo_with_text.svg +1 -0
  79. plexora/client/src/js/main.js +704 -0
  80. plexora/client/src/js/pluginRegistry.js +108 -0
  81. plexora/client/src/js/services/appStatus.js +286 -0
  82. plexora/client/src/js/services/browsePicker.js +73 -0
  83. plexora/client/src/js/services/dataLayer.js +400 -0
  84. plexora/client/src/js/services/datasetContext.js +143 -0
  85. plexora/client/src/js/services/glRenderer.js +143 -0
  86. plexora/client/src/js/services/importFormValidation.js +257 -0
  87. plexora/client/src/js/services/numericData.js +100 -0
  88. plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
  89. plexora/client/src/js/services/simpleEventHandler.js +25 -0
  90. plexora/client/src/js/vendor.js +29 -0
  91. plexora/client/src/js/views/channelList.js +663 -0
  92. plexora/client/src/js/views/colorSwatchPicker.js +202 -0
  93. plexora/client/src/js/views/columnClassifier.js +146 -0
  94. plexora/client/src/js/views/coordinateField.js +133 -0
  95. plexora/client/src/js/views/dataSourceField.js +276 -0
  96. plexora/client/src/js/views/imageViewer.js +3306 -0
  97. plexora/client/src/js/views/miniMap.js +787 -0
  98. plexora/client/src/js/views/navbarControls.js +178 -0
  99. plexora/client/src/js/views/openProjectPage.js +239 -0
  100. plexora/client/src/js/views/projectEdit.js +335 -0
  101. plexora/client/src/js/views/quickViewLanding.js +136 -0
  102. plexora/client/src/js/views/rainbow.js +326 -0
  103. plexora/client/src/js/views/requirementsModal.js +572 -0
  104. plexora/client/src/js/views/rgbImageViewer.js +115 -0
  105. plexora/client/src/js/views/searchableSelect.js +411 -0
  106. plexora/client/src/js/views/segmentationProgress.js +122 -0
  107. plexora/client/src/js/views/toolLoader.js +747 -0
  108. plexora/client/src/js/views/viewerControls.js +689 -0
  109. plexora/client/src/js/views/viewerManager.js +434 -0
  110. plexora/client/src/js/views/viewerSidebar.js +1117 -0
  111. plexora/client/src/js/workers/tileDecoder.js +130 -0
  112. plexora/client/src/shaders/frag.glsl +528 -0
  113. plexora/client/src/shaders/vert.glsl +12 -0
  114. plexora/client/templates/base.html +212 -0
  115. plexora/client/templates/index.html +238 -0
  116. plexora/client/templates/open_project.html +96 -0
  117. plexora/client/templates/project_columns.html +86 -0
  118. plexora/client/templates/project_edit.html +196 -0
  119. plexora/client/templates/upload.html +118 -0
  120. plexora/connect.py +528 -0
  121. plexora/datasource.py +782 -0
  122. plexora/jupyter.py +406 -0
  123. plexora/notebook_env.py +232 -0
  124. plexora/paths.py +486 -0
  125. plexora/plugins/__init__.py +12 -0
  126. plexora/plugins/cell_explorer/__init__.py +100 -0
  127. plexora/plugins/cell_explorer/server/__init__.py +11 -0
  128. plexora/plugins/cell_explorer/server/routes.py +167 -0
  129. plexora/plugins/cell_explorer/server/state.py +281 -0
  130. plexora/plugins/cell_explorer/server/values.py +145 -0
  131. plexora/plugins/cell_explorer/server/variables.py +367 -0
  132. plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
  133. plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
  134. plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
  135. plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
  136. plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
  137. plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
  138. plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
  139. plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
  140. plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
  141. plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
  142. plexora/plugins/figure_builder/__init__.py +125 -0
  143. plexora/plugins/figure_builder/server/__init__.py +7 -0
  144. plexora/plugins/figure_builder/server/compose.py +365 -0
  145. plexora/plugins/figure_builder/server/export.py +784 -0
  146. plexora/plugins/figure_builder/server/export_jobs.py +148 -0
  147. plexora/plugins/figure_builder/server/operations.py +556 -0
  148. plexora/plugins/figure_builder/server/pixels.py +142 -0
  149. plexora/plugins/figure_builder/server/provenance.py +174 -0
  150. plexora/plugins/figure_builder/server/render.py +278 -0
  151. plexora/plugins/figure_builder/server/repository.py +701 -0
  152. plexora/plugins/figure_builder/server/routes.py +594 -0
  153. plexora/plugins/figure_builder/server/schema.py +1023 -0
  154. plexora/plugins/figure_builder/server/sources.py +120 -0
  155. plexora/plugins/figure_builder/server/textmetrics.py +155 -0
  156. plexora/plugins/figure_builder/static/figureActions.js +363 -0
  157. plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
  158. plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
  159. plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
  160. plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
  161. plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
  162. plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
  163. plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
  164. plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
  165. plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
  166. plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
  167. plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
  168. plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
  169. plexora/plugins/figure_builder/static/figureRichText.js +634 -0
  170. plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
  171. plexora/plugins/figure_builder/static/figureSchema.js +218 -0
  172. plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
  173. plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
  174. plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
  175. plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
  176. plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
  177. plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
  178. plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
  179. plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
  180. plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
  181. plexora/plugins/gating/__init__.py +76 -0
  182. plexora/plugins/gating/server/__init__.py +0 -0
  183. plexora/plugins/gating/server/anndata_gates.py +367 -0
  184. plexora/plugins/gating/server/database.py +16 -0
  185. plexora/plugins/gating/server/model.py +320 -0
  186. plexora/plugins/gating/server/routes.py +235 -0
  187. plexora/plugins/gating/static/csvGatingList.js +947 -0
  188. plexora/plugins/gating/static/gating.css +289 -0
  189. plexora/plugins/gating/static/gatingApi.js +221 -0
  190. plexora/plugins/gating/static/gatingSidebarController.js +513 -0
  191. plexora/plugins/gating/templates/gating/legacy.html +26 -0
  192. plexora/plugins/gating/templates/gating/panel.html +69 -0
  193. plexora/plugins/roi/__init__.py +95 -0
  194. plexora/plugins/roi/server/__init__.py +8 -0
  195. plexora/plugins/roi/server/adapters.py +655 -0
  196. plexora/plugins/roi/server/geojson.py +267 -0
  197. plexora/plugins/roi/server/geometry.py +181 -0
  198. plexora/plugins/roi/server/mapping.py +169 -0
  199. plexora/plugins/roi/server/operations.py +293 -0
  200. plexora/plugins/roi/server/repository.py +215 -0
  201. plexora/plugins/roi/server/routes.py +409 -0
  202. plexora/plugins/roi/server/schema.py +323 -0
  203. plexora/plugins/roi/static/roi.css +583 -0
  204. plexora/plugins/roi/static/roiApi.js +177 -0
  205. plexora/plugins/roi/static/roiFigureBridge.js +107 -0
  206. plexora/plugins/roi/static/roiGeometry.js +350 -0
  207. plexora/plugins/roi/static/roiRenderer.js +278 -0
  208. plexora/plugins/roi/static/roiSidebarController.js +1038 -0
  209. plexora/plugins/roi/static/roiState.js +518 -0
  210. plexora/plugins/roi/static/roiTools.js +993 -0
  211. plexora/plugins/roi/templates/roi/panel.html +240 -0
  212. plexora/proxy.py +57 -0
  213. plexora/server/models/adapters/__init__.py +94 -0
  214. plexora/server/models/adapters/anndata_adapter.py +388 -0
  215. plexora/server/models/adapters/base.py +93 -0
  216. plexora/server/models/adapters/classify.py +192 -0
  217. plexora/server/models/adapters/csv_adapter.py +105 -0
  218. plexora/server/models/adapters/inspection.py +265 -0
  219. plexora/server/models/adapters/spatialdata_adapter.py +248 -0
  220. plexora/server/models/centroid_tiles.py +305 -0
  221. plexora/server/models/data_model.py +1659 -0
  222. plexora/server/models/database_model.py +169 -0
  223. plexora/server/models/project.py +1270 -0
  224. plexora/server/plugins.py +199 -0
  225. plexora/server/routes/browse_routes.py +41 -0
  226. plexora/server/routes/data_routes.py +294 -0
  227. plexora/server/routes/import_routes.py +603 -0
  228. plexora/server/routes/page_routes.py +139 -0
  229. plexora/server/routes/project_routes.py +438 -0
  230. plexora/server/routes/quick_view_routes.py +62 -0
  231. plexora/server/routes/system_routes.py +39 -0
  232. plexora/server/routes/tool_routes.py +418 -0
  233. plexora/server/utils/addHEColumns.py +11 -0
  234. plexora/server/utils/fast_png.py +97 -0
  235. plexora/server/utils/fullConversion.py +50 -0
  236. plexora/server/utils/native_dialog.py +168 -0
  237. plexora/server/utils/pre_normalization.py +47 -0
  238. plexora/server/utils/segmentation_pyramid.py +598 -0
  239. plexora/server/utils/smallestenclosingcircle.py +126 -0
  240. plexora/server/utils/tiffsurgeon.py +372 -0
  241. plexora/server_cli.py +48 -0
  242. plexora/test.py +0 -0
  243. plexora-0.0.1.dist-info/METADATA +341 -0
  244. plexora-0.0.1.dist-info/RECORD +248 -0
  245. plexora-0.0.1.dist-info/WHEEL +5 -0
  246. plexora-0.0.1.dist-info/entry_points.txt +12 -0
  247. plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
  248. plexora-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3292 @@
1
+ /* Figure Builder's own appearance.
2
+
3
+ Every selector here is a .fb- class or an #fb_ id, all of which this plugin
4
+ renders itself. Nothing styles a core element: a plugin's stylesheet stops
5
+ applying the moment its tool is closed, so a rule for a core control would
6
+ leave that control unstyled on every page without the tool -- which is the
7
+ regression tests/test_plugin_css_boundary.py exists to catch, reported by a
8
+ user after gating.css hid one of core's file inputs.
9
+
10
+ Design tokens come from core's tokens.css, loaded by base.html on every page,
11
+ so this plugin follows the app's palette instead of inventing one that drifts
12
+ from it at the next theme change. */
13
+
14
+ .fb-muted {
15
+ color: var(--text-muted);
16
+ font-size: 12px;
17
+ }
18
+
19
+ /* -- the library ------------------------------------------------------- */
20
+
21
+ .fb-card-sources {
22
+ color: var(--text-muted);
23
+ display: block;
24
+ font-size: 11px;
25
+ overflow: hidden;
26
+ padding: 0 var(--space-3) var(--space-3);
27
+ text-overflow: ellipsis;
28
+ white-space: nowrap;
29
+ }
30
+
31
+ .fb-thumb {
32
+ aspect-ratio: 4 / 3;
33
+ }
34
+
35
+ /* A figure whose database could not be read is shown, not hidden -- it is the
36
+ one the user most needs to see. Muted and unclickable, so it reads as
37
+ "damaged" rather than as "empty". */
38
+ .fb-card-damaged {
39
+ opacity: 0.65;
40
+ }
41
+
42
+ .fb-card-damaged .project-card-name {
43
+ padding-top: var(--space-3);
44
+ }
45
+
46
+ /* -- one figure's page ------------------------------------------------- */
47
+ /*
48
+ ONE workspace, and the canvas is the whole of it.
49
+
50
+ Nothing here is a column any more. The rail and the tray are cards lying ON
51
+ the canvas, which runs edge to edge underneath them -- the arrangement every
52
+ figure editor has converged on, and the reason is that a figure is a
53
+ document with an aspect ratio: giving permanent columns to controls that are
54
+ used in bursts costs the page the width it needs to be legible, all the time,
55
+ to save a click sometimes.
56
+
57
+ The canvas is padded rather than overlapped, though. A floating panel that
58
+ covers the artwork is the version of this that looks right in a screenshot
59
+ and is infuriating to work in, because the thing under it is the thing you
60
+ are comparing against. The padding is a variable, so opening the tray slides
61
+ the page clear of it and closing it gives the width back. */
62
+
63
+ /*
64
+ LIGHT, on a page whose app is dark.
65
+
66
+ This is the only light surface in Plexora, and it is not a style choice. The
67
+ subject of this page is a sheet of white paper that will be printed, and
68
+ every judgement made here -- is that grey too pale, is that label readable,
69
+ do these two panels match -- is a judgement about ink on white. Dark chrome
70
+ around a white page puts the eye's adaptation somewhere the paper is not, and
71
+ makes every panel look brighter and more contrasted than it will be in the
72
+ journal. The photo editors that stay dark do it because their subject is a
73
+ backlit image; ours is paper.
74
+
75
+ Done by re-pointing CORE's tokens for this subtree rather than by rewriting
76
+ two hundred declarations. Every .fb- rule inside .fb-workspace, including the
77
+ ones further down this file, resolves against these. It cannot leak: the
78
+ library page, the capture dock and the viewer are all outside this element,
79
+ and Quick Edit puts the dark values back for its own subtree (see below) --
80
+ its subject IS a backlit image. */
81
+ .fb-workspace {
82
+ --surface-0: #f4f5f8;
83
+ --surface-1: #ffffff;
84
+ --surface-2: #f1f3f7;
85
+ --surface-3: rgba(15, 23, 42, 0.06);
86
+ --border-subtle: #e4e7ee;
87
+ --border-strong: #ccd2dd;
88
+ --text-primary: #16202e;
89
+ --text-secondary: #3f4a5c;
90
+ --text-muted: #78839a;
91
+ /* Sky blue is a dark-theme accent; on white it is a highlighter. The
92
+ selection colour has to work in both places this page puts it -- chrome
93
+ on white, and handles over a fluorescence image -- so it is one step
94
+ deeper and one step more saturated. */
95
+ --accent-channel: #2563eb;
96
+ --accent-channel-soft: rgba(37, 99, 235, 0.28);
97
+ /* What legibly sits ON the accent. It flips with the accent inside Quick
98
+ Edit, where the surround goes back to dark and the accent back to sky. */
99
+ --fb-on-accent: #ffffff;
100
+ /* Core's danger red is #f87171, chosen to be read on a dark surface. This
101
+ workspace is the one light surface in the app, and on a white card it is
102
+ a pink -- illegible as text and, filled behind white lettering on the
103
+ delete button, worse. The same value .fb-menu already carries for the
104
+ same reason. */
105
+ --accent-danger: #d64545;
106
+
107
+ --fb-rail: 64px;
108
+ /* Wide enough for two thumbnails side by side at a size worth looking at.
109
+ A third column would have to come out of the page's width, and the page
110
+ is the thing being judged. */
111
+ --fb-tray: 312px;
112
+ --fb-gap: 12px;
113
+ /* One height for every field on a sidebar -- the select, the stepper and
114
+ the segmented tracks are three different constructions and were three
115
+ different heights, which showed as a ragged right-hand column. */
116
+ --fb-control-h: 30px;
117
+ --fb-desk: #f4f5f8;
118
+ --fb-hover: rgba(15, 23, 42, 0.06);
119
+ --fb-tint: rgba(37, 99, 235, 0.10);
120
+ --fb-card-radius: 14px;
121
+ /* Two shadows, not one: the tight pair is what makes a card read as lifted
122
+ a few millimetres rather than as pasted on. */
123
+ --fb-float: 0 1px 2px rgba(15, 23, 42, 0.06),
124
+ 0 8px 28px rgba(15, 23, 42, 0.12);
125
+
126
+ background: var(--surface-0);
127
+ color: var(--text-primary);
128
+ display: flex;
129
+ flex-direction: column;
130
+ height: calc(100vh - 48px);
131
+ }
132
+
133
+ /* -- the floating bars --------------------------------------------------- */
134
+ /*
135
+ The topbar and the status bar are not surfaces. The desk runs from the top of
136
+ the window to the bottom of it and the CONTROLS float on it, in cards cut to
137
+ their contents -- the same card as the rail and the tray, because a page with
138
+ three kinds of floating panel and two full-width strips is a page caught
139
+ half-way through a redesign.
140
+
141
+ The gap between two clusters is what says which controls are related. That is
142
+ the job the grey pill inside a solid bar was doing, done without a box inside
143
+ a box. */
144
+
145
+ /* 56 = 12 of desk above the clusters and exactly the cluster below, so the gap
146
+ between the topbar and the top of the tray card is the tray's own 12px inset
147
+ and nothing else. The status bar is the same arithmetic upside down. */
148
+ .fb-topbar {
149
+ align-items: center;
150
+ background: transparent;
151
+ display: flex;
152
+ flex: 0 0 auto;
153
+ gap: var(--fb-gap);
154
+ height: 56px;
155
+ padding: var(--fb-gap) var(--fb-gap) 0;
156
+ }
157
+
158
+ .fb-topbar-spacer {
159
+ flex: 1;
160
+ }
161
+
162
+ .fb-bar-cluster {
163
+ align-items: center;
164
+ background: var(--surface-1);
165
+ border: 1px solid var(--border-subtle);
166
+ border-radius: var(--fb-card-radius);
167
+ box-shadow: var(--fb-float);
168
+ display: flex;
169
+ flex: 0 0 auto;
170
+ gap: 2px;
171
+ height: 44px;
172
+ min-width: 0;
173
+ padding: 0 6px;
174
+ }
175
+
176
+ /* The only cluster that gives width back. It holds the title, and a long one
177
+ should shorten rather than push the controls off the end of the bar. */
178
+ .fb-bar-cluster-lead {
179
+ flex: 0 1 auto;
180
+ }
181
+
182
+ /* Inside a cluster a button carries no chrome of its own -- the card is
183
+ already the box. */
184
+ .fb-bar-cluster .fb-button {
185
+ background: transparent;
186
+ border-color: transparent;
187
+ height: 32px;
188
+ }
189
+
190
+ .fb-bar-cluster .fb-button:hover {
191
+ background: var(--fb-hover);
192
+ border-color: transparent;
193
+ }
194
+
195
+ /* A control that is its own cluster. Export is the only one on the page: it is
196
+ the end of the work rather than a step in it, and standing alone is how it
197
+ says so.
198
+
199
+ Two classes, deliberately: .fb-button sets a height further down the file,
200
+ and a single-class rule up here would lose to it on source order alone. */
201
+ .fb-button.fb-bar-solo {
202
+ box-shadow: var(--fb-float);
203
+ flex: 0 0 auto;
204
+ height: 44px;
205
+ padding: 0 var(--space-5);
206
+ }
207
+
208
+ /* Related controls inside one card, separated rather than boxed again. */
209
+ .fb-bar-divider {
210
+ background: var(--border-subtle);
211
+ flex: 0 0 auto;
212
+ height: 20px;
213
+ margin: 0 4px;
214
+ width: 1px;
215
+ }
216
+
217
+ /* No longer a pill of its own: the cluster is the box, and this is the row
218
+ inside it. */
219
+ .fb-topbar-group {
220
+ align-items: center;
221
+ display: flex;
222
+ gap: 2px;
223
+ }
224
+
225
+ .fb-back {
226
+ align-items: center;
227
+ border-radius: 8px;
228
+ color: var(--text-muted);
229
+ display: inline-flex;
230
+ height: 32px;
231
+ justify-content: center;
232
+ text-decoration: none;
233
+ width: 32px;
234
+ }
235
+
236
+ .fb-back:hover {
237
+ background: var(--fb-hover);
238
+ color: var(--text-primary);
239
+ }
240
+
241
+ .fb-title-input {
242
+ background: transparent;
243
+ border: 1px solid transparent;
244
+ border-radius: 8px;
245
+ color: var(--text-primary);
246
+ font-size: 15px;
247
+ font-weight: 600;
248
+ max-width: 340px;
249
+ min-width: 140px;
250
+ padding: 6px var(--space-2);
251
+ }
252
+
253
+ .fb-title-input:hover {
254
+ background: var(--fb-hover);
255
+ }
256
+
257
+ .fb-title-input:focus {
258
+ background: var(--surface-1);
259
+ border-color: var(--accent-channel);
260
+ box-shadow: 0 0 0 3px var(--fb-tint);
261
+ outline: none;
262
+ }
263
+
264
+ /* One word, always in the same place. A status that moves, or that only
265
+ appears when something is wrong, is a status nobody learns to read. */
266
+ .fb-save-status {
267
+ color: var(--text-muted);
268
+ font-size: 12px;
269
+ min-width: 92px;
270
+ }
271
+
272
+ .fb-status-failed,
273
+ .fb-status-unreadable {
274
+ color: #d64545;
275
+ }
276
+
277
+ .fb-status-conflict {
278
+ color: #b8770f;
279
+ }
280
+
281
+ .fb-icon-button {
282
+ align-items: center;
283
+ background: transparent;
284
+ border: none;
285
+ border-radius: 8px;
286
+ color: var(--text-secondary);
287
+ cursor: pointer;
288
+ display: inline-flex;
289
+ font-size: 12px;
290
+ height: 30px;
291
+ justify-content: center;
292
+ padding: 0;
293
+ width: 32px;
294
+ }
295
+
296
+ .fb-icon-button:hover:not(:disabled) {
297
+ background: var(--fb-hover);
298
+ color: var(--text-primary);
299
+ }
300
+
301
+ /* Dimmed rather than hidden: Undo that disappears when there is nothing to undo
302
+ is a button that seems to move around the bar. */
303
+ .fb-icon-button:disabled {
304
+ cursor: default;
305
+ opacity: 0.35;
306
+ }
307
+
308
+ .fb-button {
309
+ align-items: center;
310
+ background: var(--surface-1);
311
+ border: 1px solid var(--border-subtle);
312
+ border-radius: 9px;
313
+ color: var(--text-primary);
314
+ cursor: pointer;
315
+ display: inline-flex;
316
+ font-size: 13px;
317
+ font-weight: 500;
318
+ gap: var(--space-2);
319
+ height: 34px;
320
+ padding: 0 var(--space-3);
321
+ text-decoration: none;
322
+ }
323
+
324
+ /* The Download link in the export dialog is an <a> that is `hidden` until
325
+ there is a file. `display: inline-flex` beats the UA's [hidden] rule, so a
326
+ class that sets display has to answer for it -- without this the dialog
327
+ offers a download of nothing from the moment it opens. */
328
+ .fb-button[hidden] {
329
+ display: none;
330
+ }
331
+
332
+ .fb-button:hover {
333
+ background: var(--surface-2);
334
+ border-color: var(--border-strong);
335
+ }
336
+
337
+ .fb-button-primary {
338
+ background: var(--accent-channel);
339
+ border-color: transparent;
340
+ color: var(--fb-on-accent);
341
+ font-weight: 600;
342
+ padding: 0 var(--space-4);
343
+ }
344
+
345
+ .fb-button-primary:hover {
346
+ background: #1d4fd8;
347
+ border-color: transparent;
348
+ }
349
+
350
+ .fb-select {
351
+ background: var(--surface-1);
352
+ border: 1px solid var(--border-subtle);
353
+ border-radius: var(--radius-sm);
354
+ color: var(--text-primary);
355
+ font-size: 13px;
356
+ max-width: 220px;
357
+ padding: 5px var(--space-2);
358
+ }
359
+
360
+ /* Inside a topbar group the select carries no chrome of its own: the group is
361
+ already a box, and a box inside a box reads as two controls. */
362
+ .fb-select-quiet {
363
+ background: transparent;
364
+ border-color: transparent;
365
+ font-size: 12px;
366
+ height: 30px;
367
+ max-width: 150px;
368
+ padding: 0 var(--space-2);
369
+ }
370
+
371
+ .fb-zoom-readout {
372
+ background: transparent;
373
+ border: none;
374
+ border-radius: 8px;
375
+ color: var(--text-secondary);
376
+ cursor: pointer;
377
+ font-size: 12px;
378
+ font-variant-numeric: tabular-nums;
379
+ height: 30px;
380
+ min-width: 52px;
381
+ padding: 0 2px;
382
+ text-align: center;
383
+ }
384
+
385
+ .fb-zoom-readout:hover {
386
+ background: var(--fb-hover);
387
+ color: var(--text-primary);
388
+ }
389
+
390
+ /* A card like everything else on the desk. Full-bleed, it was the one element
391
+ left that still cut the window in half. */
392
+ .fb-banner {
393
+ align-items: flex-start;
394
+ background: var(--surface-2);
395
+ border-left: 3px solid var(--border-strong);
396
+ border-radius: var(--fb-card-radius);
397
+ box-shadow: var(--fb-float);
398
+ display: flex;
399
+ flex: 0 0 auto;
400
+ gap: var(--space-3);
401
+ margin: var(--fb-gap) var(--fb-gap) 0;
402
+ padding: var(--space-3);
403
+ }
404
+
405
+ .fb-banner[hidden] {
406
+ display: none;
407
+ }
408
+
409
+ .fb-banner-warning {
410
+ background: #fdf6e7;
411
+ border-left-color: #d99b1c;
412
+ }
413
+
414
+ .fb-banner-danger {
415
+ background: #fdefef;
416
+ border-left-color: #d64545;
417
+ }
418
+
419
+ .fb-banner-detail {
420
+ color: var(--text-secondary);
421
+ font-size: 12px;
422
+ margin-top: 2px;
423
+ }
424
+
425
+ .fb-banner-actions {
426
+ display: flex;
427
+ gap: var(--space-2);
428
+ margin-top: var(--space-2);
429
+ }
430
+
431
+ /* No columns. The canvas is the only thing laid out here; the rail and the
432
+ tray are positioned against this box and lie on top of it. Quick Edit is the
433
+ one panel that still takes width away, because reframing a panel means
434
+ watching the figure change while you do it. */
435
+ .fb-body {
436
+ display: flex;
437
+ flex: 1 1 auto;
438
+ min-height: 0;
439
+ position: relative;
440
+ }
441
+
442
+ /* -- the floating cards -------------------------------------------------- */
443
+
444
+ .fb-card {
445
+ background: var(--surface-1);
446
+ border: 1px solid var(--border-subtle);
447
+ border-radius: var(--fb-card-radius);
448
+ box-shadow: var(--fb-float);
449
+ position: absolute;
450
+ z-index: 50;
451
+ }
452
+
453
+ /* -- the tool rail ----------------------------------------------------- */
454
+ /*
455
+ Level 1: the only controls that are always visible, and there are four.
456
+ Everything that acts on a selection is in the floating bar -- which is where
457
+ Scale bar and Legend went; everything rare is in a menu. There is no Select
458
+ tool: the drawing tools are one-shot, so "no tool" is where the pointer
459
+ already is nearly all the time.
460
+
461
+ Vertically centred rather than pinned to the top, and only as tall as it
462
+ needs to be. A rail that runs the full height is a column with a background,
463
+ which is the thing this stopped being.
464
+
465
+ Labelled, at the cost of twenty pixels of width: four icons in a stack with
466
+ nothing under them is four things to hover before starting, every session,
467
+ because "a square" is not a word. */
468
+
469
+ .fb-tool-rail {
470
+ align-items: center;
471
+ display: flex;
472
+ flex-direction: column;
473
+ gap: 2px;
474
+ left: var(--fb-gap);
475
+ max-height: calc(100% - var(--fb-gap) * 2);
476
+ overflow-y: auto;
477
+ padding: var(--space-2) 0;
478
+ scrollbar-width: none;
479
+ top: 50%;
480
+ transform: translateY(-50%);
481
+ width: var(--fb-rail);
482
+ }
483
+
484
+ .fb-tool {
485
+ align-items: center;
486
+ background: transparent;
487
+ border: none;
488
+ border-radius: 10px;
489
+ color: var(--text-secondary);
490
+ cursor: pointer;
491
+ display: flex;
492
+ flex: 0 0 auto;
493
+ flex-direction: column;
494
+ font-size: 15px;
495
+ gap: 5px;
496
+ height: 52px;
497
+ justify-content: center;
498
+ margin: 0;
499
+ padding: 0;
500
+ transition: background var(--duration-fast, 120ms) ease,
501
+ color var(--duration-fast, 120ms) ease;
502
+ width: 52px;
503
+ }
504
+
505
+ .fb-tool-label {
506
+ font-size: 9.5px;
507
+ font-weight: 500;
508
+ letter-spacing: 0.01em;
509
+ line-height: 1;
510
+ }
511
+
512
+ .fb-tool:hover:not(:disabled) {
513
+ background: var(--fb-hover);
514
+ color: var(--text-primary);
515
+ }
516
+
517
+ /* A tinted tile rather than a solid block of accent. Panels is lit for as long
518
+ as the tray is open, which is most of the time, and one saturated square
519
+ sitting beside the page for all of it pulls the eye off the page. */
520
+ .fb-tool.is-active {
521
+ background: var(--fb-tint);
522
+ color: var(--accent-channel);
523
+ }
524
+
525
+ .fb-tool:disabled {
526
+ cursor: default;
527
+ opacity: 0.4;
528
+ }
529
+
530
+ /* Counts what is waiting in the tray, on the button that opens it. The tray is
531
+ shut most of the time now, and a capture arriving into a closed drawer with
532
+ nothing to say so is a capture the user thinks was lost. */
533
+ .fb-tool-badge {
534
+ background: var(--accent-channel);
535
+ border: 2px solid var(--surface-1);
536
+ border-radius: 999px;
537
+ color: var(--fb-on-accent);
538
+ font-size: 9px;
539
+ font-weight: 700;
540
+ line-height: 1.5;
541
+ min-width: 18px;
542
+ padding: 0 4px;
543
+ position: absolute;
544
+ right: 2px;
545
+ text-align: center;
546
+ top: 2px;
547
+ }
548
+
549
+ .fb-tool-badge:empty {
550
+ display: none;
551
+ }
552
+
553
+ .fb-tool-holder {
554
+ flex: 0 0 auto;
555
+ position: relative;
556
+ }
557
+
558
+ .fb-tool-rail-gap {
559
+ border-top: 1px solid var(--border-subtle);
560
+ flex: 0 0 auto;
561
+ margin: var(--space-1) 0;
562
+ width: 28px;
563
+ }
564
+
565
+ /* -- the text sidebar --------------------------------------------------
566
+ Contextual, not fixed: shown only while a text object is selected or is
567
+ being typed into. That is the difference from the properties column this
568
+ workspace removed, which was three hundred permanent pixels spent on
569
+ controls that were relevant some of the time.
570
+
571
+ Built from core's tokens, so it takes the workspace's light palette without
572
+ knowing that is what it is doing. */
573
+ /* Height comes from the contents, not from the window. It was pinned to both
574
+ `top` and `bottom`, which left a short panel as a tall card with a third of it
575
+ empty -- and the emptiness reads as "something failed to load" rather than as
576
+ "there is nothing more to set". */
577
+ /* -- every secondary sidebar, at the same size and in the same place ----- */
578
+ /*
579
+ The tray and the text panel share ONE strip beside the rail, and only one of
580
+ them is in it at a time (FigureWorkspace.showSidebar). They used to differ in
581
+ every way two panels can: the tray was pinned top AND bottom while the panel
582
+ hung from the top alone, and their z-indexes were 49 and 6 -- so opening the
583
+ second drew it underneath the first, invisible and still taking clicks.
584
+
585
+ Centred on the canvas rather than hung from its top edge, which is what the
586
+ rail beside them already does. A short panel starting level with a tall one
587
+ and stopping a third of the way down reads as a panel that failed to finish
588
+ loading; centred, it reads as a panel with three controls in it. */
589
+ .fb-side-panel {
590
+ display: flex;
591
+ flex-direction: column;
592
+ left: calc(var(--fb-rail) + var(--fb-gap) * 2);
593
+ max-height: calc(100% - var(--fb-gap) * 2);
594
+ min-height: 0;
595
+ position: absolute;
596
+ top: 50%;
597
+ transform: translateY(-50%);
598
+ /* Below .fb-card's own 50, which is where the tray already sat -- kept so
599
+ this change moves the panels and nothing else. */
600
+ z-index: 49;
601
+ }
602
+
603
+ .fb-side-panel[hidden] { display: none; }
604
+
605
+ /* Title on the left, whatever the panel counts in the middle, its way out on
606
+ the right -- in that order, at that height, for every one of them. The close
607
+ button used to be the tray's alone, so the text panel was a card that
608
+ appeared by itself and could only be dismissed by deselecting.
609
+
610
+ The rule underneath is what separates the panel's NAME from its contents. It
611
+ matters most on the text panel, whose first control is a full-width select:
612
+ without it the title looked like a caption belonging to that select rather
613
+ than to the card. Drawn here and not per panel, so every sidebar is divided
614
+ in the same place. */
615
+ .fb-side-heading {
616
+ align-items: center;
617
+ border-bottom: 1px solid var(--border-subtle);
618
+ display: flex;
619
+ gap: var(--space-2);
620
+ margin-bottom: var(--space-3);
621
+ padding: var(--space-3) var(--space-2) var(--space-2) var(--space-3);
622
+ }
623
+
624
+ .fb-side-title {
625
+ color: var(--text-primary);
626
+ flex: 1;
627
+ font-size: 14px;
628
+ font-weight: 600;
629
+ }
630
+
631
+ /* Wide enough for the row layout, which is what it is all spent on: the name
632
+ column and a segmented track of four buttons have to be legible side by side,
633
+ and at the 232px this panel started at "Horizontal align" wrapped onto two
634
+ lines. 260 stopped the wrapping and left the four alignment buttons 32px
635
+ each -- square icon buttons packed edge to edge, with the track's own frame
636
+ touching them. 300 is what gives every control room to be a control. */
637
+ .fb-text-panel {
638
+ gap: 0;
639
+ overflow-y: auto;
640
+ /* 16 rather than 12, and part of the same decision as the extra width: a
641
+ card of stacked controls reads as a form when there is air around the
642
+ block and as a strip of widgets when there is not. */
643
+ padding: 0 var(--space-4) var(--space-4);
644
+ width: 300px;
645
+ }
646
+
647
+ /* The heading is flush with the card and the controls are inset, which is the
648
+ tray's arrangement too -- so the two line up along their left edge whichever
649
+ one is showing.
650
+
651
+ The separator therefore runs the width of the CONTENT, not of the card: the
652
+ panel scrolls, `overflow-y: auto` makes `overflow-x` auto with it, and a rule
653
+ pulled out to the card's edges with negative margins would put a horizontal
654
+ scrollbar on a panel 232px wide. Ranged with the controls is the right answer
655
+ anyway -- it is the top of the content, and it starts where the content
656
+ does. */
657
+ .fb-text-panel .fb-side-heading {
658
+ padding-left: 0;
659
+ padding-right: 0;
660
+ padding-top: var(--space-3);
661
+ }
662
+
663
+ /* -- one property per row ------------------------------------------------ */
664
+ /*
665
+ Name on the left, control on the right, and every control starting at the
666
+ same x. A GRID and not a flex row with `space-between`: flex would range each
667
+ control against its own name, so the column of fields would step in and out
668
+ by the length of the word beside it and there would be no column at all.
669
+
670
+ The name column is a fixed 104px because that is what the longest of them
671
+ ("Horizontal align") needs at this size, with a little air after it; sizing it
672
+ `auto` would let one long name move every control on the panel.
673
+
674
+ The row is taller than the control in it -- 38 against 30 -- so the panel is a
675
+ list of properties with space between them rather than a stack of boxes. */
676
+ .fb-field {
677
+ align-items: center;
678
+ display: grid;
679
+ gap: var(--space-2);
680
+ grid-template-columns: 104px minmax(0, 1fr);
681
+ min-height: 38px;
682
+ }
683
+
684
+ /* Quieter than the panel's title and quieter than the values, which is the
685
+ order they matter in: the name is what you scan past to reach the control. */
686
+ .fb-field-name {
687
+ color: var(--text-secondary);
688
+ font-size: 12px;
689
+ line-height: 1.3;
690
+ }
691
+
692
+ .fb-field-control {
693
+ align-items: center;
694
+ display: flex;
695
+ gap: var(--space-2);
696
+ min-width: 0;
697
+ }
698
+
699
+ /* `min-width: 0` on the children as well: a grid or flex item's default minimum
700
+ is its CONTENT, so a long font name would push the control column wider than
701
+ the panel and hand it a horizontal scrollbar. */
702
+ .fb-field-control > .fb-input,
703
+ .fb-field-control > .fb-segmented,
704
+ .fb-field-control > .fb-stepper,
705
+ .fb-field-control > .fb-input-color {
706
+ /* Stated rather than inherited: the only thing setting it on this page is
707
+ Bootstrap's reboot, which vendor_bundle.js injects at runtime, and one
708
+ fixed height is not worth making conditional on a bundle loading. */
709
+ box-sizing: border-box;
710
+ }
711
+
712
+ .fb-field-control > .fb-input,
713
+ .fb-field-control > .fb-segmented,
714
+ .fb-field-control > .fb-stepper {
715
+ flex: 1 1 auto;
716
+ min-width: 0;
717
+ }
718
+
719
+ /* One height for every control in the column, so the panel reads down a single
720
+ edge. The select is the odd one out -- .fb-input is sized by its padding and
721
+ ends up a pixel or two short of the two controls drawn with an explicit
722
+ height -- and a `min-height` settles it without touching .fb-input anywhere
723
+ else on the page. */
724
+ .fb-field-control > .fb-input { min-height: var(--fb-control-h); }
725
+
726
+ /* The swatch keeps its own width; the hex field takes what is left. */
727
+ .fb-field-control > .fb-input-color { flex: 0 0 auto; }
728
+
729
+ /* -- focus inside a sidebar ---------------------------------------------- */
730
+ /*
731
+ viewer.css declares a bare `:focus-visible { outline: 2px solid
732
+ var(--accent-channel); outline-offset: 2px }` and loads AFTER this file, so
733
+ with no rule here every field on this panel drew a fat blue rectangle two
734
+ pixels clear of itself -- floating over the row above, over the row below,
735
+ and over the card's own edge, because an outline takes no space in layout.
736
+ On a 300px panel of stacked controls that reads as damage rather than as
737
+ focus.
738
+
739
+ Replaced, not removed: the ring is how a keyboard user finds their place, and
740
+ deleting it here would be a real regression. What goes in its place is the
741
+ control's own frame lighting up -- the border takes the accent and a soft ring
742
+ sits just outside it -- which is inside the box, so nothing moves and nothing
743
+ overlaps.
744
+
745
+ Two classes beat one pseudo-class whatever order the files load in, which is
746
+ why this is scoped to the panel rather than written `:focus-visible`. */
747
+ .fb-side-panel :focus-visible {
748
+ outline: none;
749
+ }
750
+
751
+ .fb-side-panel .fb-input:focus,
752
+ .fb-side-panel .fb-stepper:focus-within {
753
+ border-color: var(--accent-channel);
754
+ box-shadow: 0 0 0 2px var(--accent-channel-soft);
755
+ }
756
+
757
+ .fb-side-panel .fb-segmented-button:focus-visible,
758
+ .fb-side-panel .fb-icon-button:focus-visible,
759
+ .fb-side-panel input[type="checkbox"]:focus-visible {
760
+ box-shadow: 0 0 0 2px var(--accent-channel-soft);
761
+ }
762
+
763
+ /* -- the size stepper ---------------------------------------------------- */
764
+ /*
765
+ One control, not three: a shared frame with the field between its two
766
+ buttons, so "minus, 14, plus" reads as a single thing to set rather than as a
767
+ number that happens to have buttons near it. The frame is .fb-input's, drawn
768
+ here rather than by nesting one, because a nested .fb-input would put a
769
+ border inside a border. */
770
+ .fb-stepper {
771
+ align-items: stretch;
772
+ background: var(--surface-2);
773
+ border: 1px solid var(--border-subtle);
774
+ border-radius: var(--radius-sm);
775
+ display: flex;
776
+ height: var(--fb-control-h);
777
+ overflow: hidden;
778
+ }
779
+
780
+ .fb-stepper-button {
781
+ align-items: center;
782
+ background: transparent;
783
+ border: none;
784
+ color: var(--text-secondary);
785
+ cursor: pointer;
786
+ display: flex;
787
+ flex: 0 0 auto;
788
+ font-size: 10px;
789
+ justify-content: center;
790
+ padding: 0;
791
+ transition: background-color 100ms ease, color 100ms ease;
792
+ width: 26px;
793
+ }
794
+
795
+ .fb-stepper-button:hover {
796
+ background: var(--fb-hover);
797
+ color: var(--text-primary);
798
+ }
799
+
800
+ .fb-stepper-value {
801
+ background: transparent;
802
+ border: none;
803
+ color: var(--text-primary);
804
+ flex: 1 1 auto;
805
+ font-size: 13px;
806
+ font-variant-numeric: tabular-nums;
807
+ min-width: 0;
808
+ padding: 0;
809
+ text-align: center;
810
+ }
811
+
812
+ .fb-stepper-value:focus { outline: none; }
813
+
814
+ /* Nothing here suppresses the platform spinners any more, because there are
815
+ none to suppress: the field is `type="text"` with `inputmode="decimal"`. It
816
+ was a number input, and a number input reports `selectionStart` as null --
817
+ so the panel could not put the caret back after its own redraw and typing
818
+ "20" produced 02. The two buttons were always doing the spinners' job
819
+ anyway. FigureTextPanel.stepper carries the rest of that reasoning. */
820
+
821
+ /* -- the hex code beside the swatch -------------------------------------- */
822
+ /*
823
+ The swatch alone can only be read by looking at it, which is no use when the
824
+ answer has to go in a methods section or match a co-author's. Tabular figures
825
+ so the six characters do not shift width as they are typed. */
826
+ .fb-input-hex {
827
+ font-size: 12px;
828
+ font-variant-numeric: tabular-nums;
829
+ letter-spacing: 0.02em;
830
+ text-transform: lowercase;
831
+ }
832
+
833
+ /* -- segmented controls -------------------------------------------------- */
834
+ /*
835
+ One track holding mutually exclusive choices, rather than loose icon buttons
836
+ in a row: an alignment has exactly one answer at a time, and the shared track
837
+ is what says so before anything has been pressed. The pressed state is a
838
+ raised tile rather than a filled accent block -- four of those in a column
839
+ was more colour than the rest of this workspace uses anywhere. */
840
+ .fb-segmented {
841
+ background: var(--surface-2);
842
+ border: 1px solid var(--border-subtle);
843
+ border-radius: var(--radius-sm);
844
+ display: flex;
845
+ gap: 2px;
846
+ height: var(--fb-control-h);
847
+ padding: 2px;
848
+ }
849
+
850
+ .fb-segmented-button {
851
+ align-items: center;
852
+ background: transparent;
853
+ border: none;
854
+ border-radius: calc(var(--radius-sm) - 2px);
855
+ color: var(--text-secondary);
856
+ cursor: pointer;
857
+ display: inline-flex;
858
+ flex: 1 1 0;
859
+ font-size: 12px;
860
+ justify-content: center;
861
+ min-width: 0;
862
+ padding: 0;
863
+ }
864
+
865
+ .fb-segmented-button:hover:not(:disabled) {
866
+ background: var(--fb-hover);
867
+ color: var(--text-primary);
868
+ }
869
+
870
+ /* Tinted and ringed, not merely raised. The lifted-tile treatment works on a
871
+ grey track against a grey card, and Bold sitting on a WHITE card was a white
872
+ tile on a near-white ground -- so the one thing the button had to say, that
873
+ the mark is currently on, was the thing that could not be seen. It also has
874
+ to be seen from across the panel, because it is what tells you a second click
875
+ will take the bold off again. */
876
+ .fb-segmented-button.is-on {
877
+ background: var(--fb-tint);
878
+ box-shadow: inset 0 0 0 1px var(--accent-channel-soft);
879
+ color: var(--accent-channel);
880
+ }
881
+
882
+ .fb-segmented-button.is-on:hover {
883
+ background: var(--accent-channel-soft);
884
+ }
885
+
886
+ /* -- the colour swatch and the unit suffix ------------------------------- */
887
+ /*
888
+ A bare <input type="color"> draws the platform's own bevelled well, which was
889
+ the one control on the panel that matched nothing else on the page. Stripped
890
+ back to a swatch inside an .fb-input frame. */
891
+ /* Two classes, because .fb-input is declared further down this file and its
892
+ `width: 100%` would otherwise win the cascade on equal specificity and
893
+ stretch the swatch across the row.
894
+
895
+ `width` and `height`, and NOT the logical `inline-size`/`block-size` this
896
+ block used to be written in. They are the same property in a horizontal
897
+ writing mode, so the `width: auto` that was left below them -- sorted after
898
+ `padding`, the way every block in this file is sorted -- simply won, and the
899
+ swatch collapsed to its 3px of padding and its border: an eight-pixel slice
900
+ of colour beside the hex field, which read as the swatch being missing
901
+ rather than as the swatch being small. One physical property per dimension
902
+ is the only way that cannot happen again. */
903
+ .fb-input.fb-input-color {
904
+ cursor: pointer;
905
+ height: var(--fb-control-h);
906
+ padding: 3px;
907
+ width: 44px;
908
+ }
909
+
910
+ .fb-input-color::-webkit-color-swatch-wrapper { padding: 0; }
911
+
912
+ .fb-input-color::-webkit-color-swatch {
913
+ border: none;
914
+ border-radius: 2px;
915
+ }
916
+
917
+ .fb-input-color::-moz-color-swatch {
918
+ border: none;
919
+ border-radius: 2px;
920
+ }
921
+
922
+ /* "deg" spelled out took as much room as the field it labelled. */
923
+ .fb-input-suffixed {
924
+ align-items: center;
925
+ display: inline-flex;
926
+ gap: 4px;
927
+ }
928
+
929
+ .fb-input-suffix {
930
+ color: var(--text-muted);
931
+ font-size: 11px;
932
+ }
933
+
934
+ /* The row's name is the label, so the box stands on its own at the left of the
935
+ control column -- level with the fields above it, not centred in a cell of
936
+ its own. */
937
+ .fb-text-panel input[type="checkbox"] {
938
+ accent-color: var(--accent-channel);
939
+ height: 15px;
940
+ margin: 0;
941
+ width: 15px;
942
+ }
943
+
944
+ /* -- the panel tray ----------------------------------------------------- */
945
+ /*
946
+ A card the rail opens, with its own title and its own way out. It is the
947
+ only panel this page has, and the one place captured panels land.
948
+
949
+ Full height, unlike the rail: it is a list, and a list wants the window. */
950
+
951
+ /* Geometry, centring and z are .fb-side-panel's. What is left here is the one
952
+ thing that is genuinely the tray's: it is a LIST, so it takes the height it
953
+ is allowed rather than the height of its contents. */
954
+ .fb-tray-panel {
955
+ height: calc(100% - var(--fb-gap) * 2);
956
+ overflow: hidden;
957
+ width: var(--fb-tray);
958
+ }
959
+
960
+ .fb-tray-count {
961
+ background: var(--surface-2);
962
+ border-radius: 999px;
963
+ color: var(--text-muted);
964
+ font-size: 11px;
965
+ font-variant-numeric: tabular-nums;
966
+ padding: 1px 7px;
967
+ }
968
+
969
+ .fb-tray-count:empty {
970
+ display: none;
971
+ }
972
+
973
+ /* The tray's one action, at the width of the card. A small button floating in
974
+ a column this narrow reads as something left behind rather than as the way
975
+ in -- and on an empty tray it is the only thing to do. */
976
+ .fb-tray-add {
977
+ padding: 0 var(--space-3) var(--space-2);
978
+ }
979
+
980
+ .fb-tray-add-button {
981
+ justify-content: center;
982
+ width: 100%;
983
+ }
984
+
985
+ /* A filter, dressed as one.
986
+
987
+ It narrows the list under it as the characters arrive: there is nothing to
988
+ submit and nothing to get wrong, so the field is a recess in the card rather
989
+ than a control with a border. What it had was a three-pixel accent halo on
990
+ focus, which announced a commitment it never asks for -- in the one colour
991
+ this page reserves for "this is selected". */
992
+ .fb-tray-search {
993
+ align-items: center;
994
+ background: var(--surface-2);
995
+ border: 1px solid transparent;
996
+ border-radius: 9px;
997
+ color: var(--text-muted);
998
+ display: flex;
999
+ font-size: 11px;
1000
+ gap: var(--space-2);
1001
+ margin: 0 var(--space-3) var(--space-2);
1002
+ padding: 0 var(--space-2) 0 var(--space-3);
1003
+ }
1004
+
1005
+ .fb-tray-search:focus-within {
1006
+ background: var(--surface-1);
1007
+ border-color: var(--border-strong);
1008
+ }
1009
+
1010
+ .fb-tray-search[hidden] {
1011
+ display: none;
1012
+ }
1013
+
1014
+ .fb-tray-search input {
1015
+ appearance: none;
1016
+ background: transparent;
1017
+ border: none;
1018
+ color: var(--text-primary);
1019
+ flex: 1 1 auto;
1020
+ font-family: inherit;
1021
+ font-size: 12px;
1022
+ height: 32px;
1023
+ min-width: 0;
1024
+ outline: none;
1025
+ padding: 0;
1026
+ }
1027
+
1028
+ /* WebKit draws its own clear button for type=search, at a size and a moment of
1029
+ its own choosing and in one engine only. The × beside this is ours, and it
1030
+ is in every browser. */
1031
+ .fb-tray-search input::-webkit-search-cancel-button,
1032
+ .fb-tray-search input::-webkit-search-decoration {
1033
+ appearance: none;
1034
+ display: none;
1035
+ }
1036
+
1037
+ .fb-tray-search-clear {
1038
+ background: transparent;
1039
+ border: none;
1040
+ border-radius: 6px;
1041
+ color: var(--text-muted);
1042
+ cursor: pointer;
1043
+ flex: 0 0 auto;
1044
+ font-size: 10px;
1045
+ height: 22px;
1046
+ padding: 0;
1047
+ width: 22px;
1048
+ }
1049
+
1050
+ .fb-tray-search-clear:hover {
1051
+ background: var(--fb-hover);
1052
+ color: var(--text-primary);
1053
+ }
1054
+
1055
+ .fb-tray-search-clear[hidden] {
1056
+ display: none;
1057
+ }
1058
+
1059
+ /* Two across, mostly picture, and as tall as it needs to be.
1060
+
1061
+ A panel is recognised by what it LOOKS like -- this is a tray of
1062
+ photographs, and "panel_3" and "panel_4" are not how anyone tells two fields
1063
+ of the same slide apart. One per row spent two thirds of the card's width on
1064
+ a filename and showed six thumbnails; two across shows twelve at nearly two
1065
+ and a half times the area each.
1066
+
1067
+ Not three: the tray would have to be wider than the sheet's margin for a
1068
+ third column to be worth looking at, and it is the page that should have the
1069
+ width.
1070
+
1071
+ WRAPPED FLEX, not a grid, and that is not a style preference. A grid row is
1072
+ sized before its items are laid out, and a tile whose height comes from an
1073
+ aspect ratio applied to a percentage width has no height to offer at that
1074
+ point: every row came out a couple of dozen pixels tall and every thumbnail
1075
+ spilled over the two below it. A flex line takes its height from items whose
1076
+ widths are already definite -- `calc()` of the card's own width -- so the
1077
+ line is as tall as the taller of its two tiles and nothing overlaps.
1078
+
1079
+ `align-items: flex-start` for the same reason the grid wanted it: stretched
1080
+ to the line, a wide strip beside a tall crop would gain a band of empty card
1081
+ under its picture and stop being the shape it is. Uneven bottoms are what a
1082
+ tray of photographs looks like; the card scrolls. */
1083
+ .fb-tray-strip {
1084
+ align-content: flex-start;
1085
+ align-items: flex-start;
1086
+ display: flex;
1087
+ flex: 1 1 auto;
1088
+ flex-wrap: wrap;
1089
+ gap: var(--space-2);
1090
+ min-height: 0;
1091
+ overflow-y: auto;
1092
+ padding: 0 var(--space-3) var(--space-3);
1093
+ scrollbar-width: thin;
1094
+ }
1095
+
1096
+ /* Two per line exactly: the pair plus the gap between them is the whole width,
1097
+ so a third can never fit and neither can be squeezed. `border-box` is not
1098
+ inherited from anywhere on this page -- without it the border would push the
1099
+ pair past 100% and the tray would silently go back to one per line. */
1100
+ .fb-tray-item {
1101
+ border: 1px solid var(--border-subtle);
1102
+ border-radius: 10px;
1103
+ box-sizing: border-box;
1104
+ cursor: grab;
1105
+ display: block;
1106
+ flex: 0 0 auto;
1107
+ min-width: 0;
1108
+ overflow: hidden;
1109
+ position: relative;
1110
+ width: calc((100% - var(--space-2)) / 2);
1111
+ }
1112
+
1113
+ .fb-tray-item:hover {
1114
+ border-color: var(--border-strong);
1115
+ }
1116
+
1117
+ /* A ring, not a tint. The tile is almost entirely image now, and tinting the
1118
+ few pixels of card around it is a selection you cannot see. */
1119
+ .fb-tray-item.is-selected {
1120
+ border-color: var(--accent-channel);
1121
+ box-shadow: 0 0 0 2px var(--accent-channel-soft);
1122
+ }
1123
+
1124
+ /* The shape it actually is.
1125
+
1126
+ These were cropped to a common 4:3, which made a 2:1 strip look like a
1127
+ square field and a tall crop look like neither -- in a tray whose whole job
1128
+ is "which one is this", where the answer is the SHAPE as often as it is the
1129
+ content. The tiles are different heights now, and the card scrolls, which is
1130
+ what a tray is for.
1131
+
1132
+ The ratio is set inline from the panel's own viewport, so the tile is the
1133
+ right shape before any of the pictures have arrived: twelve lazy images
1134
+ resizing their tiles as they load would move the one being reached for out
1135
+ from under the pointer. `auto 4 / 3` is what is left for a panel with no
1136
+ viewport worth trusting -- the picture's own ratio once it has loaded, and a
1137
+ box of roughly the right size until then rather than a two-pixel sliver.
1138
+
1139
+ `contain` is the safety net for a preview that does not match the region it
1140
+ was taken from: letterboxed is a gap nobody acts on, cropped is a shape they
1141
+ might. */
1142
+ .fb-tray-item img {
1143
+ aspect-ratio: auto 4 / 3;
1144
+ background: #0b0f15;
1145
+ display: block;
1146
+ height: auto;
1147
+ object-fit: contain;
1148
+ width: 100%;
1149
+ }
1150
+
1151
+ /* The name, on hover.
1152
+
1153
+ Held back rather than dropped: it is the answer to "which of these two
1154
+ similar fields is it", which is a question asked occasionally, and it was
1155
+ being paid for with two thirds of every row permanently. Over the picture
1156
+ rather than under it, so revealing it moves nothing -- a caption that
1157
+ appears in the layout would make the whole grid jump under the pointer. */
1158
+ .fb-tray-item-text {
1159
+ background: linear-gradient(to top, rgba(6, 10, 16, 0.9),
1160
+ rgba(6, 10, 16, 0.6) 55%, rgba(6, 10, 16, 0));
1161
+ bottom: 0;
1162
+ color: #ffffff;
1163
+ display: flex;
1164
+ flex-direction: column;
1165
+ gap: 1px;
1166
+ left: 0;
1167
+ opacity: 0;
1168
+ padding: 16px var(--space-2) 5px;
1169
+ pointer-events: none;
1170
+ position: absolute;
1171
+ right: 0;
1172
+ transition: opacity var(--duration-fast, 120ms) ease;
1173
+ }
1174
+
1175
+ /* Selected tiles keep theirs up: having picked four out of twelve, which four
1176
+ is exactly what you want to be able to read without hovering each one. */
1177
+ .fb-tray-item:hover .fb-tray-item-text,
1178
+ .fb-tray-item.is-selected .fb-tray-item-text {
1179
+ opacity: 1;
1180
+ }
1181
+
1182
+ .fb-tray-item-name {
1183
+ font-size: 11px;
1184
+ overflow: hidden;
1185
+ text-overflow: ellipsis;
1186
+ white-space: nowrap;
1187
+ }
1188
+
1189
+ /* A panel from a source with no physical calibration has nothing to say on the
1190
+ second line. */
1191
+ .fb-tray-item-meta {
1192
+ color: rgba(255, 255, 255, 0.78);
1193
+ font-size: 10px;
1194
+ overflow: hidden;
1195
+ text-overflow: ellipsis;
1196
+ white-space: nowrap;
1197
+ }
1198
+
1199
+ .fb-tray-item-meta:empty {
1200
+ display: none;
1201
+ }
1202
+
1203
+ .fb-tray-empty {
1204
+ padding: 0 var(--space-4) var(--space-4);
1205
+ }
1206
+
1207
+ .fb-tray-empty[hidden] {
1208
+ display: none;
1209
+ }
1210
+
1211
+ .fb-tray-empty p {
1212
+ font-size: 12px;
1213
+ line-height: 1.5;
1214
+ margin: 0 0 var(--space-2);
1215
+ }
1216
+
1217
+ /* A flex item in the strip, so it has to be told to take the whole line rather
1218
+ than sit in half of one with nothing beside it. */
1219
+ .fb-tray-nomatch {
1220
+ font-size: 12px;
1221
+ padding: var(--space-2);
1222
+ width: 100%;
1223
+ }
1224
+
1225
+ /* -- the canvas --------------------------------------------------------- */
1226
+
1227
+ .fb-main {
1228
+ display: flex;
1229
+ flex: 1 1 auto;
1230
+ flex-direction: column;
1231
+ min-height: 0;
1232
+ min-width: 0;
1233
+ position: relative;
1234
+ }
1235
+
1236
+ /* The canvas is a scroll surface, not a viewport: panning a page is scrolling
1237
+ it, which every pointing device already knows how to do. Space-drag is the
1238
+ second way, for the trackpad hand that is already on the page.
1239
+
1240
+ The left padding clears the RAIL, and only the rail. It used to grow when
1241
+ the tray opened, which moved the sheet sideways every time somebody looked
1242
+ at their panels: the page slid, the zoom no longer framed what it had
1243
+ framed, and the thing being composed moved because of a drawer. The tray
1244
+ floats over the desk beside the sheet instead -- on a window wide enough to
1245
+ compose a figure there is desk to spare there, and on one that is not, a
1246
+ card lying over the margin for as long as it is open is a cheaper price than
1247
+ a page that will not hold still. */
1248
+ .fb-canvas-scroll {
1249
+ background: var(--fb-desk);
1250
+ flex: 1 1 auto;
1251
+ min-height: 0;
1252
+ overflow: auto;
1253
+ padding: var(--space-5) var(--space-5) var(--space-4);
1254
+ padding-left: calc(var(--fb-rail) + var(--fb-gap) * 2 + var(--space-4));
1255
+ }
1256
+
1257
+ .fb-canvas-scroll.is-pannable {
1258
+ cursor: grab;
1259
+ }
1260
+
1261
+ .fb-canvas-scroll.is-panning {
1262
+ cursor: grabbing;
1263
+ }
1264
+
1265
+ /* The sheet and the two things that belong to it. Shrink-wrapped and centred
1266
+ as one block, so the size caption sits on the page's left edge and the Add
1267
+ page button under its middle -- both of which drift off it if they are
1268
+ centred independently in a scroll surface. */
1269
+ .fb-sheet {
1270
+ margin: 0 auto;
1271
+ width: max-content;
1272
+ }
1273
+
1274
+ .fb-sheet-caption {
1275
+ color: var(--text-muted);
1276
+ font-size: 11px;
1277
+ font-variant-numeric: tabular-nums;
1278
+ padding-bottom: 6px;
1279
+ }
1280
+
1281
+ /* Rulers are fixed to the viewport rather than to the page, so the scroll area
1282
+ makes room for them rather than containing them. */
1283
+ .fb-main.has-rulers .fb-canvas-scroll {
1284
+ margin-left: 20px;
1285
+ margin-top: 20px;
1286
+ }
1287
+
1288
+ .fb-ruler {
1289
+ background: var(--surface-1);
1290
+ display: none;
1291
+ position: absolute;
1292
+ z-index: 30;
1293
+ }
1294
+
1295
+ .fb-main.has-rulers .fb-ruler {
1296
+ display: block;
1297
+ }
1298
+
1299
+ .fb-ruler-h {
1300
+ border-bottom: 1px solid var(--border-subtle);
1301
+ left: 20px;
1302
+ top: 0;
1303
+ }
1304
+
1305
+ .fb-ruler-v {
1306
+ border-right: 1px solid var(--border-subtle);
1307
+ left: 0;
1308
+ top: 20px;
1309
+ }
1310
+
1311
+ .fb-ruler-corner {
1312
+ background: var(--surface-1);
1313
+ border-bottom: 1px solid var(--border-subtle);
1314
+ border-right: 1px solid var(--border-subtle);
1315
+ display: none;
1316
+ height: 20px;
1317
+ left: 0;
1318
+ position: absolute;
1319
+ top: 0;
1320
+ width: 20px;
1321
+ z-index: 31;
1322
+ }
1323
+
1324
+ .fb-main.has-rulers .fb-ruler-corner {
1325
+ display: block;
1326
+ }
1327
+
1328
+ /* -- the status bar ------------------------------------------------------ */
1329
+ /*
1330
+ Zoom lives down here rather than in the topbar, beside the readout it
1331
+ changes and away from the controls that change the DOCUMENT. The topbar acts
1332
+ on the figure -- title, pages, undo, export -- and zoom acts on nothing at
1333
+ all: it is where the eye is, not what the figure is, and the two were sitting
1334
+ in one row looking like the same kind of thing.
1335
+
1336
+ OUTSIDE .fb-body, not inside the canvas. The floating cards are positioned
1337
+ against the body and run to its bottom edge; with the bar in there, the tray
1338
+ ended underneath it -- and raising the tray's z-index instead would have put
1339
+ a panel over a permanent control. Taking the bar out of the box the cards
1340
+ float in makes the collision impossible rather than settled by number.
1341
+
1342
+ Like the topbar, the bar itself is not a surface: the desk runs to the foot
1343
+ of the window and the zoom cluster floats on it. What is on the left stays
1344
+ ambient -- a count and a reminder -- because putting a sentence nobody clicks
1345
+ into a card of its own would give it the weight of a control. */
1346
+
1347
+ .fb-statusbar {
1348
+ align-items: center;
1349
+ background: transparent;
1350
+ display: flex;
1351
+ flex: 0 0 auto;
1352
+ gap: var(--space-3);
1353
+ height: 56px;
1354
+ padding: 0 var(--fb-gap) var(--fb-gap);
1355
+ }
1356
+
1357
+ .fb-statusbar-count {
1358
+ color: var(--text-secondary);
1359
+ font-size: 12px;
1360
+ font-variant-numeric: tabular-nums;
1361
+ padding-left: var(--space-2);
1362
+ }
1363
+
1364
+ .fb-statusbar-hint {
1365
+ color: var(--text-muted);
1366
+ font-size: 11px;
1367
+ overflow: hidden;
1368
+ text-overflow: ellipsis;
1369
+ white-space: nowrap;
1370
+ }
1371
+
1372
+ .fb-statusbar-hint kbd {
1373
+ background: var(--surface-2);
1374
+ border: 1px solid var(--border-subtle);
1375
+ border-radius: 4px;
1376
+ font-family: inherit;
1377
+ font-size: 10px;
1378
+ padding: 1px 4px;
1379
+ }
1380
+
1381
+ /* The one layer FigureCanvas.render() never touches. Anything that has to
1382
+ survive a re-render -- the context bar, an open popover, an in-place text
1383
+ editor -- is mounted here rather than inside the page surface, which is
1384
+ replaced wholesale on every document change. */
1385
+ .fb-overlay-layer {
1386
+ inset: 0;
1387
+ pointer-events: none;
1388
+ position: absolute;
1389
+ z-index: 60;
1390
+ }
1391
+
1392
+ .fb-overlay-layer > * {
1393
+ pointer-events: auto;
1394
+ position: absolute;
1395
+ }
1396
+
1397
+ /* Rounded, bordered, and shadowed just enough to lift. On a light desk a heavy
1398
+ drop shadow is what makes a mockup look like a mockup: what says "sheet of
1399
+ paper" here is the hairline edge, and the shadow only has to stop the white
1400
+ page dissolving into the near-white desk. */
1401
+ /* `isolation` is the fix for a real bug and not a tidy-up. Panels draw at
1402
+ `z-index: z` and annotations at `1000 + z`, and NOTHING between them and the
1403
+ document root made a stacking context -- .fb-page-surface and .fb-page are
1404
+ both positioned with `z-index: auto`, which does not. So a caption at 1001
1405
+ competed directly with .fb-overlay-layer at 60, and won: every popover opened
1406
+ from the floating bar rendered UNDER the object it was about to change, which
1407
+ is the one place it is guaranteed to be over something. Confining those
1408
+ numbers to the page is what makes the overlay reliably above them, and it
1409
+ costs nothing else -- the ordering inside the page is unchanged. */
1410
+ .fb-page {
1411
+ isolation: isolate;
1412
+ background: #ffffff;
1413
+ border-radius: 4px;
1414
+ box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.10),
1415
+ 0 10px 24px rgba(15, 23, 42, 0.10);
1416
+ position: relative;
1417
+ }
1418
+
1419
+ /* A page with no background at all, drawn as the conventional checkerboard.
1420
+ Leaving it literally transparent would show the desk through it, which reads
1421
+ as a page the colour of the app rather than as no page. */
1422
+ .fb-page.is-transparent {
1423
+ background-color: #ffffff;
1424
+ background-image:
1425
+ linear-gradient(45deg, #d6d6d6 25%, transparent 25%, transparent 75%, #d6d6d6 75%),
1426
+ linear-gradient(45deg, #d6d6d6 25%, transparent 25%, transparent 75%, #d6d6d6 75%);
1427
+ background-position: 0 0, 8px 8px;
1428
+ background-size: 16px 16px;
1429
+ }
1430
+
1431
+ /* Under the page and as wide as it, which is where the page it makes would
1432
+ appear. Inside the scroll surface rather than in the status bar, so it
1433
+ scrolls away with the sheet it follows: it is part of the document, not part
1434
+ of the window. */
1435
+ .fb-add-page {
1436
+ align-items: center;
1437
+ background: transparent;
1438
+ border: 1px dashed var(--border-strong);
1439
+ border-radius: 9px;
1440
+ color: var(--text-muted);
1441
+ cursor: pointer;
1442
+ display: flex;
1443
+ font-size: 12px;
1444
+ gap: var(--space-2);
1445
+ justify-content: center;
1446
+ margin-top: var(--space-3);
1447
+ padding: 9px var(--space-4);
1448
+ width: 100%;
1449
+ }
1450
+
1451
+ .fb-add-page:hover {
1452
+ background: var(--surface-1);
1453
+ border-color: var(--accent-channel);
1454
+ border-style: solid;
1455
+ color: var(--accent-channel);
1456
+ }
1457
+
1458
+ /* Grid and margins are page CHILDREN, so a 5 mm square stays 5 mm at every
1459
+ zoom -- drawn in screen pixels they would drift off the millimetres they are
1460
+ supposed to represent the moment the zoom changed. */
1461
+ .fb-page-grid {
1462
+ background-image:
1463
+ linear-gradient(to right, rgba(90, 120, 150, 0.22) 1px, transparent 1px),
1464
+ linear-gradient(to bottom, rgba(90, 120, 150, 0.22) 1px, transparent 1px);
1465
+ inset: 0;
1466
+ pointer-events: none;
1467
+ position: absolute;
1468
+ z-index: 0;
1469
+ }
1470
+
1471
+ .fb-page-grid[hidden],
1472
+ .fb-page-margins[hidden] {
1473
+ display: none;
1474
+ }
1475
+
1476
+ .fb-page-margins {
1477
+ border: 1px dashed rgba(90, 120, 150, 0.5);
1478
+ pointer-events: none;
1479
+ position: absolute;
1480
+ z-index: 0;
1481
+ }
1482
+
1483
+ .fb-page-surface {
1484
+ inset: 0;
1485
+ position: absolute;
1486
+ }
1487
+
1488
+ /* Guides and the marquee sit above the panels and take no pointer events, so a
1489
+ snap line can never be the thing a drag grabs. */
1490
+ .fb-page-guides {
1491
+ inset: 0;
1492
+ pointer-events: none;
1493
+ position: absolute;
1494
+ z-index: 5000;
1495
+ }
1496
+
1497
+ .fb-guide {
1498
+ background: var(--accent-channel);
1499
+ position: absolute;
1500
+ }
1501
+
1502
+ .fb-guide-v {
1503
+ bottom: 0;
1504
+ top: 0;
1505
+ width: 1px;
1506
+ }
1507
+
1508
+ .fb-guide-h {
1509
+ height: 1px;
1510
+ left: 0;
1511
+ right: 0;
1512
+ }
1513
+
1514
+ .fb-marquee {
1515
+ background: rgba(120, 190, 255, 0.12);
1516
+ border: 1px dashed var(--accent-channel);
1517
+ position: absolute;
1518
+ }
1519
+
1520
+ .fb-panel {
1521
+ border: 1px solid transparent;
1522
+ overflow: hidden;
1523
+ position: absolute;
1524
+ touch-action: none;
1525
+ }
1526
+
1527
+ .fb-panel.is-selected {
1528
+ border-color: var(--accent-channel);
1529
+ box-shadow: 0 0 0 1px var(--accent-channel);
1530
+ overflow: visible;
1531
+ }
1532
+
1533
+ /* Handles sit OUTSIDE the panel edge, so grabbing one never means clicking on
1534
+ the image and losing a pixel of the thing being sized. */
1535
+ .fb-handle {
1536
+ background: var(--accent-channel);
1537
+ border: 1px solid #ffffff;
1538
+ border-radius: 2px;
1539
+ height: 9px;
1540
+ position: absolute;
1541
+ width: 9px;
1542
+ z-index: 10;
1543
+ }
1544
+
1545
+ .fb-handle-nw { cursor: nwse-resize; left: -5px; top: -5px; }
1546
+ .fb-handle-ne { cursor: nesw-resize; right: -5px; top: -5px; }
1547
+ .fb-handle-se { cursor: nwse-resize; bottom: -5px; right: -5px; }
1548
+ .fb-handle-sw { cursor: nesw-resize; bottom: -5px; left: -5px; }
1549
+ .fb-handle-n { cursor: ns-resize; left: calc(50% - 4px); top: -5px; }
1550
+ .fb-handle-s { bottom: -5px; cursor: ns-resize; left: calc(50% - 4px); }
1551
+ .fb-handle-e { cursor: ew-resize; right: -5px; top: calc(50% - 4px); }
1552
+ .fb-handle-w { cursor: ew-resize; left: -5px; top: calc(50% - 4px); }
1553
+
1554
+ /* Round, and standing clear above the top edge: on the outline it would sit
1555
+ under the `n` handle, and on a one-line caption those are the same pixels. */
1556
+ .fb-handle-rotate {
1557
+ border-radius: 50%;
1558
+ cursor: grab;
1559
+ left: calc(50% - 4px);
1560
+ top: -20px;
1561
+ }
1562
+
1563
+ .fb-handle-rotate::before {
1564
+ border-left: 1px solid var(--accent-channel);
1565
+ content: "";
1566
+ height: 11px;
1567
+ left: 3px;
1568
+ position: absolute;
1569
+ top: 8px;
1570
+ }
1571
+
1572
+ /* Drawn from what was recorded at capture time, never from the live plugins.
1573
+ Inside the panel here, because on screen that is where it reads; export
1574
+ places it as a vector block beside the raster. */
1575
+ .fb-panel-legend {
1576
+ background: rgba(0, 0, 0, 0.55);
1577
+ border-radius: 2px;
1578
+ display: flex;
1579
+ flex-direction: column;
1580
+ gap: 1px;
1581
+ left: 4px;
1582
+ max-width: 60%;
1583
+ padding: 3px 5px;
1584
+ position: absolute;
1585
+ top: 18px;
1586
+ }
1587
+
1588
+ .fb-legend-row {
1589
+ align-items: center;
1590
+ color: #ffffff;
1591
+ display: flex;
1592
+ font-size: 9px;
1593
+ gap: 4px;
1594
+ line-height: 1.3;
1595
+ overflow: hidden;
1596
+ text-overflow: ellipsis;
1597
+ white-space: nowrap;
1598
+ }
1599
+
1600
+ .fb-legend-swatch {
1601
+ border-radius: 1px;
1602
+ flex: 0 0 auto;
1603
+ height: 7px;
1604
+ width: 7px;
1605
+ }
1606
+
1607
+ .fb-legend-ramp {
1608
+ border-radius: 1px;
1609
+ flex: 0 0 auto;
1610
+ height: 7px;
1611
+ width: 32px;
1612
+ }
1613
+
1614
+ /* Drawn as a real bar rather than as text, and only ever when the source has a
1615
+ physical calibration -- see figureCanvas.scaleBarMarkup. */
1616
+ .fb-panel-scalebar {
1617
+ background: #ffffff;
1618
+ bottom: 8px;
1619
+ box-shadow: 0 0 2px rgba(0, 0, 0, 0.9);
1620
+ height: 3px;
1621
+ position: absolute;
1622
+ right: 6px;
1623
+ }
1624
+
1625
+ .fb-panel-scalebar-label {
1626
+ bottom: 5px;
1627
+ color: #ffffff;
1628
+ font-size: 10px;
1629
+ position: absolute;
1630
+ right: 0;
1631
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
1632
+ white-space: nowrap;
1633
+ }
1634
+
1635
+ /* A panel whose source changed or went missing. Marked, never redrawn: a
1636
+ scientifically established panel is not re-rendered from materially different
1637
+ data because the data moved. */
1638
+ .fb-panel-badge {
1639
+ color: var(--accent-warning, #e0a44c);
1640
+ position: absolute;
1641
+ right: 4px;
1642
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
1643
+ top: 2px;
1644
+ }
1645
+
1646
+ .fb-panel-badge-missing {
1647
+ color: var(--accent-danger);
1648
+ }
1649
+
1650
+ .fb-annotation {
1651
+ position: absolute;
1652
+ }
1653
+
1654
+ /* No `line-height` and no `white-space` any more: the lines are positioned by
1655
+ FigureCanvas.textLayout and drawn as SVG with an explicit baseline, because
1656
+ the browser's own baseline comes from whichever font file it resolved and
1657
+ the PDF's comes from the AFM metrics -- about a quarter of a millimetre
1658
+ apart at 8pt, which no test could reach. The leading now lives in
1659
+ FigureRichText.LINE_HEIGHT, where both languages read it. */
1660
+ .fb-annotation-text {
1661
+ overflow: visible;
1662
+ }
1663
+
1664
+ /* Visible overflow rather than clipped: a box narrower than a long word shows
1665
+ the word running past its edge, which is the truth about what will export. */
1666
+ .fb-text-svg {
1667
+ display: block;
1668
+ overflow: visible;
1669
+ pointer-events: none;
1670
+ }
1671
+
1672
+ .fb-annotation-rect {
1673
+ border: 1px solid currentColor;
1674
+ }
1675
+
1676
+ .fb-annotation-ellipse {
1677
+ border: 1px solid currentColor;
1678
+ border-radius: 50%;
1679
+ }
1680
+
1681
+ .fb-annotation.is-selected {
1682
+ outline: 1px solid var(--accent-channel);
1683
+ }
1684
+
1685
+ /* A line's element is its NORMALISED bounding box plus padding for the
1686
+ arrowhead, which for a long diagonal covers a large empty area. If it took
1687
+ clicks it would be a region where selecting the panels underneath quietly
1688
+ stopped working, so the ink takes them instead: a fat transparent line under
1689
+ the visible one (.fb-stroke-hit). */
1690
+ .fb-annotation-stroke {
1691
+ pointer-events: none;
1692
+ }
1693
+
1694
+ .fb-annotation-stroke.is-selected {
1695
+ outline: none;
1696
+ }
1697
+
1698
+ .fb-annotation-stroke svg {
1699
+ display: block;
1700
+ overflow: visible;
1701
+ }
1702
+
1703
+ .fb-stroke-hit {
1704
+ pointer-events: stroke;
1705
+ stroke: transparent;
1706
+ stroke-width: 12;
1707
+ }
1708
+
1709
+ /* Handles are the exception: they are the only part of a line's box that has to
1710
+ answer a press. */
1711
+ .fb-annotation .fb-handle {
1712
+ pointer-events: auto;
1713
+ }
1714
+
1715
+ /* A line's two ENDS. Round, so they read as points rather than as the corners
1716
+ of a box that is not there. */
1717
+ .fb-handle-point {
1718
+ border-radius: 50%;
1719
+ cursor: crosshair;
1720
+ height: 10px;
1721
+ width: 10px;
1722
+ }
1723
+
1724
+ /* The shape being dragged out, before it exists. In the guides layer, which
1725
+ takes no pointer events and is not what render() replaces. */
1726
+ .fb-draft {
1727
+ background: rgba(120, 190, 255, 0.08);
1728
+ border: 1px dashed var(--accent-channel);
1729
+ position: absolute;
1730
+ }
1731
+
1732
+ .fb-draft-ellipse {
1733
+ border-radius: 50%;
1734
+ }
1735
+
1736
+ /* A drawing tool is armed: the whole surface is a place to start a shape, so
1737
+ the cursor says so rather than the rail 200 pixels away being the only clue. */
1738
+ .fb-page-surface.is-drawing {
1739
+ cursor: crosshair;
1740
+ }
1741
+
1742
+ /* Typing into a text annotation where it sits. Mounted in the overlay layer,
1743
+ because the page surface is replaced on every document change and the change
1744
+ in question is the one the user is making. */
1745
+ /* A contenteditable, not a textarea -- a textarea holds one style for its whole
1746
+ contents, which is the limitation this feature exists to remove. It is
1747
+ absolutely positioned over the annotation it is editing, in the overlay layer
1748
+ that FigureCanvas.render() never touches. */
1749
+ .fb-text-editor {
1750
+ background: var(--surface-0);
1751
+ border: 1px solid var(--accent-channel);
1752
+ border-radius: 2px;
1753
+ margin: 0;
1754
+ padding: 0 1px;
1755
+ position: absolute;
1756
+ white-space: pre-wrap;
1757
+ z-index: 40;
1758
+ }
1759
+
1760
+ /*
1761
+ No browser focus ring on anything that sits ON the sheet.
1762
+
1763
+ viewer.css declares a bare `:focus-visible { outline: 2px solid
1764
+ var(--accent-channel); outline-offset: 2px }`, and on this page it LOADS
1765
+ AFTER this file -- base.html links a plugin's stylesheets in <head>, and
1766
+ workspace.html links viewer.css inside the content block, which is in the
1767
+ body. Both selectors score (0,1,0), so source order decides and viewer.css
1768
+ wins: `.fb-text-editor { outline: none }` above was overruled and never did
1769
+ anything. Clicking into a caption drew a fat blue ring two pixels outside the
1770
+ editor's own blue border, which was itself inside the annotation's blue
1771
+ selection outline. Three blue boxes for one caption.
1772
+
1773
+ Scoped, not global. A focus ring is how a keyboard user finds the rail, the
1774
+ topbar and the sidebars, and removing it there would be a real regression.
1775
+ Inside the page it says nothing: selection on this canvas is drawn by the
1776
+ app -- the thin outline and the handles -- for pointer and keyboard alike,
1777
+ and the ring is a second, louder answer to a question already answered.
1778
+
1779
+ Specificity, not `!important`: two classes beat one pseudo-class whatever the
1780
+ order of the files. */
1781
+ .fb-workspace .fb-page :focus,
1782
+ .fb-workspace .fb-page :focus-visible,
1783
+ .fb-workspace .fb-text-editor:focus,
1784
+ .fb-workspace .fb-text-editor:focus-visible {
1785
+ outline: none;
1786
+ }
1787
+
1788
+ /* Nothing typographic here any more, and that is the point. Family, size,
1789
+ colour and leading are set on .fb-text-editor itself by
1790
+ FigureTextEditor.applyBoxType, from the annotation's own style -- so a
1791
+ character typed where no run yet exists inherits the BOX's type instead of
1792
+ the app's UI type, and a caption set to 1.5 leading is typed at 1.5.
1793
+
1794
+ The leading was written here as `line-height: 1.2` to match
1795
+ FigureRichText.LINE_HEIGHT, which is the DEFAULT and not this caption's. */
1796
+ .fb-text-editor-line {
1797
+ min-height: 1em;
1798
+ }
1799
+
1800
+ .fb-text-editor-line:empty::after {
1801
+ content: "";
1802
+ display: inline-block;
1803
+ }
1804
+
1805
+ .fb-panel-image {
1806
+ display: block;
1807
+ height: 100%;
1808
+ object-fit: cover;
1809
+ width: 100%;
1810
+ }
1811
+
1812
+ /* A preview that has not been rendered yet, or whose source is gone. Shown as
1813
+ an empty frame rather than as a broken-image glyph: the panel still exists,
1814
+ still lays out and still exports its vector furniture. */
1815
+ .fb-panel-image-missing {
1816
+ background: repeating-linear-gradient(45deg,
1817
+ var(--surface-2) 0 6px, var(--surface-1) 6px 12px);
1818
+ }
1819
+
1820
+ .fb-panel-label {
1821
+ color: #ffffff;
1822
+ font-size: 13px;
1823
+ font-weight: 700;
1824
+ left: 4px;
1825
+ position: absolute;
1826
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
1827
+ top: 2px;
1828
+ }
1829
+
1830
+ .fb-panel-title {
1831
+ bottom: 2px;
1832
+ color: #ffffff;
1833
+ font-size: 11px;
1834
+ left: 4px;
1835
+ position: absolute;
1836
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
1837
+ }
1838
+
1839
+ /* -- Quick Edit ---------------------------------------------------------- */
1840
+ /*
1841
+ A slide-over rather than a modal: the point of Quick Edit is that the figure
1842
+ stays visible while the panel is adjusted, so the canvas narrows and nothing
1843
+ is covered up.
1844
+
1845
+ The channel controls inside it are CORE's markup, styled by viewer.css (which
1846
+ workspace.html links for exactly this). Nothing here restyles them -- a
1847
+ plugin that repainted core's channel widget would make the two copies of it
1848
+ look different, which is the one thing reusing it was meant to avoid. */
1849
+
1850
+ /*
1851
+ The one dark thing left on a light page, and deliberately so: the subject of
1852
+ Quick Edit is a backlit fluorescence image, and the surround a colour balance
1853
+ is judged against has to be neutral-dark or every channel looks dimmer than
1854
+ it is. Core's own values, put back for this subtree -- which is also what the
1855
+ channel widget inside it was written for, since it is core's markup styled by
1856
+ viewer.css. */
1857
+ .fb-quickedit {
1858
+ --surface-0: #05070a;
1859
+ --surface-1: #0b0f15;
1860
+ --surface-2: #121820;
1861
+ --surface-3: rgba(255, 255, 255, 0.08);
1862
+ --border-subtle: rgba(255, 255, 255, 0.12);
1863
+ --border-strong: rgba(255, 255, 255, 0.22);
1864
+ --text-primary: #f8fafc;
1865
+ --text-secondary: #d8e1ed;
1866
+ --text-muted: #8ea0b8;
1867
+ --accent-channel: #38bdf8;
1868
+ --fb-on-accent: #06121f;
1869
+
1870
+ background: var(--surface-1);
1871
+ border-left: 1px solid var(--border-subtle);
1872
+ color: var(--text-primary);
1873
+ display: flex;
1874
+ flex-direction: column;
1875
+ min-height: 0;
1876
+ width: 420px;
1877
+ }
1878
+
1879
+ .fb-quickedit[hidden] {
1880
+ display: none;
1881
+ }
1882
+
1883
+ .fb-quickedit-head {
1884
+ align-items: center;
1885
+ border-bottom: 1px solid var(--border-subtle);
1886
+ display: flex;
1887
+ gap: var(--space-2);
1888
+ padding: var(--space-2) var(--space-3);
1889
+ }
1890
+
1891
+ .fb-quickedit-title {
1892
+ color: var(--text-primary);
1893
+ flex: 1;
1894
+ font-size: 13px;
1895
+ font-weight: 600;
1896
+ overflow: hidden;
1897
+ text-overflow: ellipsis;
1898
+ white-space: nowrap;
1899
+ }
1900
+
1901
+ /* Up to about half the height, so the figure keeps the rest. A mini viewer
1902
+ that filled the panel would be a second viewer, which is the thing this is
1903
+ deliberately not. */
1904
+ .fb-quickedit-view {
1905
+ background: #000000;
1906
+ flex: 0 0 auto;
1907
+ height: 46vh;
1908
+ max-height: 460px;
1909
+ min-height: 220px;
1910
+ position: relative;
1911
+ }
1912
+
1913
+ .fb-quickedit-view canvas {
1914
+ cursor: grab;
1915
+ display: block;
1916
+ touch-action: none;
1917
+ }
1918
+
1919
+ .fb-quickedit-view canvas:active {
1920
+ cursor: grabbing;
1921
+ }
1922
+
1923
+ .fb-quickedit-note {
1924
+ font-size: 11px;
1925
+ margin: 0;
1926
+ padding: var(--space-2) var(--space-3);
1927
+ }
1928
+
1929
+ .fb-quickedit-channels {
1930
+ flex: 1 1 auto;
1931
+ min-height: 0;
1932
+ overflow-y: auto;
1933
+ padding: 0 var(--space-3) var(--space-3);
1934
+ }
1935
+
1936
+ .fb-quickedit-actions {
1937
+ align-items: center;
1938
+ border-top: 1px solid var(--border-subtle);
1939
+ display: flex;
1940
+ gap: var(--space-2);
1941
+ padding: var(--space-2) var(--space-3);
1942
+ }
1943
+
1944
+ /* -- the contextual bar -------------------------------------------------- */
1945
+ /*
1946
+ What replaced the properties column. It exists only while something is
1947
+ selected, sits above it, and holds icons rather than labelled rows -- a bar
1948
+ floating over the page has to be small enough not to be in the way of the
1949
+ thing it is about. Every label is in the tooltip and in aria-label. */
1950
+
1951
+ /* `flex-wrap` because every button now carries a word as well as an icon, and
1952
+ a panel selection puts eleven of them in a row. Wrapping to a second line is
1953
+ the only one of the three answers that keeps them all readable -- truncating
1954
+ loses the labels this exists to add, and scrolling hides controls behind a
1955
+ gesture nobody would think to make on a toolbar.
1956
+
1957
+ With the labels UNDER the icons it very rarely has to: eleven stacked buttons
1958
+ are about 560px, where eleven side-by-side ones were past 900 and wrapped on
1959
+ any window narrower than a desktop. The rule stays because the widest case is
1960
+ still the window's to decide, not this file's.
1961
+
1962
+ Rounder and softer than the popovers it opens. This is the one piece of
1963
+ chrome that sits ON the artwork, so it has to read as a separate object
1964
+ floating above the sheet rather than as a region of it -- which is what the
1965
+ larger radius and the wider, softer shadow are doing. */
1966
+ .fb-context-bar {
1967
+ align-items: stretch;
1968
+ background: var(--surface-1);
1969
+ border: 1px solid var(--border-subtle);
1970
+ border-radius: var(--fb-card-radius);
1971
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.07),
1972
+ 0 10px 30px rgba(15, 23, 42, 0.14);
1973
+ display: flex;
1974
+ flex-wrap: wrap;
1975
+ gap: 2px;
1976
+ max-width: calc(100% - 16px);
1977
+ padding: 5px 6px;
1978
+ white-space: nowrap;
1979
+ z-index: 10;
1980
+ }
1981
+
1982
+ .fb-context-bar[hidden] {
1983
+ display: none;
1984
+ }
1985
+
1986
+ /* Taller than it is wide, and sized from the WORD: `min-width` is what stops a
1987
+ two-letter label leaving a button the size of its icon, and the padding is
1988
+ what lets a long one grow past it. The proportion is the point -- a row of
1989
+ near-square tiles reads as a set of equals, where a row of wide flat ones
1990
+ reads as a sentence of unrelated words. */
1991
+ .fb-context-button {
1992
+ align-items: center;
1993
+ background: transparent;
1994
+ border: none;
1995
+ border-radius: 8px;
1996
+ color: var(--text-secondary);
1997
+ cursor: pointer;
1998
+ display: inline-flex;
1999
+ flex-direction: column;
2000
+ gap: 4px;
2001
+ justify-content: center;
2002
+ min-width: 48px;
2003
+ padding: 7px 8px 6px;
2004
+ transition: background-color 120ms ease, color 120ms ease,
2005
+ box-shadow 120ms ease, transform 120ms ease;
2006
+ }
2007
+
2008
+ /* The overflow, which has no word under it. Narrower, and its icon centred on
2009
+ the FULL height of the bar rather than pushed up to where the captioned icons
2010
+ sit -- which is what `align-items: stretch` on the bar is for: the button is
2011
+ as tall as its neighbours, so centring inside it centres inside the row. */
2012
+ .fb-context-button.is-wordless {
2013
+ min-width: 30px;
2014
+ padding: 0 6px;
2015
+ }
2016
+
2017
+ /* Bigger than the word it captions, because it is what the button is
2018
+ recognised by once the row has been learnt; the word is what makes learning
2019
+ it possible in the first place. */
2020
+ .fb-context-icon {
2021
+ font-size: 14px;
2022
+ line-height: 1;
2023
+ }
2024
+
2025
+ /* The word under the icon. Every one of them sits on the same baseline, which
2026
+ is the whole reason for stacking: a row of icons with words BESIDE them has
2027
+ as many text baselines as it has button widths. */
2028
+ .fb-context-label {
2029
+ font-size: 10px;
2030
+ font-weight: 500;
2031
+ letter-spacing: 0.01em;
2032
+ line-height: 1;
2033
+ }
2034
+
2035
+ /* What the bar is picked up by: two columns of dots in a lane of its own, so
2036
+ there is somewhere to press that is not a command. Drawn with a gradient
2037
+ rather than as six elements, which keeps it out of the DOM the bar rebuilds
2038
+ on every selection change. */
2039
+ .fb-context-grip {
2040
+ align-self: center;
2041
+ background-image: radial-gradient(circle at 1px 1px,
2042
+ var(--text-muted) 1px, transparent 0);
2043
+ background-position: center;
2044
+ background-repeat: repeat;
2045
+ background-size: 4px 5px;
2046
+ cursor: grab;
2047
+ flex: 0 0 auto;
2048
+ height: 20px;
2049
+ margin: 0 3px 0 1px;
2050
+ opacity: 0.4;
2051
+ transition: opacity 120ms ease;
2052
+ width: 7px;
2053
+ }
2054
+
2055
+ .fb-context-grip:hover {
2056
+ opacity: 0.75;
2057
+ }
2058
+
2059
+ .fb-context-bar.is-dragging {
2060
+ cursor: grabbing;
2061
+ user-select: none;
2062
+ }
2063
+
2064
+ .fb-context-bar.is-dragging .fb-context-grip {
2065
+ cursor: grabbing;
2066
+ opacity: 1;
2067
+ }
2068
+
2069
+ .fb-context-button:hover:not(:disabled) {
2070
+ background: var(--fb-hover);
2071
+ color: var(--text-primary);
2072
+ }
2073
+
2074
+ /* Pressed, and it has to be felt rather than read -- the press is over before
2075
+ there is time to look at it. */
2076
+ .fb-context-button:active:not(:disabled) {
2077
+ transform: scale(0.94);
2078
+ }
2079
+
2080
+ /* The button whose popover is open. Tinted rather than merely hovered: the
2081
+ popover is somewhere else on screen, and this is the only thing tying it back
2082
+ to the control that opened it. */
2083
+ .fb-context-button.is-open,
2084
+ .fb-context-button.is-open:hover:not(:disabled) {
2085
+ background: var(--fb-tint);
2086
+ color: var(--accent-channel);
2087
+ }
2088
+
2089
+ /* On, not merely hovered: a scale bar that is showing has to look different
2090
+ from one the pointer happens to be over. The same tint-and-ring the segmented
2091
+ buttons in the text sidebar use, so "this is currently on" looks the same
2092
+ wherever it is said -- it was a solid accent block here and a tinted ring
2093
+ there, which made them read as two different kinds of state. */
2094
+ .fb-context-button.is-on {
2095
+ background: var(--fb-tint);
2096
+ box-shadow: inset 0 0 0 1px var(--accent-channel-soft);
2097
+ color: var(--accent-channel);
2098
+ }
2099
+
2100
+ .fb-context-button.is-on:hover:not(:disabled) {
2101
+ background: var(--accent-channel-soft);
2102
+ color: var(--accent-channel);
2103
+ }
2104
+
2105
+ .fb-context-button:disabled {
2106
+ cursor: default;
2107
+ opacity: 0.32;
2108
+ }
2109
+
2110
+ /* Delete is on the bar now rather than three levels down in "More", so it has
2111
+ to be the one button there that does not look like the others. Coloured on
2112
+ hover only: a permanently red icon in a row of grey ones reads as a warning
2113
+ about the selection rather than as an action available to it. */
2114
+ .fb-context-button.is-danger:hover:not(:disabled) {
2115
+ background: rgb(220 38 38 / 10%);
2116
+ color: #dc2626;
2117
+ }
2118
+
2119
+ /* Full height, hairline, and inset from the buttons by more than the gap
2120
+ between them -- the divider has to be read as the boundary of a GROUP, and at
2121
+ the same 2px spacing as everything else it reads as one more control. */
2122
+ .fb-context-divider {
2123
+ align-self: stretch;
2124
+ background: var(--border-subtle);
2125
+ flex: 0 0 auto;
2126
+ margin: 4px 5px;
2127
+ width: 1px;
2128
+ }
2129
+
2130
+ .fb-context-note {
2131
+ color: var(--text-muted);
2132
+ font-size: 11px;
2133
+ padding: 0 var(--space-2);
2134
+ }
2135
+
2136
+ /* Thirty characters at a size worth aiming at. Six columns because that is
2137
+ what fits the popover's own width without making it wider than the bar. */
2138
+ .fb-symbol-grid {
2139
+ display: grid;
2140
+ gap: 2px;
2141
+ grid-template-columns: repeat(6, 1fr);
2142
+ }
2143
+
2144
+ .fb-symbol {
2145
+ background: transparent;
2146
+ border: none;
2147
+ border-radius: 5px;
2148
+ color: var(--text-primary);
2149
+ cursor: pointer;
2150
+ font-size: 15px;
2151
+ height: 28px;
2152
+ line-height: 1;
2153
+ padding: 0;
2154
+ }
2155
+
2156
+ .fb-symbol:hover {
2157
+ background: var(--fb-tint);
2158
+ color: var(--accent-channel);
2159
+ }
2160
+
2161
+ /* Label left, field right, one line each -- the shape of a properties list, so
2162
+ Rotation, Width, Height and Units read down the same two columns. */
2163
+ .fb-tf-row {
2164
+ align-items: center;
2165
+ display: flex;
2166
+ gap: var(--space-2);
2167
+ justify-content: space-between;
2168
+ margin-bottom: 4px;
2169
+ }
2170
+
2171
+ .fb-tf-row:last-child { margin-bottom: 0; }
2172
+
2173
+ /*
2174
+ A popover arrives FROM its button, not out of nothing.
2175
+
2176
+ Eight frames of fade and a four-pixel rise: long enough for the eye to catch
2177
+ where it came from, short enough that nobody waiting on it notices waiting.
2178
+ The transform origin is the top edge because every one of these opens
2179
+ downwards from the bar -- `positionPopover` flips it above the bar only when
2180
+ the page has run out of room below, and being slightly wrong in that case
2181
+ costs less than the extra state it would take to be right. */
2182
+ .fb-context-popover {
2183
+ animation: fb-pop-in 120ms ease-out;
2184
+ background: var(--surface-1);
2185
+ border: 1px solid var(--border-subtle);
2186
+ border-radius: 10px;
2187
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08),
2188
+ 0 10px 30px rgba(15, 23, 42, 0.16);
2189
+ min-width: 190px;
2190
+ padding: var(--space-2);
2191
+ transform-origin: top center;
2192
+ z-index: 20;
2193
+ }
2194
+
2195
+ @keyframes fb-pop-in {
2196
+ from { opacity: 0; transform: translateY(-4px) scale(0.97); }
2197
+ to { opacity: 1; transform: none; }
2198
+ }
2199
+
2200
+ /* Asked for by the platform, and honoured: everything above is decoration on a
2201
+ state change that has already happened, so there is nothing to lose by
2202
+ cutting it. */
2203
+ @media (prefers-reduced-motion: reduce) {
2204
+ .fb-context-popover,
2205
+ .fb-menu {
2206
+ animation: none;
2207
+ }
2208
+
2209
+ .fb-context-button,
2210
+ .fb-menu-item {
2211
+ transition: none;
2212
+ }
2213
+
2214
+ .fb-context-button:active:not(:disabled) {
2215
+ transform: none;
2216
+ }
2217
+ }
2218
+ /* Two panels drawing the same marker in different colours. Set apart rather
2219
+ than phrased as a warning: it may be deliberate, and the safe answer -- keep
2220
+ them separate -- is the first button. */
2221
+ .fb-conflict {
2222
+ border-top: 1px solid var(--border-subtle);
2223
+ margin-top: var(--space-2);
2224
+ padding-top: var(--space-2);
2225
+ }
2226
+
2227
+ .fb-conflict strong {
2228
+ color: #96650a;
2229
+ display: block;
2230
+ font-size: 12px;
2231
+ }
2232
+
2233
+ /* Something worth saying that is not worth stopping for. Bottom centre of the
2234
+ canvas, over the page rather than over the chrome, gone by itself. */
2235
+ .fb-toast {
2236
+ align-items: center;
2237
+ background: var(--surface-1);
2238
+ border: 1px solid var(--border-subtle);
2239
+ border-radius: 8px;
2240
+ bottom: 20px;
2241
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08),
2242
+ 0 10px 30px rgba(15, 23, 42, 0.16);
2243
+ color: var(--text-primary);
2244
+ display: flex;
2245
+ font-size: 12px;
2246
+ gap: var(--space-2);
2247
+ left: 50%;
2248
+ max-width: 520px;
2249
+ padding: var(--space-2) var(--space-3);
2250
+ transform: translateX(-50%);
2251
+ z-index: 80;
2252
+ }
2253
+
2254
+ .fb-toast-action {
2255
+ background: transparent;
2256
+ border: none;
2257
+ color: var(--accent-channel);
2258
+ cursor: pointer;
2259
+ font-size: 12px;
2260
+ font-weight: 600;
2261
+ padding: 0;
2262
+ white-space: nowrap;
2263
+ }
2264
+
2265
+ .fb-toast-close {
2266
+ background: transparent;
2267
+ border: none;
2268
+ color: var(--text-muted);
2269
+ cursor: pointer;
2270
+ font-size: 11px;
2271
+ padding: 0 0 0 var(--space-2);
2272
+ }
2273
+
2274
+
2275
+ .fb-popover-note {
2276
+ font-size: 11px;
2277
+ line-height: 1.35;
2278
+ margin: var(--space-2) 0 0;
2279
+ }
2280
+
2281
+ /* -- menus --------------------------------------------------------------- */
2282
+ /*
2283
+ Appended to <body> and positioned fixed, because a menu inside the topbar is
2284
+ clipped by it. */
2285
+
2286
+ /*
2287
+ Mounted on <body>, so it is the ONE part of this plugin's chrome that is
2288
+ outside .fb-workspace and cannot inherit the light values set there. It
2289
+ carries its own copy: without them a menu on the figure page would be core's
2290
+ near-white text on this card's white background, which is a menu you cannot
2291
+ read. Only figure pages build one, so there is nothing else to disturb. */
2292
+ .fb-menu {
2293
+ animation: fb-pop-in 110ms ease-out;
2294
+ transform-origin: top left;
2295
+
2296
+ --surface-1: #ffffff;
2297
+ --surface-3: rgba(15, 23, 42, 0.06);
2298
+ --border-subtle: #e4e7ee;
2299
+ --text-primary: #16202e;
2300
+ --text-muted: #78839a;
2301
+ --accent-danger: #d64545;
2302
+
2303
+ background: var(--surface-1);
2304
+ border: 1px solid var(--border-subtle);
2305
+ border-radius: 10px;
2306
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08),
2307
+ 0 10px 30px rgba(15, 23, 42, 0.16);
2308
+ display: flex;
2309
+ flex-direction: column;
2310
+ /* The project list under "Add panels" is as long as the server's project
2311
+ list, which is not a number this page gets to choose. Bounded and
2312
+ scrolled, rather than left to run off the bottom of the window. */
2313
+ max-height: min(70vh, 420px);
2314
+ min-width: 170px;
2315
+ overflow-y: auto;
2316
+ padding: 5px;
2317
+ position: fixed;
2318
+ z-index: 9000;
2319
+ }
2320
+
2321
+ /* Three columns: the icon in a fixed gutter on the left, the label taking
2322
+ whatever is left, the shortcut ranged right against the far edge. */
2323
+ .fb-menu-item {
2324
+ align-items: center;
2325
+ background: transparent;
2326
+ border: none;
2327
+ border-radius: 6px;
2328
+ color: var(--text-primary);
2329
+ cursor: pointer;
2330
+ display: flex;
2331
+ font-size: 13px;
2332
+ gap: var(--space-2);
2333
+ padding: 6px var(--space-2);
2334
+ text-align: left;
2335
+ transition: background-color 100ms ease;
2336
+ white-space: nowrap;
2337
+ width: 100%;
2338
+ }
2339
+
2340
+ /* A fixed gutter, present whether or not the row has an icon: a menu where only
2341
+ some rows are indented has a ragged left edge, and the eye reads the ragged
2342
+ edge before it reads any of the words. Menus where NO row has an icon do not
2343
+ draw it at all -- see FigureContextMenu.open. */
2344
+ .fb-menu-icon {
2345
+ display: inline-block;
2346
+ flex: 0 0 auto;
2347
+ font-size: 11px;
2348
+ opacity: 0.7;
2349
+ text-align: center;
2350
+ width: 14px;
2351
+ }
2352
+
2353
+ /* Takes the slack, which is what pushes the shortcut to the right edge --
2354
+ `margin-left: auto` on the key alone would work too, but this way the label
2355
+ also gets somewhere to be truncated if a menu is ever constrained. */
2356
+ .fb-menu-text {
2357
+ flex: 1 1 auto;
2358
+ overflow: hidden;
2359
+ text-overflow: ellipsis;
2360
+ }
2361
+
2362
+ /* The key. Right-aligned and set back a step in the hierarchy: it is a reminder
2363
+ for the next time rather than part of the row's meaning, and at the label's
2364
+ own weight it competed with the thing it annotates. The left padding is a
2365
+ minimum gap, so a long label and a long key never touch. */
2366
+ .fb-menu-key {
2367
+ color: var(--text-muted);
2368
+ flex: 0 0 auto;
2369
+ font-size: 11px;
2370
+ font-variant-numeric: tabular-nums;
2371
+ letter-spacing: 0.02em;
2372
+ padding-left: var(--space-4, 16px);
2373
+ }
2374
+
2375
+ .fb-menu-item:hover:not(:disabled) {
2376
+ background: var(--surface-3, rgba(255, 255, 255, 0.08));
2377
+ }
2378
+
2379
+ /* Dimmed WITH the row. A key at full muted-grey next to a greyed-out label
2380
+ reads as the brighter of the two, which points the eye at the one row in the
2381
+ menu that cannot be used. */
2382
+ .fb-menu-item:disabled .fb-menu-key,
2383
+ .fb-menu-item:disabled .fb-menu-icon {
2384
+ opacity: 0.45;
2385
+ }
2386
+ /* The destructive one, coloured rather than merely last: "Delete from figure"
2387
+ destroys a captured scene, and it sits two rows below "Remove from page",
2388
+ which does not. */
2389
+ .fb-menu-item.is-danger {
2390
+ color: var(--accent-danger);
2391
+ }
2392
+
2393
+
2394
+ .fb-menu-item:disabled {
2395
+ color: var(--text-muted);
2396
+ cursor: default;
2397
+ }
2398
+
2399
+ .fb-menu-note {
2400
+ color: var(--text-muted);
2401
+ font-size: 11px;
2402
+ overflow: hidden;
2403
+ padding: var(--space-2);
2404
+ text-overflow: ellipsis;
2405
+ }
2406
+ /* Always the same width, ticked or not, so the labels line up and the menu
2407
+ does not shuffle sideways as things are turned on. */
2408
+ .fb-menu-tick {
2409
+ display: inline-block;
2410
+ text-align: center;
2411
+ width: 14px;
2412
+ }
2413
+
2414
+
2415
+ .fb-menu-separator {
2416
+ background: var(--border-subtle);
2417
+ height: 1px;
2418
+ margin: 4px 0;
2419
+ }
2420
+
2421
+ /* -- small form parts ---------------------------------------------------- */
2422
+
2423
+ .fb-input {
2424
+ background: var(--surface-2);
2425
+ border: 1px solid var(--border-subtle);
2426
+ border-radius: var(--radius-sm);
2427
+ color: var(--text-primary);
2428
+ font-size: 13px;
2429
+ padding: 5px var(--space-2);
2430
+ width: 100%;
2431
+ }
2432
+
2433
+ .fb-input-quiet {
2434
+ background: transparent;
2435
+ border-color: transparent;
2436
+ font-size: 12px;
2437
+ padding: 3px 0;
2438
+ }
2439
+
2440
+ .fb-input-tiny {
2441
+ width: 58px;
2442
+ }
2443
+
2444
+ .fb-figure-row {
2445
+ align-items: center;
2446
+ display: flex;
2447
+ gap: var(--space-2);
2448
+ }
2449
+
2450
+ .fb-check {
2451
+ align-items: center;
2452
+ color: var(--text-secondary);
2453
+ display: flex;
2454
+ font-size: 12px;
2455
+ gap: var(--space-2);
2456
+ margin: var(--space-2) 0;
2457
+ }
2458
+
2459
+ /* -- dialogs ----------------------------------------------------------- */
2460
+ /*
2461
+ There is no sidebar panel to style any more: Figure Builder declares no
2462
+ tool_panel_slot, and everything it shows is either on the image (the dock,
2463
+ the viewfinder and the capture boxes, further down) or on the figure canvas.
2464
+ The rules that used to be here -- .fb-where, .fb-summary, .fb-edit-session
2465
+ and the rest -- went with the panel they described. */
2466
+
2467
+ /* A native <dialog>: modal, focus-trapped and Esc-dismissible without a line of
2468
+ script, and it cannot end up behind the canvas the way a positioned div can.
2469
+ The backdrop is the browser's; only the box itself is styled here. */
2470
+ .fb-dialog {
2471
+ background: var(--surface-1);
2472
+ border: 1px solid var(--border-subtle);
2473
+ border-radius: var(--radius-lg);
2474
+ box-shadow: var(--shadow-lg, 0 8px 32px rgba(0, 0, 0, 0.45));
2475
+ color: var(--text-primary);
2476
+ max-width: 460px;
2477
+ padding: var(--space-4);
2478
+ width: 90vw;
2479
+ }
2480
+
2481
+ .fb-dialog h2 {
2482
+ font-size: 17px;
2483
+ font-weight: 700;
2484
+ margin: 0 0 var(--space-2);
2485
+ }
2486
+
2487
+ .fb-dialog .fb-select {
2488
+ max-width: none;
2489
+ width: 100%;
2490
+ }
2491
+
2492
+ .fb-dialog::backdrop {
2493
+ background: rgba(5, 7, 10, 0.6);
2494
+ }
2495
+
2496
+ /* The same shape as core's .control-label, written out here rather than
2497
+ borrowed. The export dialog is Figure Builder's, and viewer.css is linked on
2498
+ this page for the channel widget alone -- two labels reaching into it for
2499
+ their type size was the kind of dependency that gets noticed the day
2500
+ somebody changes it for the viewer. */
2501
+ .fb-field-label {
2502
+ color: var(--text-muted);
2503
+ display: block;
2504
+ font-size: 11px;
2505
+ font-weight: 700;
2506
+ margin-bottom: 6px;
2507
+ text-transform: uppercase;
2508
+ }
2509
+
2510
+ .fb-dialog .fb-field-label:not(:first-of-type) {
2511
+ margin-top: var(--space-3);
2512
+ }
2513
+
2514
+ .fb-export-warnings {
2515
+ margin: var(--space-3) 0;
2516
+ }
2517
+
2518
+ .fb-export-warnings .fb-banner {
2519
+ margin-bottom: var(--space-2);
2520
+ }
2521
+
2522
+ .fb-dialog-actions {
2523
+ display: flex;
2524
+ flex-wrap: wrap;
2525
+ gap: var(--space-2);
2526
+ margin-top: var(--space-3);
2527
+ }
2528
+
2529
+ .fb-dialog-footer {
2530
+ border-top: 1px solid var(--border-subtle);
2531
+ padding-top: var(--space-3);
2532
+ }
2533
+
2534
+ /* -- asking a question --------------------------------------------------- */
2535
+ /*
2536
+ Narrower than the export sheet, because a question is read in one glance and
2537
+ a line of text 460px wide is not. FigureConfirm builds these; the reasoning
2538
+ for replacing `window.confirm` is in its docstring.
2539
+
2540
+ Ranged right, and the answers in order of consequence from left to right --
2541
+ Cancel first, the destructive one last and furthest from where the pointer
2542
+ already is. */
2543
+ .fb-confirm {
2544
+ max-width: 400px;
2545
+ }
2546
+
2547
+ .fb-confirm-body {
2548
+ color: var(--text-secondary);
2549
+ font-size: 13px;
2550
+ line-height: 1.5;
2551
+ margin: 0 0 var(--space-2);
2552
+ }
2553
+
2554
+ .fb-confirm-actions {
2555
+ justify-content: flex-end;
2556
+ margin-top: var(--space-4);
2557
+ }
2558
+
2559
+ /* Red, and filled like the primary button rather than merely tinted: this is
2560
+ the button that ends something, and it has to be told apart from the one
2561
+ beside it before it is pressed, not after. */
2562
+ .fb-button-danger {
2563
+ background: var(--accent-danger);
2564
+ border-color: transparent;
2565
+ color: #ffffff;
2566
+ font-weight: 600;
2567
+ padding: 0 var(--space-4);
2568
+ }
2569
+
2570
+ .fb-button-danger:hover {
2571
+ filter: brightness(1.08);
2572
+ }
2573
+
2574
+ /* -- the capture dock, on the image ------------------------------------ */
2575
+ /*
2576
+ Everything from here down is drawn OVER the viewer, so it takes its colours
2577
+ from literal translucent darks rather than from the surface tokens -- the
2578
+ same choice core makes for .viewer-channel-legend and the overview lens. A
2579
+ surface token is a colour for a panel with a known background; over tissue
2580
+ that happens to be bright, the light-theme value of one is unreadable.
2581
+
2582
+ Top-right because core's own furniture is elsewhere: project label top-left,
2583
+ channel legend bottom-right, overview lens bottom-left. */
2584
+
2585
+ .fb-dock {
2586
+ align-items: flex-end;
2587
+ display: flex;
2588
+ flex-direction: column;
2589
+ gap: 8px;
2590
+ /* The floor, for the moment before the first measurement. The real cap is
2591
+ an inline max-height from FigureCaptureDock.layout(), which stops the
2592
+ dock above core's channel legend -- a distance no stylesheet can know,
2593
+ since the legend's height is however many channels are on. */
2594
+ max-height: calc(100% - 24px);
2595
+ /* The dock's own box is only as wide as its widest child, but its padding
2596
+ and gaps are still surface: passing pointer events through them keeps a
2597
+ drag that starts in the gap on the image, where it was aimed. */
2598
+ pointer-events: none;
2599
+ position: absolute;
2600
+ right: 12px;
2601
+ top: 12px;
2602
+ z-index: 240;
2603
+ }
2604
+
2605
+ /* Capturing is off while a panel's view is on loan to the live viewer, so the
2606
+ control that arms it says so rather than silently refusing. The dock itself
2607
+ stays: it is carrying the session, and the way out of the edit is in it. */
2608
+ .fb-dock.is-editing .fb-orb {
2609
+ opacity: 0.35;
2610
+ pointer-events: none;
2611
+ }
2612
+
2613
+ .fb-dock-head {
2614
+ align-items: center;
2615
+ display: flex;
2616
+ /* The head and the error never give up height to the strip below them: one
2617
+ is what arms the mode and the other is what says why it failed. */
2618
+ flex: 0 0 auto;
2619
+ gap: 8px;
2620
+ pointer-events: auto;
2621
+ }
2622
+
2623
+ /* The instructions, and the shortcut when there are none to give. Beside the
2624
+ orb rather than under it, so it never covers the strip and never grows the
2625
+ column the user is trying to see past. */
2626
+ .fb-dock-caption {
2627
+ backdrop-filter: blur(6px);
2628
+ /* Text, not a control. At its full width it covers 420px of image, and a
2629
+ drag that started on it would hit a tooltip instead of the picture. */
2630
+ pointer-events: none;
2631
+ background: rgba(8, 12, 18, 0.72);
2632
+ border: 1px solid rgba(148, 163, 184, 0.28);
2633
+ border-radius: 999px;
2634
+ color: rgba(226, 232, 240, 0.92);
2635
+ font-size: 11px;
2636
+ line-height: 1.6;
2637
+ max-width: min(46vw, 420px);
2638
+ padding: 5px 12px;
2639
+ }
2640
+
2641
+ .fb-dock-caption kbd {
2642
+ background: rgba(148, 163, 184, 0.18);
2643
+ border: 1px solid rgba(148, 163, 184, 0.35);
2644
+ border-radius: 3px;
2645
+ font-size: 10px;
2646
+ padding: 0 4px;
2647
+ }
2648
+
2649
+ /* The one control that matters, and the only one that is a circle: it is the
2650
+ mode toggle, and a mode toggle has to be findable without reading. The gloss
2651
+ is two layers -- a highlight where the light would be and a vertical body --
2652
+ because a flat disc over a photograph reads as part of the photograph. */
2653
+ .fb-orb {
2654
+ align-items: center;
2655
+ backdrop-filter: blur(6px);
2656
+ background:
2657
+ radial-gradient(120% 120% at 30% 22%,
2658
+ rgba(255, 255, 255, 0.30) 0%,
2659
+ rgba(255, 255, 255, 0.07) 42%,
2660
+ rgba(255, 255, 255, 0) 62%),
2661
+ linear-gradient(180deg, rgba(30, 41, 59, 0.94), rgba(11, 17, 28, 0.96));
2662
+ border: 1px solid rgba(148, 163, 184, 0.38);
2663
+ border-radius: 50%;
2664
+ box-shadow: 0 8px 22px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.22);
2665
+ color: rgba(226, 232, 240, 0.95);
2666
+ cursor: pointer;
2667
+ display: inline-flex;
2668
+ font-size: 18px;
2669
+ height: 54px;
2670
+ justify-content: center;
2671
+ transition: transform var(--duration-fast, 120ms) ease,
2672
+ box-shadow var(--duration-fast, 120ms) ease;
2673
+ width: 54px;
2674
+ }
2675
+
2676
+ .fb-orb:hover {
2677
+ transform: translateY(-1px);
2678
+ }
2679
+
2680
+ .fb-orb:active {
2681
+ transform: translateY(1px);
2682
+ }
2683
+
2684
+ /* Armed is a state the user has to be able to read from the corner of the eye:
2685
+ while it is on, a drag on the image does something different from what it
2686
+ does the rest of the time. */
2687
+ .fb-dock.is-armed .fb-orb {
2688
+ background:
2689
+ radial-gradient(120% 120% at 30% 22%,
2690
+ rgba(255, 255, 255, 0.42) 0%,
2691
+ rgba(255, 255, 255, 0.12) 42%,
2692
+ rgba(255, 255, 255, 0) 62%),
2693
+ linear-gradient(180deg, rgba(56, 189, 248, 0.95), rgba(14, 116, 178, 0.96));
2694
+ border-color: rgba(186, 230, 253, 0.9);
2695
+ box-shadow: 0 0 0 5px rgba(56, 189, 248, 0.18),
2696
+ 0 8px 22px rgba(0, 0, 0, 0.5),
2697
+ inset 0 1px 0 rgba(255, 255, 255, 0.35);
2698
+ color: #04121f;
2699
+ }
2700
+
2701
+ /* The way out. Figure Builder has no card in the sidebar, so it has no X
2702
+ there either -- this one and the canvas's own remove the tool outright
2703
+ rather than folding it away. Small and beside the orb, never near it in
2704
+ weight: leaving the mode and leaving the tool are one keystroke apart and
2705
+ must not look alike. */
2706
+ .fb-dock-close {
2707
+ align-items: center;
2708
+ background: rgba(8, 12, 18, 0.7);
2709
+ border: 1px solid rgba(148, 163, 184, 0.3);
2710
+ border-radius: 50%;
2711
+ color: rgba(226, 232, 240, 0.85);
2712
+ cursor: pointer;
2713
+ display: inline-flex;
2714
+ font-size: 11px;
2715
+ height: 24px;
2716
+ justify-content: center;
2717
+ pointer-events: auto;
2718
+ width: 24px;
2719
+ }
2720
+
2721
+ .fb-dock-close:hover {
2722
+ background: rgba(220, 80, 80, 0.85);
2723
+ border-color: rgba(255, 190, 190, 0.7);
2724
+ color: #fff;
2725
+ }
2726
+
2727
+ /* A failure the user has to be able to see without a sidebar to put it in. */
2728
+ .fb-dock-error {
2729
+ background: rgba(120, 30, 30, 0.85);
2730
+ border: 1px solid rgba(255, 150, 150, 0.5);
2731
+ border-radius: 6px;
2732
+ color: #ffe8e8;
2733
+ flex: 0 0 auto;
2734
+ font-size: 11px;
2735
+ margin: 0;
2736
+ max-width: 260px;
2737
+ padding: 6px 9px;
2738
+ pointer-events: auto;
2739
+ }
2740
+
2741
+
2742
+ /* -- the session strip -------------------------------------------------- */
2743
+
2744
+ .fb-strip {
2745
+ backdrop-filter: blur(9px);
2746
+ background: rgba(8, 12, 18, 0.58);
2747
+ border: 1px solid rgba(148, 163, 184, 0.24);
2748
+ border-radius: 10px;
2749
+ color: rgba(226, 232, 240, 0.92);
2750
+ display: flex;
2751
+ flex-direction: column;
2752
+ min-height: 0;
2753
+ overflow: hidden;
2754
+ pointer-events: auto;
2755
+ width: 158px;
2756
+ }
2757
+
2758
+ .fb-strip-toggle {
2759
+ align-items: center;
2760
+ background: none;
2761
+ border: none;
2762
+ color: inherit;
2763
+ cursor: pointer;
2764
+ display: flex;
2765
+ font-size: 11px;
2766
+ font-weight: 600;
2767
+ gap: 6px;
2768
+ letter-spacing: 0.03em;
2769
+ padding: 8px 10px;
2770
+ text-transform: uppercase;
2771
+ width: 100%;
2772
+ }
2773
+
2774
+ .fb-strip-title {
2775
+ flex: 1;
2776
+ text-align: left;
2777
+ }
2778
+
2779
+ .fb-strip-count {
2780
+ background: rgba(148, 163, 184, 0.22);
2781
+ border-radius: 999px;
2782
+ font-size: 10px;
2783
+ min-width: 18px;
2784
+ padding: 1px 6px;
2785
+ text-align: center;
2786
+ }
2787
+
2788
+ .fb-strip-chevron {
2789
+ transition: transform var(--duration-fast, 120ms) ease;
2790
+ }
2791
+
2792
+ .fb-dock.is-collapsed .fb-strip-chevron {
2793
+ transform: rotate(-90deg);
2794
+ }
2795
+
2796
+ /* Collapsed leaves the header, not nothing: a strip that vanished would take
2797
+ the count with it, and the count is the reason to open it again. */
2798
+ .fb-dock.is-collapsed .fb-strip-body {
2799
+ display: none;
2800
+ }
2801
+
2802
+ .fb-strip-body {
2803
+ display: flex;
2804
+ flex-direction: column;
2805
+ /* Both of these are load-bearing for the scroll below: without min-height a
2806
+ flex child refuses to shrink below its content, and without overflow the
2807
+ shrunk box paints outside itself anyway. */
2808
+ min-height: 0;
2809
+ overflow: hidden;
2810
+ }
2811
+
2812
+ /* The list scrolls; the thumbnails keep their size.
2813
+
2814
+ `flex: 0 0 auto` on the items is the whole of it. A flex column shrinks its
2815
+ children before it lets itself overflow, so without it a twentieth capture
2816
+ squeezed the other nineteen into slivers -- and a sliver of one crop of
2817
+ tissue looks exactly like a sliver of another.
2818
+
2819
+ The height is whatever the dock has left rather than a fixed slice of the
2820
+ window: `.fb-dock` is already capped at the viewer's height, and a second cap
2821
+ here would scroll a list that still had room in a short window. */
2822
+ .fb-strip-items {
2823
+ display: flex;
2824
+ flex: 1 1 auto;
2825
+ flex-direction: column;
2826
+ gap: 6px;
2827
+ min-height: 0;
2828
+ overflow-y: auto;
2829
+ padding: 0 8px 8px;
2830
+ scrollbar-width: thin;
2831
+ }
2832
+
2833
+ .fb-strip-item {
2834
+ border: 1px solid rgba(148, 163, 184, 0.26);
2835
+ border-radius: 4px;
2836
+ flex: 0 0 auto;
2837
+ margin: 0;
2838
+ overflow: hidden;
2839
+ position: relative;
2840
+ }
2841
+
2842
+ .fb-strip-item img,
2843
+ .fb-strip-pending {
2844
+ aspect-ratio: 4 / 3;
2845
+ display: block;
2846
+ object-fit: cover;
2847
+ width: 100%;
2848
+ }
2849
+
2850
+ .fb-strip-pending {
2851
+ background: repeating-linear-gradient(45deg,
2852
+ rgba(148, 163, 184, 0.18) 0 6px, rgba(148, 163, 184, 0.08) 6px 12px);
2853
+ }
2854
+
2855
+ /* A capture that is not in a figure yet is marked rather than hidden: it is
2856
+ memory, and memory does not survive the next navigation. */
2857
+ .fb-strip-item.is-pending {
2858
+ border-color: var(--accent-warning, #e0a44c);
2859
+ }
2860
+
2861
+ .fb-strip-remove {
2862
+ align-items: center;
2863
+ background: rgba(2, 6, 23, 0.72);
2864
+ border: none;
2865
+ border-radius: 50%;
2866
+ color: rgba(226, 232, 240, 0.95);
2867
+ cursor: pointer;
2868
+ display: inline-flex;
2869
+ font-size: 10px;
2870
+ height: 20px;
2871
+ justify-content: center;
2872
+ /* Dim rather than absent: a control that appears only on hover is one that
2873
+ does not exist on a touchscreen. */
2874
+ opacity: 0.45;
2875
+ position: absolute;
2876
+ right: 3px;
2877
+ top: 3px;
2878
+ transition: opacity var(--duration-fast, 120ms) ease;
2879
+ width: 20px;
2880
+ }
2881
+
2882
+ .fb-strip-item:hover .fb-strip-remove,
2883
+ .fb-strip-remove:focus-visible {
2884
+ opacity: 1;
2885
+ }
2886
+
2887
+ .fb-strip-empty {
2888
+ color: rgba(148, 163, 184, 0.9);
2889
+ font-size: 11px;
2890
+ margin: 0;
2891
+ padding: 0 10px 10px;
2892
+ }
2893
+
2894
+ .fb-strip-empty[hidden] {
2895
+ display: none;
2896
+ }
2897
+
2898
+ .fb-strip-foot {
2899
+ border-top: 1px solid rgba(148, 163, 184, 0.2);
2900
+ display: flex;
2901
+ flex: 0 0 auto;
2902
+ flex-direction: column;
2903
+ gap: 6px;
2904
+ padding: 8px;
2905
+ }
2906
+
2907
+ .fb-strip-label {
2908
+ color: rgba(148, 163, 184, 0.9);
2909
+ font-size: 10px;
2910
+ letter-spacing: 0.04em;
2911
+ text-transform: uppercase;
2912
+ }
2913
+
2914
+ .fb-strip-figure-row {
2915
+ align-items: center;
2916
+ display: flex;
2917
+ gap: 6px;
2918
+ }
2919
+
2920
+ .fb-strip-figure,
2921
+ .fb-strip-add,
2922
+ .fb-strip-go {
2923
+ background: rgba(148, 163, 184, 0.16);
2924
+ border: 1px solid rgba(148, 163, 184, 0.28);
2925
+ border-radius: var(--radius-sm, 4px);
2926
+ color: rgba(226, 232, 240, 0.95);
2927
+ cursor: pointer;
2928
+ font-size: 12px;
2929
+ padding: 5px 8px;
2930
+ }
2931
+
2932
+ .fb-strip-figure {
2933
+ flex: 1;
2934
+ overflow: hidden;
2935
+ text-align: left;
2936
+ text-overflow: ellipsis;
2937
+ white-space: nowrap;
2938
+ }
2939
+
2940
+ .fb-strip-figure.is-empty {
2941
+ color: rgba(148, 163, 184, 0.85);
2942
+ font-style: italic;
2943
+ }
2944
+
2945
+ .fb-strip-add {
2946
+ flex: 0 0 auto;
2947
+ padding: 5px 7px;
2948
+ }
2949
+
2950
+ .fb-strip-go {
2951
+ background: rgba(56, 189, 248, 0.22);
2952
+ border-color: rgba(56, 189, 248, 0.5);
2953
+ font-weight: 600;
2954
+ }
2955
+
2956
+ /* One hover for the three buttons in the foot. This rule lost its body once --
2957
+ an edit landed between the brace and the declaration, and every rule after it
2958
+ as far as the capture boxes became a nested one that could never match. The
2959
+ symptom was the status line rendering at body size, which reads as a layout
2960
+ choice rather than as eighty lines of dead stylesheet. */
2961
+ .fb-strip-figure:hover,
2962
+ .fb-strip-add:hover,
2963
+ .fb-strip-go:hover {
2964
+ background: rgba(56, 189, 248, 0.32);
2965
+ }
2966
+
2967
+ /* Which capture the strip and the image are both pointing at. */
2968
+ .fb-strip-item.is-selected {
2969
+ border-color: rgba(56, 189, 248, 0.95);
2970
+ box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.55);
2971
+ }
2972
+
2973
+ /* The same number as the tag on the box out on the image. Always shown here,
2974
+ unlike out there: in a 158px column it is the only way to tell two crops of
2975
+ similar tissue apart. */
2976
+ .fb-strip-index {
2977
+ background: rgba(2, 6, 23, 0.72);
2978
+ border-radius: 3px;
2979
+ bottom: 3px;
2980
+ color: rgba(226, 232, 240, 0.95);
2981
+ font-size: 10px;
2982
+ left: 3px;
2983
+ line-height: 1.6;
2984
+ padding: 0 5px;
2985
+ position: absolute;
2986
+ }
2987
+
2988
+ /* Compact status, not a headline: how many panels, how many pages, and whether
2989
+ the last write landed. It sits below a 158px button in a corner of the image,
2990
+ so it is the smallest legible thing in the dock and stays grey. It is there to
2991
+ be glanced at and confirmed, never to be read. */
2992
+ .fb-strip-meta {
2993
+ color: rgba(148, 163, 184, 0.78);
2994
+ font-size: 9px;
2995
+ letter-spacing: 0.01em;
2996
+ line-height: 1.45;
2997
+ }
2998
+
2999
+ /* -- the borrowed-viewer session ---------------------------------------- */
3000
+ /*
3001
+ Replaces the strip rather than sitting under it. While a panel's view is
3002
+ loaded into the live viewer, capturing is off: a shot taken then would be a
3003
+ panel of somebody else's scene, and a strip still offering to take one would
3004
+ be an offer the tool refuses. */
3005
+
3006
+ .fb-strip[hidden],
3007
+ .fb-dock-edit[hidden] {
3008
+ display: none;
3009
+ }
3010
+
3011
+ .fb-dock-edit {
3012
+ backdrop-filter: blur(9px);
3013
+ background: rgba(8, 12, 18, 0.68);
3014
+ border: 1px solid rgba(56, 189, 248, 0.45);
3015
+ border-radius: 10px;
3016
+ color: rgba(226, 232, 240, 0.92);
3017
+ display: flex;
3018
+ flex-direction: column;
3019
+ gap: 6px;
3020
+ padding: 10px;
3021
+ pointer-events: auto;
3022
+ width: 230px;
3023
+ }
3024
+
3025
+ .fb-dock-edit-head {
3026
+ align-items: center;
3027
+ display: flex;
3028
+ font-size: 12px;
3029
+ font-weight: 600;
3030
+ gap: 6px;
3031
+ }
3032
+
3033
+ .fb-dock-edit-body,
3034
+ .fb-dock-edit-note {
3035
+ color: rgba(148, 163, 184, 0.95);
3036
+ font-size: 11px;
3037
+ line-height: 1.5;
3038
+ margin: 0;
3039
+ }
3040
+
3041
+ .fb-dock-edit-note {
3042
+ color: var(--accent-warning, #e0a44c);
3043
+ }
3044
+
3045
+ .fb-dock-edit-actions {
3046
+ display: flex;
3047
+ gap: 6px;
3048
+ }
3049
+
3050
+ .fb-dock-edit-actions .fb-strip-figure {
3051
+ flex: 0 0 auto;
3052
+ }
3053
+
3054
+ /* -- the capture boxes, on the image ------------------------------------ */
3055
+ /*
3056
+ Where each capture came from, still there. Thin, unfilled and quiet at rest:
3057
+ a session can leave a dozen of these on one slide, and anything with a fill
3058
+ or a permanent label would end up being read as part of the tissue.
3059
+
3060
+ The container clips, which the wrapper it sits in does not: a box whose left
3061
+ edge is on screen and whose right edge is a mile off it would otherwise be a
3062
+ div a mile wide, laid over whatever is beside the viewer. */
3063
+
3064
+ .fb-boxes {
3065
+ overflow: hidden;
3066
+ pointer-events: none;
3067
+ position: absolute;
3068
+ z-index: 225;
3069
+ }
3070
+
3071
+ /* The BOX takes no clicks -- only the bands along its edges do (below). A
3072
+ filled hit area would turn every captured region into a place where panning,
3073
+ and every other tool's drag, quietly stopped working. */
3074
+ .fb-box {
3075
+ border: 1px solid rgba(186, 230, 253, 0.42);
3076
+ box-shadow: 0 0 0 1px rgba(2, 6, 23, 0.35);
3077
+ pointer-events: none;
3078
+ position: absolute;
3079
+ transition: border-color var(--duration-fast, 120ms) ease,
3080
+ box-shadow var(--duration-fast, 120ms) ease;
3081
+ }
3082
+
3083
+ .fb-box.is-hover {
3084
+ border-color: rgba(186, 230, 253, 0.85);
3085
+ box-shadow: 0 0 0 1px rgba(2, 6, 23, 0.55);
3086
+ }
3087
+
3088
+ /* Exactly one at a time, and everything else stays subdued: the point of the
3089
+ boxes is to be readable at a glance, which a wall of equally bright
3090
+ rectangles is not. */
3091
+ .fb-box.is-selected {
3092
+ border-color: rgba(56, 189, 248, 0.98);
3093
+ border-style: solid;
3094
+ box-shadow: 0 0 0 1px rgba(2, 6, 23, 0.7), 0 0 12px rgba(56, 189, 248, 0.35);
3095
+ }
3096
+
3097
+ /* Nine pixels of edge, straddling a one-pixel outline: a 1px hit target is a
3098
+ target nobody hits. */
3099
+ .fb-box-edge {
3100
+ cursor: pointer;
3101
+ pointer-events: auto;
3102
+ position: absolute;
3103
+ }
3104
+
3105
+ .fb-box-n { height: 9px; left: -4px; right: -4px; top: -5px; }
3106
+ .fb-box-s { bottom: -5px; height: 9px; left: -4px; right: -4px; }
3107
+ .fb-box-w { bottom: -4px; left: -5px; top: -4px; width: 9px; }
3108
+ .fb-box-e { bottom: -4px; right: -5px; top: -4px; width: 9px; }
3109
+
3110
+ /* The number that ties a box to its thumbnail. Hidden at rest for the same
3111
+ reason the outline is thin -- twelve numbered rectangles over tissue is a
3112
+ diagram, not a photograph. */
3113
+ .fb-box-tag {
3114
+ background: rgba(2, 6, 23, 0.78);
3115
+ border-radius: 3px;
3116
+ color: rgba(226, 232, 240, 0.95);
3117
+ cursor: pointer;
3118
+ font-size: 10px;
3119
+ left: -1px;
3120
+ line-height: 1.6;
3121
+ opacity: 0;
3122
+ padding: 0 5px;
3123
+ pointer-events: auto;
3124
+ position: absolute;
3125
+ top: -18px;
3126
+ transition: opacity var(--duration-fast, 120ms) ease;
3127
+ }
3128
+
3129
+ .fb-box.is-hover .fb-box-tag,
3130
+ .fb-box.is-selected .fb-box-tag {
3131
+ opacity: 1;
3132
+ }
3133
+
3134
+ .fb-box.is-selected .fb-box-tag {
3135
+ background: rgba(56, 189, 248, 0.95);
3136
+ color: #04121f;
3137
+ }
3138
+
3139
+
3140
+
3141
+ /* -- the viewfinder ----------------------------------------------------- */
3142
+ /*
3143
+ A dashed light border over a dark outline, so it reads on tissue of any
3144
+ brightness -- one stroke alone disappears against half the images this is
3145
+ used on. */
3146
+
3147
+ .fb-viewfinder {
3148
+ border: 1px dashed rgba(186, 230, 253, 0.95);
3149
+ border-radius: 2px;
3150
+ box-shadow: 0 0 0 1px rgba(2, 6, 23, 0.6), inset 0 0 0 1px rgba(2, 6, 23, 0.45);
3151
+ cursor: move;
3152
+ position: absolute;
3153
+ z-index: 230;
3154
+ }
3155
+
3156
+ /* Mid-redraw and mid-pan the frame must not be the thing under the pointer:
3157
+ the drag belongs to the image in both cases. */
3158
+ .fb-viewfinder.is-drawing,
3159
+ .fb-viewfinder.is-transparent {
3160
+ pointer-events: none;
3161
+ }
3162
+
3163
+ /* Locked onto a capture: the shutter will take THAT region and not whatever
3164
+ the frame is over, so the frame stops being a dashed rectangle you moved and
3165
+ becomes the solid one already on the image -- the same sky as a selected
3166
+ capture box, which is what it is sitting exactly on top of. */
3167
+ .fb-viewfinder.is-pinned {
3168
+ border-color: rgba(56, 189, 248, 0.98);
3169
+ border-style: solid;
3170
+ box-shadow: 0 0 0 1px rgba(2, 6, 23, 0.7), 0 0 12px rgba(56, 189, 248, 0.35);
3171
+ }
3172
+ /* Framing a panel that is on loan to the viewer, rather than aiming a shutter.
3173
+ Amber instead of sky, so the two states of the same rectangle cannot be
3174
+ mistaken for each other: one says "the next capture takes this", and this one
3175
+ says "this is where the panel's edges are". */
3176
+ .fb-viewfinder.is-framing {
3177
+ border-color: rgba(224, 164, 76, 0.98);
3178
+ border-style: solid;
3179
+ box-shadow: 0 0 0 1px rgba(2, 6, 23, 0.7), 0 0 12px rgba(224, 164, 76, 0.35);
3180
+ }
3181
+
3182
+
3183
+ .fb-viewfinder-caption {
3184
+ background: rgba(2, 6, 23, 0.72);
3185
+ border-radius: 3px;
3186
+ color: rgba(226, 232, 240, 0.95);
3187
+ font-size: 11px;
3188
+ left: 0;
3189
+ padding: 1px 6px;
3190
+ position: absolute;
3191
+ top: -21px;
3192
+ white-space: nowrap;
3193
+ }
3194
+
3195
+ .fb-viewfinder.is-pinned .fb-viewfinder-caption {
3196
+ background: rgba(8, 47, 73, 0.86);
3197
+ color: rgba(224, 242, 254, 0.98);
3198
+ }
3199
+
3200
+ /* :not(:hover) because this rule is more specific than .fb-shutter:hover and
3201
+ would otherwise win against it -- leaving the one button on the frame with no
3202
+ hover feedback at exactly the moment it has changed what it does. */
3203
+ .fb-viewfinder.is-pinned .fb-shutter:not(:hover) {
3204
+ background: rgba(8, 47, 73, 0.88);
3205
+ border-color: rgba(56, 189, 248, 0.6);
3206
+ }
3207
+
3208
+ .fb-vf-handle {
3209
+ background: rgba(186, 230, 253, 0.98);
3210
+ border: 1px solid rgba(2, 6, 23, 0.65);
3211
+ border-radius: 2px;
3212
+ height: 12px;
3213
+ position: absolute;
3214
+ width: 12px;
3215
+ }
3216
+
3217
+ .fb-vf-nw { cursor: nwse-resize; left: -6px; top: -6px; }
3218
+ .fb-vf-ne { cursor: nesw-resize; right: -6px; top: -6px; }
3219
+ .fb-vf-se { cursor: nwse-resize; bottom: -6px; right: -6px; }
3220
+ .fb-vf-sw { cursor: nesw-resize; bottom: -6px; left: -6px; }
3221
+
3222
+ /* On the frame, not in the dock: the shutter belongs to the thing it takes a
3223
+ picture of. It is a <button>, so it can never end up in the preview -- the
3224
+ preview is a crop of the viewer's canvases, and this is not one. */
3225
+ .fb-shutter {
3226
+ align-items: center;
3227
+ background: rgba(2, 6, 23, 0.78);
3228
+ border: 1px solid rgba(148, 163, 184, 0.45);
3229
+ border-radius: 999px;
3230
+ bottom: 8px;
3231
+ color: #f8fafc;
3232
+ cursor: pointer;
3233
+ display: inline-flex;
3234
+ font-size: 12px;
3235
+ gap: 6px;
3236
+ left: 50%;
3237
+ padding: 5px 10px;
3238
+ position: absolute;
3239
+ transform: translateX(-50%);
3240
+ }
3241
+
3242
+ /* The shortcut, written on the control it fires. The dock's caption lists it
3243
+ too, but the caption is at the other end of the frame and this is where the
3244
+ hand already is -- and a camera icon on its own says nothing about a key. */
3245
+ .fb-shutter-key {
3246
+ background: rgba(148, 163, 184, 0.22);
3247
+ border: 1px solid rgba(148, 163, 184, 0.4);
3248
+ border-radius: 3px;
3249
+ font-family: inherit;
3250
+ font-size: 10px;
3251
+ font-weight: 600;
3252
+ line-height: 1.5;
3253
+ padding: 0 4px;
3254
+ }
3255
+
3256
+ .fb-shutter:hover {
3257
+ background: rgba(56, 189, 248, 0.92);
3258
+ border-color: rgba(186, 230, 253, 0.9);
3259
+ color: #04121f;
3260
+ }
3261
+
3262
+ /* The key inverts with the button, or it stays a pale chip on a pale field. */
3263
+ .fb-shutter:hover .fb-shutter-key {
3264
+ background: rgba(2, 6, 23, 0.16);
3265
+ border-color: rgba(2, 6, 23, 0.32);
3266
+ }
3267
+
3268
+ /* No shutter blink any more: a shot ends capture mode, so the frame is removed
3269
+ at the moment it would have flashed. What says a capture happened is the frame
3270
+ going and the thumbnail arriving at the top of the strip. */
3271
+
3272
+ @media (prefers-reduced-motion: reduce) {
3273
+ .fb-orb,
3274
+ .fb-strip-chevron,
3275
+ .fb-strip-remove,
3276
+ .fb-box,
3277
+ .fb-box-tag {
3278
+ transition: none;
3279
+ }
3280
+ }
3281
+
3282
+ /* For screen readers only: the orb and the small round buttons carry icons,
3283
+ and an icon is not a name. */
3284
+ .fb-visually-hidden {
3285
+ clip: rect(0 0 0 0);
3286
+ clip-path: inset(50%);
3287
+ height: 1px;
3288
+ overflow: hidden;
3289
+ position: absolute;
3290
+ white-space: nowrap;
3291
+ width: 1px;
3292
+ }