chrome-devtools-frontend 1.0.941208 → 1.0.943182

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 (289) hide show
  1. package/WATCHLISTS +1 -1
  2. package/config/gni/all_devtools_files.gni +0 -64
  3. package/config/gni/devtools_grd_files.gni +54 -19
  4. package/config/gni/devtools_image_files.gni +1 -3
  5. package/front_end/.eslintrc.js +11 -1
  6. package/front_end/Images/src/{feedback_thin_16x16_icon.svg → survey_feedback_icon.svg} +1 -1
  7. package/front_end/Tests.js +1 -32
  8. package/front_end/core/common/Color.ts +5 -0
  9. package/front_end/core/i18n/locales/en-US.json +17 -29
  10. package/front_end/core/i18n/locales/en-XL.json +17 -29
  11. package/front_end/core/sdk/CPUProfilerModel.ts +7 -9
  12. package/front_end/core/sdk/ConsoleModel.ts +27 -33
  13. package/front_end/core/sdk/DebuggerModel.ts +4 -14
  14. package/front_end/core/sdk/sdk-meta.ts +17 -3
  15. package/front_end/entrypoints/device_mode_emulation_frame/device_mode_emulation_frame.ts +1 -1
  16. package/front_end/entrypoints/devtools_app/devtools_app.js +1 -1
  17. package/front_end/entrypoints/devtools_app/devtools_app.json +1 -12
  18. package/front_end/entrypoints/formatter_worker/formatter_worker-entrypoint.ts +1 -1
  19. package/front_end/entrypoints/heap_snapshot_worker/heap_snapshot_worker-entrypoint.ts +1 -1
  20. package/front_end/entrypoints/inspector/inspector.js +1 -1
  21. package/front_end/entrypoints/inspector/inspector.json +1 -3
  22. package/front_end/entrypoints/inspector_main/inspector_main-meta.ts +2 -3
  23. package/front_end/entrypoints/js_app/js_app.js +1 -1
  24. package/front_end/entrypoints/js_app/js_app.json +1 -3
  25. package/front_end/entrypoints/main/MainImpl.ts +26 -0
  26. package/front_end/entrypoints/main/main-meta.ts +1 -2
  27. package/front_end/entrypoints/ndb_app/ndb_app.js +1 -1
  28. package/front_end/entrypoints/node_app/node_app-meta.ts +0 -2
  29. package/front_end/entrypoints/node_app/node_app.js +1 -1
  30. package/front_end/entrypoints/node_app/node_app.json +1 -3
  31. package/front_end/entrypoints/node_main/node_main-meta.ts +0 -1
  32. package/front_end/entrypoints/shell/shell.js +0 -11
  33. package/front_end/entrypoints/shell/shell.json +1 -5
  34. package/front_end/entrypoints/wasmparser_worker/wasmparser_worker-entrypoint.ts +1 -1
  35. package/front_end/entrypoints/worker_app/worker_app.js +1 -1
  36. package/front_end/entrypoints/worker_app/worker_app.json +1 -7
  37. package/front_end/generated/InspectorBackendCommands.js +19 -0
  38. package/front_end/generated/protocol-mapping.d.ts +31 -1
  39. package/front_end/generated/protocol-proxy-api.d.ts +34 -2
  40. package/front_end/generated/protocol.d.ts +81 -6
  41. package/front_end/legacy_test_runner/bindings_test_runner/IsolatedFilesystemTestRunner.js +2 -2
  42. package/front_end/legacy_test_runner/console_test_runner/console_test_runner.js +5 -1
  43. package/front_end/legacy_test_runner/legacy_test_runner.ts +10 -1
  44. package/front_end/legacy_test_runner/test_runner/TestRunner.js +3 -1
  45. package/front_end/models/formatter/SourceFormatter.ts +0 -10
  46. package/front_end/models/persistence/persistence-meta.ts +0 -1
  47. package/front_end/models/workspace/UISourceCode.ts +9 -42
  48. package/front_end/panels/accessibility/accessibility-meta.ts +0 -1
  49. package/front_end/panels/animation/AnimationTimeline.ts +3 -3
  50. package/front_end/panels/animation/animation-meta.ts +0 -1
  51. package/front_end/panels/application/ApplicationPanelSidebar.ts +3 -3
  52. package/front_end/panels/application/BackForwardCacheStrings.ts +3 -1
  53. package/front_end/panels/application/application-meta.ts +0 -4
  54. package/front_end/panels/application/components/EndpointsGrid.ts +1 -1
  55. package/front_end/panels/application/components/ReportsGrid.ts +1 -1
  56. package/front_end/panels/application/components/stackTraceRow.css +8 -0
  57. package/front_end/panels/browser_debugger/browser_debugger-meta.ts +1 -2
  58. package/front_end/panels/changes/changes-meta.ts +0 -1
  59. package/front_end/panels/console/ConsolePinPane.ts +23 -32
  60. package/front_end/panels/console/ConsoleViewMessage.ts +8 -1
  61. package/front_end/panels/console/console-meta.ts +0 -1
  62. package/front_end/panels/console_counters/console_counters-meta.ts +0 -1
  63. package/front_end/panels/coverage/CoverageDecorationManager.ts +4 -5
  64. package/front_end/panels/coverage/CoverageView.ts +2 -105
  65. package/front_end/panels/coverage/coverage-meta.ts +0 -1
  66. package/front_end/panels/css_overview/components/CSSOverviewStartView.ts +11 -56
  67. package/front_end/panels/css_overview/components/cssOverviewStartView.css +1 -8
  68. package/front_end/panels/css_overview/css_overview-meta.ts +0 -1
  69. package/front_end/panels/developer_resources/developer_resources-meta.ts +0 -1
  70. package/front_end/panels/elements/ElementsTreeElement.ts +4 -9
  71. package/front_end/panels/elements/components/StylePropertyEditor.ts +2 -0
  72. package/front_end/panels/elements/components/adornerSettingsPane.css +0 -4
  73. package/front_end/panels/elements/elements-meta.ts +0 -1
  74. package/front_end/panels/emulation/DeviceModeToolbar.ts +3 -1
  75. package/front_end/panels/emulation/DeviceModeView.ts +2 -1
  76. package/front_end/panels/emulation/InspectedPagePlaceholder.ts +3 -1
  77. package/front_end/panels/emulation/MediaQueryInspector.ts +3 -1
  78. package/front_end/panels/emulation/emulation-meta.ts +2 -5
  79. package/front_end/panels/help/help-meta.ts +0 -1
  80. package/front_end/panels/input/input-meta.ts +0 -1
  81. package/front_end/panels/issues/issues-meta.ts +0 -3
  82. package/front_end/panels/js_profiler/js_profiler-meta.ts +0 -4
  83. package/front_end/panels/layers/layers-meta.ts +0 -4
  84. package/front_end/panels/lighthouse/LighthousePanel.ts +2 -4
  85. package/front_end/panels/lighthouse/LighthouseReportRenderer.ts +1 -4
  86. package/front_end/panels/lighthouse/lighthouse-meta.ts +0 -1
  87. package/front_end/panels/lighthouse/lighthouseStartView.css +4 -0
  88. package/front_end/panels/lighthouse/module.json +0 -6
  89. package/front_end/panels/media/media-meta.ts +0 -4
  90. package/front_end/panels/mobile_throttling/mobile_throttling-meta.ts +0 -1
  91. package/front_end/panels/network/ResourceWebSocketFrameView.ts +2 -1
  92. package/front_end/panels/network/network-meta.ts +1 -5
  93. package/front_end/panels/performance_monitor/performance_monitor-meta.ts +0 -1
  94. package/front_end/panels/profiler/CPUProfileView.ts +10 -3
  95. package/front_end/panels/profiler/profiler-meta.ts +0 -3
  96. package/front_end/panels/protocol_monitor/protocol_monitor-meta.ts +0 -1
  97. package/front_end/panels/screencast/screencast-meta.ts +0 -4
  98. package/front_end/panels/security/security-meta.ts +0 -4
  99. package/front_end/panels/sensors/sensors-meta.ts +0 -1
  100. package/front_end/panels/settings/emulation/emulation-meta.ts +0 -1
  101. package/front_end/panels/settings/settings-meta.ts +0 -1
  102. package/front_end/panels/sources/BreakpointEditDialog.ts +16 -30
  103. package/front_end/panels/sources/CSSPlugin.ts +310 -331
  104. package/front_end/panels/sources/CallStackSidebarPane.ts +28 -34
  105. package/front_end/panels/sources/CoveragePlugin.ts +121 -6
  106. package/front_end/panels/sources/DebuggerPlugin.ts +1166 -1243
  107. package/front_end/panels/sources/EditingLocationHistoryManager.ts +71 -101
  108. package/front_end/panels/sources/GoToLineQuickOpen.ts +4 -3
  109. package/front_end/panels/sources/InplaceFormatterEditorAction.ts +3 -3
  110. package/front_end/panels/sources/JavaScriptCompilerPlugin.ts +26 -23
  111. package/front_end/panels/sources/Plugin.ts +20 -4
  112. package/front_end/panels/sources/ProfilePlugin.ts +185 -0
  113. package/front_end/panels/sources/ScriptFormatterEditorAction.ts +3 -3
  114. package/front_end/panels/sources/ScriptOriginPlugin.ts +0 -10
  115. package/front_end/panels/sources/SnippetsPlugin.ts +1 -10
  116. package/front_end/panels/sources/SourcesPanel.ts +6 -5
  117. package/front_end/panels/sources/SourcesView.ts +10 -8
  118. package/front_end/panels/sources/TabbedEditorContainer.ts +31 -27
  119. package/front_end/panels/sources/UISourceCodeFrame.ts +335 -470
  120. package/front_end/panels/sources/WatchExpressionsSidebarPane.ts +3 -2
  121. package/front_end/panels/sources/sources-legacy.ts +0 -6
  122. package/front_end/panels/sources/sources-meta.ts +2 -6
  123. package/front_end/panels/sources/sources.ts +0 -2
  124. package/front_end/panels/timeline/timeline-meta.ts +2 -9
  125. package/front_end/panels/web_audio/web_audio-meta.ts +0 -1
  126. package/front_end/panels/webauthn/webauthn-meta.ts +0 -1
  127. package/front_end/third_party/codemirror.next/bundle.ts +9 -13
  128. package/front_end/third_party/codemirror.next/chunk/codemirror.js +1 -1
  129. package/front_end/third_party/codemirror.next/chunk/javascript.js +2 -2
  130. package/front_end/third_party/codemirror.next/chunk/markdown.js +2 -6
  131. package/front_end/third_party/codemirror.next/chunk/php.js +2 -6
  132. package/front_end/third_party/codemirror.next/chunk/python.js +1 -1
  133. package/front_end/third_party/codemirror.next/chunk/wast.js +1 -1
  134. package/front_end/third_party/codemirror.next/chunk/xml.js +2 -2
  135. package/front_end/third_party/codemirror.next/codemirror.next.d.ts +279 -198
  136. package/front_end/third_party/codemirror.next/codemirror.next.js +1 -1
  137. package/front_end/third_party/codemirror.next/package.json +13 -11
  138. package/front_end/third_party/lighthouse/lighthouse-dt-bundle.js +1128 -1158
  139. package/front_end/third_party/lighthouse/locales/ar-XB.json +211 -79
  140. package/front_end/third_party/lighthouse/locales/ar.json +213 -81
  141. package/front_end/third_party/lighthouse/locales/bg.json +211 -79
  142. package/front_end/third_party/lighthouse/locales/ca.json +212 -80
  143. package/front_end/third_party/lighthouse/locales/cs.json +211 -79
  144. package/front_end/third_party/lighthouse/locales/da.json +211 -79
  145. package/front_end/third_party/lighthouse/locales/de.json +211 -79
  146. package/front_end/third_party/lighthouse/locales/el.json +213 -81
  147. package/front_end/third_party/lighthouse/locales/en-GB.json +211 -79
  148. package/front_end/third_party/lighthouse/locales/en-US.json +186 -75
  149. package/front_end/third_party/lighthouse/locales/en-XA.json +211 -79
  150. package/front_end/third_party/lighthouse/locales/en-XL.json +186 -75
  151. package/front_end/third_party/lighthouse/locales/es-419.json +211 -79
  152. package/front_end/third_party/lighthouse/locales/es.json +212 -80
  153. package/front_end/third_party/lighthouse/locales/fi.json +211 -79
  154. package/front_end/third_party/lighthouse/locales/fil.json +211 -79
  155. package/front_end/third_party/lighthouse/locales/fr.json +211 -79
  156. package/front_end/third_party/lighthouse/locales/he.json +212 -80
  157. package/front_end/third_party/lighthouse/locales/hi.json +214 -82
  158. package/front_end/third_party/lighthouse/locales/hr.json +211 -79
  159. package/front_end/third_party/lighthouse/locales/hu.json +211 -79
  160. package/front_end/third_party/lighthouse/locales/id.json +211 -79
  161. package/front_end/third_party/lighthouse/locales/it.json +211 -79
  162. package/front_end/third_party/lighthouse/locales/ja.json +211 -79
  163. package/front_end/third_party/lighthouse/locales/ko.json +211 -79
  164. package/front_end/third_party/lighthouse/locales/lt.json +211 -79
  165. package/front_end/third_party/lighthouse/locales/lv.json +214 -82
  166. package/front_end/third_party/lighthouse/locales/nl.json +211 -79
  167. package/front_end/third_party/lighthouse/locales/no.json +211 -79
  168. package/front_end/third_party/lighthouse/locales/pl.json +211 -79
  169. package/front_end/third_party/lighthouse/locales/pt-PT.json +211 -79
  170. package/front_end/third_party/lighthouse/locales/pt.json +211 -79
  171. package/front_end/third_party/lighthouse/locales/ro.json +212 -80
  172. package/front_end/third_party/lighthouse/locales/ru.json +211 -79
  173. package/front_end/third_party/lighthouse/locales/sk.json +211 -79
  174. package/front_end/third_party/lighthouse/locales/sl.json +211 -79
  175. package/front_end/third_party/lighthouse/locales/sr-Latn.json +211 -79
  176. package/front_end/third_party/lighthouse/locales/sr.json +211 -79
  177. package/front_end/third_party/lighthouse/locales/sv.json +211 -79
  178. package/front_end/third_party/lighthouse/locales/ta.json +218 -86
  179. package/front_end/third_party/lighthouse/locales/te.json +251 -119
  180. package/front_end/third_party/lighthouse/locales/th.json +211 -79
  181. package/front_end/third_party/lighthouse/locales/tr.json +211 -79
  182. package/front_end/third_party/lighthouse/locales/uk.json +212 -80
  183. package/front_end/third_party/lighthouse/locales/vi.json +211 -79
  184. package/front_end/third_party/lighthouse/locales/zh-HK.json +211 -79
  185. package/front_end/third_party/lighthouse/locales/zh-TW.json +211 -79
  186. package/front_end/third_party/lighthouse/locales/zh.json +211 -79
  187. package/front_end/third_party/lighthouse/report/bundle.d.ts +72 -34
  188. package/front_end/third_party/lighthouse/report/bundle.js +698 -492
  189. package/front_end/third_party/lighthouse/report-assets/report-generator.js +1 -2
  190. package/front_end/third_party/lighthouse/report-assets/report.js +40 -35
  191. package/front_end/third_party/lighthouse/report-assets/standalone-template.html +2 -4
  192. package/front_end/ui/components/code_highlighter/CodeHighlighter.ts +60 -68
  193. package/front_end/ui/components/data_grid/dataGrid.css +12 -10
  194. package/front_end/ui/components/docs/component_docs.ts +14 -0
  195. package/front_end/ui/components/docs/create_breadcrumbs.ts +1 -1
  196. package/front_end/ui/components/docs/css_overview/start_view.html +25 -0
  197. package/front_end/ui/components/docs/css_overview/start_view.ts +14 -0
  198. package/front_end/ui/components/docs/icon_button/basic.ts +3 -3
  199. package/front_end/ui/components/docs/toggle_dark_mode.ts +1 -0
  200. package/front_end/ui/components/docs/toggle_fonts.ts +2 -0
  201. package/front_end/ui/components/helpers/get-stylesheet.ts +0 -15
  202. package/front_end/ui/components/linear_memory_inspector/linear_memory_inspector-meta.ts +1 -2
  203. package/front_end/ui/components/markdown_view/MarkdownImagesMap.ts +1 -1
  204. package/front_end/ui/components/survey_link/SurveyLink.ts +1 -1
  205. package/front_end/ui/components/text_editor/TextEditor.ts +79 -36
  206. package/front_end/ui/components/text_editor/config.ts +42 -26
  207. package/front_end/ui/components/text_editor/javascript.ts +2 -3
  208. package/front_end/ui/components/text_editor/position.ts +17 -0
  209. package/front_end/ui/components/text_editor/text_editor.ts +1 -0
  210. package/front_end/ui/components/text_editor/theme.ts +5 -1
  211. package/front_end/ui/components/tree_outline/TreeOutline.ts +63 -8
  212. package/front_end/ui/components/tree_outline/TreeOutlineUtils.ts +8 -6
  213. package/front_end/ui/legacy/Dialog.ts +3 -1
  214. package/front_end/ui/legacy/DropTarget.ts +2 -1
  215. package/front_end/ui/legacy/EmptyWidget.ts +2 -1
  216. package/front_end/ui/legacy/FilterBar.ts +2 -1
  217. package/front_end/ui/legacy/GlassPane.ts +4 -2
  218. package/front_end/ui/legacy/Infobar.ts +5 -8
  219. package/front_end/ui/legacy/InspectorView.ts +6 -1
  220. package/front_end/ui/legacy/ListWidget.ts +2 -1
  221. package/front_end/ui/legacy/PopoverHelper.ts +2 -1
  222. package/front_end/ui/legacy/ProgressIndicator.ts +2 -1
  223. package/front_end/ui/legacy/RemoteDebuggingTerminatedScreen.ts +2 -1
  224. package/front_end/ui/legacy/ReportView.ts +2 -1
  225. package/front_end/ui/legacy/RootView.ts +2 -1
  226. package/front_end/ui/legacy/SearchableView.ts +2 -1
  227. package/front_end/ui/legacy/ShortcutRegistry.ts +11 -7
  228. package/front_end/ui/legacy/SoftContextMenu.ts +3 -2
  229. package/front_end/ui/legacy/SoftDropDown.ts +4 -2
  230. package/front_end/ui/legacy/SplitWidget.ts +2 -1
  231. package/front_end/ui/legacy/SuggestBox.ts +2 -1
  232. package/front_end/ui/legacy/TabbedPane.ts +2 -1
  233. package/front_end/ui/legacy/TargetCrashedScreen.ts +2 -1
  234. package/front_end/ui/legacy/TextPrompt.ts +2 -1
  235. package/front_end/ui/legacy/Toolbar.ts +3 -2
  236. package/front_end/ui/legacy/Treeoutline.ts +4 -3
  237. package/front_end/ui/legacy/UIUtils.ts +17 -14
  238. package/front_end/ui/legacy/ViewManager.ts +2 -1
  239. package/front_end/ui/legacy/components/inline_editor/CSSAngle.ts +1 -2
  240. package/front_end/ui/legacy/components/inline_editor/cssAngle.css +4 -0
  241. package/front_end/ui/legacy/components/object_ui/JavaScriptREPL.ts +2 -2
  242. package/front_end/ui/legacy/components/object_ui/object_ui-meta.ts +0 -4
  243. package/front_end/ui/legacy/components/perf_ui/LineLevelProfile.ts +35 -131
  244. package/front_end/ui/legacy/components/perf_ui/perf_ui-meta.ts +0 -3
  245. package/front_end/ui/legacy/components/quick_open/filteredListWidget.css +2 -2
  246. package/front_end/ui/legacy/components/quick_open/quick_open-meta.ts +2 -3
  247. package/front_end/ui/legacy/components/source_frame/BinaryResourceViewFactory.ts +3 -6
  248. package/front_end/ui/legacy/components/source_frame/FontView.ts +4 -1
  249. package/front_end/ui/legacy/components/source_frame/ImageView.ts +4 -1
  250. package/front_end/ui/legacy/components/source_frame/JSONView.ts +4 -1
  251. package/front_end/ui/legacy/components/source_frame/ResourceSourceFrame.ts +21 -15
  252. package/front_end/ui/legacy/components/source_frame/SourceFrame.ts +501 -252
  253. package/front_end/ui/legacy/components/source_frame/XMLView.ts +7 -2
  254. package/front_end/ui/legacy/components/source_frame/source_frame-legacy.ts +0 -11
  255. package/front_end/ui/legacy/components/source_frame/source_frame.ts +0 -2
  256. package/front_end/ui/legacy/components/text_editor/CodeMirrorTextEditor.ts +2 -0
  257. package/front_end/ui/legacy/components/text_editor/cmdevtools.css +3 -1
  258. package/front_end/ui/legacy/components/text_editor/module.json +0 -3
  259. package/front_end/ui/legacy/components/utils/Linkifier.ts +7 -15
  260. package/front_end/ui/legacy/radioButton.css +1 -13
  261. package/front_end/ui/legacy/textButton.css +5 -4
  262. package/front_end/ui/legacy/themeColors.css +36 -0
  263. package/front_end/ui/legacy/theme_support/theme_support_impl.ts +7 -9
  264. package/front_end/ui/legacy/utils/create-shadow-root-with-core-styles.ts +2 -2
  265. package/front_end/ui/legacy/utils/inject-core-styles.ts +7 -4
  266. package/package.json +1 -1
  267. package/scripts/check_gn.js +0 -35
  268. package/scripts/eslint_rules/lib/es_modules_import.js +15 -8
  269. package/scripts/eslint_rules/tests/es_modules_import_test.js +8 -0
  270. package/front_end/Images/radioDot-dark-theme.png +0 -0
  271. package/front_end/Images/radioDot.png +0 -0
  272. package/front_end/emulated_devices/module.json +0 -6
  273. package/front_end/panels/application/module.json +0 -6
  274. package/front_end/panels/emulation/module.json +0 -11
  275. package/front_end/panels/issues/module.json +0 -6
  276. package/front_end/panels/js_profiler/module.json +0 -5
  277. package/front_end/panels/layer_viewer/module.json +0 -5
  278. package/front_end/panels/layers/module.json +0 -5
  279. package/front_end/panels/media/module.json +0 -5
  280. package/front_end/panels/network/module.json +0 -5
  281. package/front_end/panels/profiler/module.json +0 -5
  282. package/front_end/panels/screencast/module.json +0 -6
  283. package/front_end/panels/security/module.json +0 -5
  284. package/front_end/panels/timeline/module.json +0 -7
  285. package/front_end/third_party/lighthouse/report-assets/report.css +0 -1774
  286. package/front_end/ui/legacy/components/source_frame/SourcesTextEditor.ts +0 -1030
  287. package/front_end/ui/legacy/components/source_frame/messagesPopover.css +0 -32
  288. package/front_end/ui/legacy/components/source_frame/module.json +0 -14
  289. package/front_end/ui/legacy/module.json +0 -41
@@ -38,6 +38,10 @@ const listOfTlds = [
38
38
  ];
39
39
 
40
40
  class Util {
41
+ /** @type {I18n<typeof UIStrings>} */
42
+ // @ts-expect-error: Is set in report renderer.
43
+ static i18n = null;
44
+
41
45
  static get PASS_THRESHOLD() {
42
46
  return PASS_THRESHOLD;
43
47
  }
@@ -103,6 +107,23 @@ class Util {
103
107
  /** @type {Map<string, Array<LH.ReportResult.AuditRef>>} */
104
108
  const relevantAuditToMetricsMap = new Map();
105
109
 
110
+ // This backcompat converts old LHRs (<9.0.0) to use the new "hidden" group.
111
+ // Old LHRs used "no group" to identify audits that should be hidden in performance instead of the "hidden" group.
112
+ // Newer LHRs use "no group" to identify opportunities and diagnostics whose groups are assigned by details type.
113
+ const [majorVersion] = clone.lighthouseVersion.split('.').map(Number);
114
+ const perfCategory = clone.categories['performance'];
115
+ if (majorVersion < 9 && perfCategory) {
116
+ if (!clone.categoryGroups) clone.categoryGroups = {};
117
+ clone.categoryGroups['hidden'] = {title: ''};
118
+ for (const auditRef of perfCategory.auditRefs) {
119
+ if (!auditRef.group) {
120
+ auditRef.group = 'hidden';
121
+ } else if (['load-opportunities', 'diagnostics'].includes(auditRef.group)) {
122
+ delete auditRef.group;
123
+ }
124
+ }
125
+ }
126
+
106
127
  for (const category of Object.values(clone.categories)) {
107
128
  // Make basic lookup table for relevantAudits
108
129
  category.auditRefs.forEach(metricRef => {
@@ -166,6 +187,8 @@ class Util {
166
187
 
167
188
  /**
168
189
  * Convert a score to a rating label.
190
+ * TODO: Return `'error'` for `score === null && !scoreDisplayMode`.
191
+ *
169
192
  * @param {number|null} score
170
193
  * @param {string=} scoreDisplayMode
171
194
  * @return {string}
@@ -386,43 +409,21 @@ class Util {
386
409
  return hostname.split('.').slice(-splitTld.length).join('.');
387
410
  }
388
411
 
389
- /**
390
- * @param {LH.Result['configSettings']} settings
391
- * @return {!Array<{name: string, description: string}>}
392
- */
393
- static getEnvironmentDisplayValues(settings) {
394
- const emulationDesc = Util.getEmulationDescriptions(settings);
395
-
396
- return [
397
- {
398
- name: Util.i18n.strings.runtimeSettingsDevice,
399
- description: emulationDesc.deviceEmulation,
400
- },
401
- {
402
- name: Util.i18n.strings.runtimeSettingsNetworkThrottling,
403
- description: emulationDesc.networkThrottling,
404
- },
405
- {
406
- name: Util.i18n.strings.runtimeSettingsCPUThrottling,
407
- description: emulationDesc.cpuThrottling,
408
- },
409
- ];
410
- }
411
412
 
412
413
  /**
413
414
  * @param {LH.Result['configSettings']} settings
414
- * @return {{deviceEmulation: string, networkThrottling: string, cpuThrottling: string}}
415
+ * @return {!{deviceEmulation: string, networkThrottling: string, cpuThrottling: string, summary: string}}
415
416
  */
416
417
  static getEmulationDescriptions(settings) {
417
418
  let cpuThrottling;
418
419
  let networkThrottling;
420
+ let summary;
419
421
 
420
422
  const throttling = settings.throttling;
421
423
 
422
424
  switch (settings.throttlingMethod) {
423
425
  case 'provided':
424
- cpuThrottling = Util.i18n.strings.throttlingProvided;
425
- networkThrottling = Util.i18n.strings.throttlingProvided;
426
+ summary = networkThrottling = cpuThrottling = Util.i18n.strings.throttlingProvided;
426
427
  break;
427
428
  case 'devtools': {
428
429
  const {cpuSlowdownMultiplier, requestLatencyMs} = throttling;
@@ -430,6 +431,13 @@ class Util {
430
431
  networkThrottling = `${Util.i18n.formatNumber(requestLatencyMs)}${NBSP}ms HTTP RTT, ` +
431
432
  `${Util.i18n.formatNumber(throttling.downloadThroughputKbps)}${NBSP}Kbps down, ` +
432
433
  `${Util.i18n.formatNumber(throttling.uploadThroughputKbps)}${NBSP}Kbps up (DevTools)`;
434
+
435
+ const isSlow4G = () => {
436
+ return requestLatencyMs === 150 * 3.75 &&
437
+ throttling.downloadThroughputKbps === 1.6 * 1024 * 0.9 &&
438
+ throttling.uploadThroughputKbps === 750 * 0.9;
439
+ };
440
+ summary = isSlow4G() ? Util.i18n.strings.runtimeSlow4g : Util.i18n.strings.runtimeCustom;
433
441
  break;
434
442
  }
435
443
  case 'simulate': {
@@ -437,11 +445,15 @@ class Util {
437
445
  cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (Simulated)`;
438
446
  networkThrottling = `${Util.i18n.formatNumber(rttMs)}${NBSP}ms TCP RTT, ` +
439
447
  `${Util.i18n.formatNumber(throughputKbps)}${NBSP}Kbps throughput (Simulated)`;
448
+
449
+ const isSlow4G = () => {
450
+ return rttMs === 150 && throughputKbps === 1.6 * 1024;
451
+ };
452
+ summary = isSlow4G() ? Util.i18n.strings.runtimeSlow4g : Util.i18n.strings.runtimeCustom;
440
453
  break;
441
454
  }
442
455
  default:
443
- cpuThrottling = Util.i18n.strings.runtimeUnknown;
444
- networkThrottling = Util.i18n.strings.runtimeUnknown;
456
+ summary = cpuThrottling = networkThrottling = Util.i18n.strings.runtimeUnknown;
445
457
  }
446
458
 
447
459
  // TODO(paulirish): revise Runtime Settings strings: https://github.com/GoogleChrome/lighthouse/pull/11796
@@ -454,6 +466,7 @@ class Util {
454
466
  deviceEmulation,
455
467
  cpuThrottling,
456
468
  networkThrottling,
469
+ summary,
457
470
  };
458
471
  }
459
472
 
@@ -505,6 +518,43 @@ class Util {
505
518
  static isPluginCategory(categoryId) {
506
519
  return categoryId.startsWith('lighthouse-plugin-');
507
520
  }
521
+
522
+ /**
523
+ * @param {LH.Result.GatherMode} gatherMode
524
+ */
525
+ static shouldDisplayAsFraction(gatherMode) {
526
+ return gatherMode === 'timespan' || gatherMode === 'snapshot';
527
+ }
528
+
529
+ /**
530
+ * @param {LH.ReportResult.Category} category
531
+ */
532
+ static calculateCategoryFraction(category) {
533
+ let numPassableAudits = 0;
534
+ let numPassed = 0;
535
+ let numInformative = 0;
536
+ let totalWeight = 0;
537
+ for (const auditRef of category.auditRefs) {
538
+ const auditPassed = Util.showAsPassed(auditRef.result);
539
+
540
+ // Don't count the audit if it's manual, N/A, or isn't displayed.
541
+ if (auditRef.group === 'hidden' ||
542
+ auditRef.result.scoreDisplayMode === 'manual' ||
543
+ auditRef.result.scoreDisplayMode === 'notApplicable') {
544
+ continue;
545
+ } else if (auditRef.result.scoreDisplayMode === 'informative') {
546
+ if (!auditPassed) {
547
+ ++numInformative;
548
+ }
549
+ continue;
550
+ }
551
+
552
+ ++numPassableAudits;
553
+ totalWeight += auditRef.weight;
554
+ if (auditPassed) numPassed++;
555
+ }
556
+ return {numPassed, numPassableAudits, numInformative, totalWeight};
557
+ }
508
558
  }
509
559
 
510
560
  /**
@@ -524,14 +574,10 @@ Util.getUniqueSuffix = (() => {
524
574
  };
525
575
  })();
526
576
 
527
- /** @type {I18n<typeof Util['UIStrings']>} */
528
- // @ts-expect-error: Is set in report renderer.
529
- Util.i18n = null;
530
-
531
577
  /**
532
578
  * Report-renderer-specific strings.
533
579
  */
534
- Util.UIStrings = {
580
+ const UIStrings = {
535
581
  /** Disclaimer shown to users below the metric values (First Contentful Paint, Time to Interactive, etc) to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. */
536
582
  varianceDisclaimer: 'Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics.',
537
583
  /** Text link pointing to an interactive calculator that explains Lighthouse scoring. The link text should be fairly short. */
@@ -598,22 +644,12 @@ Util.UIStrings = {
598
644
  /** Option in a dropdown menu that toggles the themeing of the report between Light(default) and Dark themes. */
599
645
  dropdownDarkTheme: 'Toggle Dark Theme',
600
646
 
601
- /** Title of the Runtime settings table in a Lighthouse report. Runtime settings are the environment configurations that a specific report used at auditing time. */
602
- runtimeSettingsTitle: 'Runtime Settings',
603
- /** Label for a row in a table that shows the URL that was audited during a Lighthouse run. */
604
- runtimeSettingsUrl: 'URL',
605
- /** Label for a row in a table that shows the time at which a Lighthouse run was conducted; formatted as a timestamp, e.g. Jan 1, 1970 12:00 AM UTC. */
606
- runtimeSettingsFetchTime: 'Fetch Time',
607
647
  /** Label for a row in a table that describes the kind of device that was emulated for the Lighthouse run. Example values for row elements: 'No Emulation', 'Emulated Desktop', etc. */
608
648
  runtimeSettingsDevice: 'Device',
609
649
  /** Label for a row in a table that describes the network throttling conditions that were used during a Lighthouse run, if any. */
610
650
  runtimeSettingsNetworkThrottling: 'Network throttling',
611
651
  /** Label for a row in a table that describes the CPU throttling conditions that were used during a Lighthouse run, if any.*/
612
652
  runtimeSettingsCPUThrottling: 'CPU throttling',
613
- /** Label for a row in a table that shows in what tool Lighthouse is being run (e.g. The lighthouse CLI, Chrome DevTools, Lightrider, WebPageTest, etc). */
614
- runtimeSettingsChannel: 'Channel',
615
- /** Label for a row in a table that shows the User Agent that was detected on the Host machine that ran Lighthouse. */
616
- runtimeSettingsUA: 'User agent (host)',
617
653
  /** Label for a row in a table that shows the User Agent that was used to send out all network requests during the Lighthouse run. */
618
654
  runtimeSettingsUANetwork: 'User agent (network)',
619
655
  /** Label for a row in a table that shows the estimated CPU power of the machine running Lighthouse. Example row values: 532, 1492, 783. */
@@ -632,12 +668,29 @@ Util.UIStrings = {
632
668
  runtimeDesktopEmulation: 'Emulated Desktop',
633
669
  /** Descriptive explanation for a runtime setting that is set to an unknown value. */
634
670
  runtimeUnknown: 'Unknown',
671
+ /** Descriptive label that this analysis run was from a single pageload of a browser (not a summary of hundreds of loads) */
672
+ runtimeSingleLoad: 'Single page load',
673
+ /** Descriptive label that this analysis only considers the initial load of the page, and no interaction beyond when the page had "fully loaded" */
674
+ runtimeAnalysisWindow: 'Initial page load',
675
+ /** Descriptive explanation that this analysis run was from a single pageload of a browser, whereas field data often summarizes hundreds+ of page loads */
676
+ runtimeSingleLoadTooltip: 'This data is taken from a single page load, as opposed to field data summarizing many sessions.', // eslint-disable-line max-len
635
677
 
636
678
  /** Descriptive explanation for environment throttling that was provided by the runtime environment instead of provided by Lighthouse throttling. */
637
679
  throttlingProvided: 'Provided by environment',
680
+ /** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Show' and 'Hide'. */
681
+ show: 'Show',
682
+ /** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Show' and 'Hide'. */
683
+ hide: 'Hide',
684
+ /** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Expand view' and 'Collapse view'. */
685
+ expandView: 'Expand view',
686
+ /** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Expand view' and 'Collapse view'. */
687
+ collapseView: 'Collapse view',
688
+ /** Label indicating that Lighthouse throttled the page to emulate a slow 4G network connection. */
689
+ runtimeSlow4g: 'Slow 4G throttling',
690
+ /** Label indicating that Lighthouse throttled the page using custom throttling settings. */
691
+ runtimeCustom: 'Custom throttling',
638
692
  };
639
-
640
- Util.UIStrings;
693
+ Util.UIStrings = UIStrings;
641
694
 
642
695
  // auto-generated by build/build-report-components.js
643
696
 
@@ -650,9 +703,9 @@ Util.UIStrings;
650
703
  * @param {DOM} dom
651
704
  */
652
705
  function create3pFilterComponent(dom) {
653
- const el0 = dom.document().createDocumentFragment();
706
+ const el0 = dom.createFragment();
654
707
  const el1 = dom.createElement('style');
655
- el1.append('\n .lh-3p-filter {\n background-color: var(--table-higlight-background-color);\n color: var(--color-gray-600);\n float: right;\n padding: 6px;\n }\n .lh-3p-filter-label, .lh-3p-filter-input {\n vertical-align: middle;\n user-select: none;\n }\n .lh-3p-filter-input:disabled + .lh-3p-ui-string {\n text-decoration: line-through;\n }\n ');
708
+ el1.append('\n .lh-3p-filter {\n color: var(--color-gray-600);\n float: right;\n padding: 6px var(--stackpack-padding-horizontal);\n }\n .lh-3p-filter-label, .lh-3p-filter-input {\n vertical-align: middle;\n user-select: none;\n }\n .lh-3p-filter-input:disabled + .lh-3p-ui-string {\n text-decoration: line-through;\n }\n ');
656
709
  el0.append(el1);
657
710
  const el2 = dom.createElement('div', 'lh-3p-filter');
658
711
  const el3 = dom.createElement('label', 'lh-3p-filter-label');
@@ -672,7 +725,7 @@ function create3pFilterComponent(dom) {
672
725
  * @param {DOM} dom
673
726
  */
674
727
  function createAuditComponent(dom) {
675
- const el0 = dom.document().createDocumentFragment();
728
+ const el0 = dom.createFragment();
676
729
  const el1 = dom.createElement('div', 'lh-audit');
677
730
  const el2 = dom.createElement('details', 'lh-expandable-details');
678
731
  const el3 = dom.createElement('summary');
@@ -697,7 +750,7 @@ function createAuditComponent(dom) {
697
750
  * @param {DOM} dom
698
751
  */
699
752
  function createCategoryHeaderComponent(dom) {
700
- const el0 = dom.document().createDocumentFragment();
753
+ const el0 = dom.createFragment();
701
754
  const el1 = dom.createElement('div', 'lh-category-header');
702
755
  const el2 = dom.createElement('div', 'lh-score__gauge');
703
756
  el2.setAttribute('role', 'heading');
@@ -712,7 +765,7 @@ function createCategoryHeaderComponent(dom) {
712
765
  * @param {DOM} dom
713
766
  */
714
767
  function createChevronComponent(dom) {
715
- const el0 = dom.document().createDocumentFragment();
768
+ const el0 = dom.createFragment();
716
769
  const el1 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg', 'lh-chevron');
717
770
  el1.setAttribute('viewBox', '0 0 100 100');
718
771
  const el2 = dom.createElementNS('http://www.w3.org/2000/svg', 'g', 'lh-chevron__lines');
@@ -730,17 +783,23 @@ function createChevronComponent(dom) {
730
783
  * @param {DOM} dom
731
784
  */
732
785
  function createClumpComponent(dom) {
733
- const el0 = dom.document().createDocumentFragment();
734
- const el1 = dom.createElement('details', 'lh-clump lh-audit-group');
735
- const el2 = dom.createElement('summary');
736
- const el3 = dom.createElement('div', 'lh-audit-group__summary');
737
- const el4 = dom.createElement('div', 'lh-audit-group__header');
738
- const el5 = dom.createElement('span', 'lh-audit-group__title');
739
- const el6 = dom.createElement('span', 'lh-audit-group__itemcount');
740
- el4.append(' ', el5, ' ', el6, ' ', ' ', ' ');
786
+ const el0 = dom.createFragment();
787
+ const el1 = dom.createElement('div', 'lh-audit-group');
788
+ const el2 = dom.createElement('details', 'lh-clump');
789
+ const el3 = dom.createElement('summary');
790
+ const el4 = dom.createElement('div', 'lh-audit-group__summary');
791
+ const el5 = dom.createElement('div', 'lh-audit-group__header');
792
+ const el6 = dom.createElement('span', 'lh-audit-group__title');
793
+ const el7 = dom.createElement('span', 'lh-audit-group__itemcount');
794
+ el5.append(' ', el6, ' ', el7, ' ', ' ', ' ');
795
+ const el8 = dom.createElement('div', 'lh-clump-toggle');
796
+ const el9 = dom.createElement('span', 'lh-clump-toggletext--show');
797
+ const el10 = dom.createElement('span', 'lh-clump-toggletext--hide');
798
+ el8.append(' ', el9, ' ', el10, ' ');
799
+ el4.append(' ', el5, ' ', el8, ' ');
741
800
  el3.append(' ', el4, ' ');
742
801
  el2.append(' ', el3, ' ');
743
- el1.append(' ', el2, ' ');
802
+ el1.append(' ', ' ', el2, ' ');
744
803
  el0.append(el1);
745
804
  return el0;
746
805
  }
@@ -749,7 +808,7 @@ function createClumpComponent(dom) {
749
808
  * @param {DOM} dom
750
809
  */
751
810
  function createCrcComponent(dom) {
752
- const el0 = dom.document().createDocumentFragment();
811
+ const el0 = dom.createFragment();
753
812
  const el1 = dom.createElement('div', 'lh-crc-container');
754
813
  const el2 = dom.createElement('style');
755
814
  el2.append('\n .lh-crc .lh-tree-marker {\n width: 12px;\n height: 26px;\n display: block;\n float: left;\n background-position: top left;\n }\n .lh-crc .lh-horiz-down {\n background: url(\'data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><g fill="%23D8D8D8" fill-rule="evenodd"><path d="M16 12v2H-2v-2z"/><path d="M9 12v14H7V12z"/></g></svg>\');\n }\n .lh-crc .lh-right {\n background: url(\'data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M16 12v2H0v-2z" fill="%23D8D8D8" fill-rule="evenodd"/></svg>\');\n }\n .lh-crc .lh-up-right {\n background: url(\'data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M7 0h2v14H7zm2 12h7v2H9z" fill="%23D8D8D8" fill-rule="evenodd"/></svg>\');\n }\n .lh-crc .lh-vert-right {\n background: url(\'data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M7 0h2v27H7zm2 12h7v2H9z" fill="%23D8D8D8" fill-rule="evenodd"/></svg>\');\n }\n .lh-crc .lh-vert {\n background: url(\'data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M7 0h2v26H7z" fill="%23D8D8D8" fill-rule="evenodd"/></svg>\');\n }\n .lh-crc .lh-crc-tree {\n font-size: 14px;\n width: 100%;\n overflow-x: auto;\n }\n .lh-crc .lh-crc-node {\n height: 26px;\n line-height: 26px;\n white-space: nowrap;\n }\n .lh-crc .lh-crc-node__tree-value {\n margin-left: 10px;\n }\n .lh-crc .lh-crc-node__tree-value div {\n display: inline;\n }\n .lh-crc .lh-crc-node__chain-duration {\n font-weight: 700;\n }\n .lh-crc .lh-crc-initial-nav {\n color: #595959;\n font-style: italic;\n }\n .lh-crc__summary-value {\n margin-bottom: 10px;\n }\n ');
@@ -771,7 +830,7 @@ function createCrcComponent(dom) {
771
830
  * @param {DOM} dom
772
831
  */
773
832
  function createCrcChainComponent(dom) {
774
- const el0 = dom.document().createDocumentFragment();
833
+ const el0 = dom.createFragment();
775
834
  const el1 = dom.createElement('div', 'lh-crc-node');
776
835
  const el2 = dom.createElement('span', 'lh-crc-node__tree-marker');
777
836
  const el3 = dom.createElement('span', 'lh-crc-node__tree-value');
@@ -784,7 +843,7 @@ function createCrcChainComponent(dom) {
784
843
  * @param {DOM} dom
785
844
  */
786
845
  function createElementScreenshotComponent(dom) {
787
- const el0 = dom.document().createDocumentFragment();
846
+ const el0 = dom.createFragment();
788
847
  const el1 = dom.createElement('div', 'lh-element-screenshot');
789
848
  const el2 = dom.createElement('div', 'lh-element-screenshot__content');
790
849
  const el3 = dom.createElement('div', 'lh-element-screenshot__mask');
@@ -805,45 +864,28 @@ function createElementScreenshotComponent(dom) {
805
864
  return el0;
806
865
  }
807
866
 
808
- /**
809
- * @param {DOM} dom
810
- */
811
- function createEnvItemComponent(dom) {
812
- const el0 = dom.document().createDocumentFragment();
813
- const el1 = dom.createElement('li', 'lh-env__item');
814
- const el2 = dom.createElement('span', 'lh-env__name');
815
- const el3 = dom.createElement('span', 'lh-env__description');
816
- el1.append(' ', el2, ' ', el3, ' ');
817
- el0.append(el1);
818
- return el0;
819
- }
820
-
821
867
  /**
822
868
  * @param {DOM} dom
823
869
  */
824
870
  function createFooterComponent(dom) {
825
- const el0 = dom.document().createDocumentFragment();
871
+ const el0 = dom.createFragment();
826
872
  const el1 = dom.createElement('style');
827
- el1.append('\n .lh-footer {\n padding: var(--footer-padding-vertical) calc(var(--default-padding) * 2);\n max-width: var(--report-width);\n margin: 0 auto;\n }\n .lh-footer .lh-generated {\n text-align: center;\n }\n .lh-env__title {\n font-size: var(--env-item-font-size-big);\n line-height: var(--env-item-line-height-big);\n text-align: center;\n padding: var(--score-container-padding);\n }\n .lh-env {\n padding: var(--default-padding) 0;\n }\n .lh-env__items {\n padding-left: 16px;\n margin: 0 0 var(--audits-margin-bottom);\n padding: 0;\n }\n .lh-env__items .lh-env__item:nth-child(2n) {\n background-color: var(--env-item-background-color);\n }\n .lh-env__item {\n display: flex;\n padding: var(--env-item-padding);\n position: relative;\n }\n span.lh-env__name {\n font-weight: bold;\n min-width: var(--env-name-min-width);\n flex: 0.5;\n padding: 0 8px;\n }\n span.lh-env__description {\n text-align: left;\n flex: 1;\n }\n ');
873
+ el1.append('\n .lh-footer {\n padding: var(--footer-padding-vertical) calc(var(--default-padding) * 2);\n max-width: var(--report-content-width);\n margin: 0 auto;\n }\n .lh-footer .lh-generated {\n text-align: center;\n }\n ');
828
874
  el0.append(el1);
829
875
  const el2 = dom.createElement('footer', 'lh-footer');
830
- const el3 = dom.createElement('div', 'lh-env');
831
- const el4 = dom.createElement('div', 'lh-env__title');
832
- el4.append('Runtime Settings');
833
- const el5 = dom.createElement('ul', 'lh-env__items');
834
- el5.append(' ', ' ');
835
- el3.append(' ', el4, ' ', el5, ' ');
836
- const el6 = dom.createElement('div', 'lh-generated');
837
- const el7 = dom.createElement('b');
838
- el7.append('Lighthouse');
839
- const el8 = dom.createElement('span', 'lh-footer__version');
840
- const el9 = dom.createElement('a', 'lh-footer__version_issue');
841
- el9.setAttribute('href', 'https://github.com/GoogleChrome/Lighthouse/issues');
842
- el9.setAttribute('target', '_blank');
843
- el9.setAttribute('rel', 'noopener');
844
- el9.append('File an issue');
845
- el6.append(' ', ' Generated by ', el7, ' ', el8, ' | ', el9, ' ');
846
- el2.append(' ', ' ', el3, ' ', el6, ' ');
876
+ const el3 = dom.createElement('ul', 'lh-meta__items');
877
+ el3.append(' ');
878
+ const el4 = dom.createElement('div', 'lh-generated');
879
+ const el5 = dom.createElement('b');
880
+ el5.append('Lighthouse');
881
+ const el6 = dom.createElement('span', 'lh-footer__version');
882
+ const el7 = dom.createElement('a', 'lh-footer__version_issue');
883
+ el7.setAttribute('href', 'https://github.com/GoogleChrome/Lighthouse/issues');
884
+ el7.setAttribute('target', '_blank');
885
+ el7.setAttribute('rel', 'noopener');
886
+ el7.append('File an issue');
887
+ el4.append(' ', ' Generated by ', el5, ' ', el6, ' | ', el7, ' ');
888
+ el2.append(' ', el3, ' ', el4, ' ');
847
889
  el0.append(el2);
848
890
  return el0;
849
891
  }
@@ -852,9 +894,8 @@ function createFooterComponent(dom) {
852
894
  * @param {DOM} dom
853
895
  */
854
896
  function createFractionComponent(dom) {
855
- const el0 = dom.document().createDocumentFragment();
897
+ const el0 = dom.createFragment();
856
898
  const el1 = dom.createElement('a', 'lh-fraction__wrapper');
857
- el1.setAttribute('href', '#');
858
899
  const el2 = dom.createElement('div', 'lh-fraction__content-wrapper');
859
900
  const el3 = dom.createElement('div', 'lh-fraction__content');
860
901
  const el4 = dom.createElement('div', 'lh-fraction__background');
@@ -870,9 +911,8 @@ function createFractionComponent(dom) {
870
911
  * @param {DOM} dom
871
912
  */
872
913
  function createGaugeComponent(dom) {
873
- const el0 = dom.document().createDocumentFragment();
914
+ const el0 = dom.createFragment();
874
915
  const el1 = dom.createElement('a', 'lh-gauge__wrapper');
875
- el1.setAttribute('href', '#');
876
916
  const el2 = dom.createElement('div', 'lh-gauge__svg-wrapper');
877
917
  const el3 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg', 'lh-gauge');
878
918
  el3.setAttribute('viewBox', '0 0 120 120');
@@ -899,12 +939,11 @@ function createGaugeComponent(dom) {
899
939
  * @param {DOM} dom
900
940
  */
901
941
  function createGaugePwaComponent(dom) {
902
- const el0 = dom.document().createDocumentFragment();
942
+ const el0 = dom.createFragment();
903
943
  const el1 = dom.createElement('style');
904
944
  el1.append('\n .lh-gauge--pwa .lh-gauge--pwa__component {\n display: none;\n }\n .lh-gauge--pwa__wrapper:not(.lh-badged--all) .lh-gauge--pwa__logo > path {\n /* Gray logo unless everything is passing. */\n fill: #B0B0B0;\n }\n\n .lh-gauge--pwa__disc {\n fill: var(--color-gray-200);\n }\n\n .lh-gauge--pwa__logo--primary-color {\n fill: #304FFE;\n }\n\n .lh-gauge--pwa__logo--secondary-color {\n fill: #3D3D3D;\n }\n .lh-dark .lh-gauge--pwa__logo--secondary-color {\n fill: #D8B6B6;\n }\n\n /* No passing groups. */\n .lh-gauge--pwa__wrapper:not([class*=\'lh-badged--\']) .lh-gauge--pwa__na-line {\n display: inline;\n }\n /* Just optimized. Same n/a line as no passing groups. */\n .lh-gauge--pwa__wrapper.lh-badged--pwa-optimized:not(.lh-badged--pwa-installable) .lh-gauge--pwa__na-line {\n display: inline;\n }\n\n /* Just installable. */\n .lh-gauge--pwa__wrapper.lh-badged--pwa-installable .lh-gauge--pwa__installable-badge {\n display: inline;\n }\n\n /* All passing groups. */\n .lh-gauge--pwa__wrapper.lh-badged--all .lh-gauge--pwa__check-circle {\n display: inline;\n }\n ');
905
945
  el0.append(el1);
906
946
  const el2 = dom.createElement('a', 'lh-gauge__wrapper lh-gauge--pwa__wrapper');
907
- el2.setAttribute('href', '#');
908
947
  const el3 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg', 'lh-gauge lh-gauge--pwa');
909
948
  el3.setAttribute('viewBox', '0 0 60 60');
910
949
  const el4 = dom.createElementNS('http://www.w3.org/2000/svg', 'defs');
@@ -1000,7 +1039,7 @@ function createGaugePwaComponent(dom) {
1000
1039
  * @param {DOM} dom
1001
1040
  */
1002
1041
  function createHeadingComponent(dom) {
1003
- const el0 = dom.document().createDocumentFragment();
1042
+ const el0 = dom.createFragment();
1004
1043
  const el1 = dom.createElement('style');
1005
1044
  el1.append('\n /* CSS Fireworks. Originally by Eddie Lin\n https://codepen.io/paulirish/pen/yEVMbP\n */\n .lh-pyro {\n display: none;\n z-index: 1;\n pointer-events: none;\n }\n .lh-score100 .lh-pyro {\n display: block;\n }\n .lh-score100 .lh-lighthouse stop:first-child {\n stop-color: hsla(200, 12%, 95%, 0);\n }\n .lh-score100 .lh-lighthouse stop:last-child {\n stop-color: hsla(65, 81%, 76%, 1);\n }\n\n .lh-pyro > .lh-pyro-before, .lh-pyro > .lh-pyro-after {\n position: absolute;\n width: 5px;\n height: 5px;\n border-radius: 2.5px;\n box-shadow: 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff;\n animation: 1s bang ease-out infinite backwards, 1s gravity ease-in infinite backwards, 5s position linear infinite backwards;\n animation-delay: 1s, 1s, 1s;\n }\n\n .lh-pyro > .lh-pyro-after {\n animation-delay: 2.25s, 2.25s, 2.25s;\n animation-duration: 1.25s, 1.25s, 6.25s;\n }\n .lh-fireworks-paused .lh-pyro > div {\n animation-play-state: paused;\n }\n\n @keyframes bang {\n to {\n box-shadow: -70px -115.67px #47ebbc, -28px -99.67px #eb47a4, 58px -31.67px #7eeb47, 13px -141.67px #eb47c5, -19px 6.33px #7347eb, -2px -74.67px #ebd247, 24px -151.67px #eb47e0, 57px -138.67px #b4eb47, -51px -104.67px #479eeb, 62px 8.33px #ebcf47, -93px 0.33px #d547eb, -16px -118.67px #47bfeb, 53px -84.67px #47eb83, 66px -57.67px #eb47bf, -93px -65.67px #91eb47, 30px -13.67px #86eb47, -2px -59.67px #83eb47, -44px 1.33px #eb47eb, 61px -58.67px #47eb73, 5px -22.67px #47e8eb, -66px -28.67px #ebe247, 42px -123.67px #eb5547, -75px 26.33px #7beb47, 15px -52.67px #a147eb, 36px -51.67px #eb8347, -38px -12.67px #eb5547, -46px -59.67px #47eb81, 78px -114.67px #eb47ba, 15px -156.67px #eb47bf, -36px 1.33px #eb4783, -72px -86.67px #eba147, 31px -46.67px #ebe247, -68px 29.33px #47e2eb, -55px 19.33px #ebe047, -56px 27.33px #4776eb, -13px -91.67px #eb5547, -47px -138.67px #47ebc7, -18px -96.67px #eb47ac, 11px -88.67px #4783eb, -67px -28.67px #47baeb, 53px 10.33px #ba47eb, 11px 19.33px #5247eb, -5px -11.67px #eb4791, -68px -4.67px #47eba7, 95px -37.67px #eb478b, -67px -162.67px #eb5d47, -54px -120.67px #eb6847, 49px -12.67px #ebe047, 88px 8.33px #47ebda, 97px 33.33px #eb8147, 6px -71.67px #ebbc47;\n }\n }\n @keyframes gravity {\n to {\n transform: translateY(80px);\n opacity: 0;\n }\n }\n @keyframes position {\n 0%, 19.9% {\n margin-top: 4%;\n margin-left: 47%;\n }\n 20%, 39.9% {\n margin-top: 7%;\n margin-left: 30%;\n }\n 40%, 59.9% {\n margin-top: 6%;\n margin-left: 70%;\n }\n 60%, 79.9% {\n margin-top: 3%;\n margin-left: 20%;\n }\n 80%, 99.9% {\n margin-top: 3%;\n margin-left: 80%;\n }\n }\n ');
1006
1045
  el0.append(el1);
@@ -1015,7 +1054,7 @@ function createHeadingComponent(dom) {
1015
1054
  * @param {DOM} dom
1016
1055
  */
1017
1056
  function createMetricComponent(dom) {
1018
- const el0 = dom.document().createDocumentFragment();
1057
+ const el0 = dom.createFragment();
1019
1058
  const el1 = dom.createElement('div', 'lh-metric');
1020
1059
  const el2 = dom.createElement('div', 'lh-metric__innerwrap');
1021
1060
  const el3 = dom.createElement('div', 'lh-metric__icon');
@@ -1028,53 +1067,15 @@ function createMetricComponent(dom) {
1028
1067
  return el0;
1029
1068
  }
1030
1069
 
1031
- /**
1032
- * @param {DOM} dom
1033
- */
1034
- function createMetricsToggleComponent(dom) {
1035
- const el0 = dom.document().createDocumentFragment();
1036
- const el1 = dom.createElement('div', 'lh-metrics-toggle');
1037
- const el2 = dom.createElement('input', 'lh-metrics-toggle__input');
1038
- el2.setAttribute('type', 'checkbox');
1039
- el2.setAttribute('id', 'toggle-metric-descriptions');
1040
- el2.setAttribute('aria-label', 'Toggle the display of metric descriptions');
1041
- const el3 = dom.createElement('label', 'lh-metrics-toggle__label');
1042
- el3.setAttribute('for', 'toggle-metric-descriptions');
1043
- const el4 = dom.createElement('div', 'lh-metrics-toggle__icon lh-metrics-toggle__icon--less');
1044
- el4.setAttribute('aria-hidden', 'true');
1045
- const el5 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg');
1046
- el5.setAttribute('width', '24');
1047
- el5.setAttribute('height', '24');
1048
- el5.setAttribute('viewBox', '0 0 24 24');
1049
- const el6 = dom.createElementNS('http://www.w3.org/2000/svg', 'path', 'lh-metrics-toggle__lines');
1050
- el6.setAttribute('d', 'M4 9h16v2H4zm0 4h10v2H4z');
1051
- el5.append(' ', el6, ' ');
1052
- el4.append(' ', el5, ' ');
1053
- const el7 = dom.createElement('div', 'lh-metrics-toggle__icon lh-metrics-toggle__icon--more');
1054
- el7.setAttribute('aria-hidden', 'true');
1055
- const el8 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg');
1056
- el8.setAttribute('width', '24');
1057
- el8.setAttribute('height', '24');
1058
- el8.setAttribute('viewBox', '0 0 24 24');
1059
- const el9 = dom.createElementNS('http://www.w3.org/2000/svg', 'path', 'lh-metrics-toggle__lines');
1060
- el9.setAttribute('d', 'M3 18h12v-2H3v2zM3 6v2h18V6H3zm0 7h18v-2H3v2z');
1061
- el8.append(' ', el9, ' ');
1062
- el7.append(' ', el8, ' ');
1063
- el3.append(' ', el4, ' ', el7, ' ');
1064
- el1.append(' ', el2, ' ', el3, ' ');
1065
- el0.append(el1);
1066
- return el0;
1067
- }
1068
-
1069
1070
  /**
1070
1071
  * @param {DOM} dom
1071
1072
  */
1072
1073
  function createOpportunityComponent(dom) {
1073
- const el0 = dom.document().createDocumentFragment();
1074
+ const el0 = dom.createFragment();
1074
1075
  const el1 = dom.createElement('div', 'lh-audit lh-audit--load-opportunity');
1075
1076
  const el2 = dom.createElement('details', 'lh-expandable-details');
1076
1077
  const el3 = dom.createElement('summary');
1077
- const el4 = dom.createElement('div', 'lh-audit__header lh-expandable-details__summary');
1078
+ const el4 = dom.createElement('div', 'lh-audit__header');
1078
1079
  const el5 = dom.createElement('div', 'lh-load-opportunity__cols');
1079
1080
  const el6 = dom.createElement('div', 'lh-load-opportunity__col lh-load-opportunity__col--one');
1080
1081
  const el7 = dom.createElement('span', 'lh-audit__score-icon');
@@ -1104,7 +1105,7 @@ function createOpportunityComponent(dom) {
1104
1105
  * @param {DOM} dom
1105
1106
  */
1106
1107
  function createOpportunityHeaderComponent(dom) {
1107
- const el0 = dom.document().createDocumentFragment();
1108
+ const el0 = dom.createFragment();
1108
1109
  const el1 = dom.createElement('div', 'lh-load-opportunity__header lh-load-opportunity__cols');
1109
1110
  const el2 = dom.createElement('div', 'lh-load-opportunity__col lh-load-opportunity__col--one');
1110
1111
  const el3 = dom.createElement('div', 'lh-load-opportunity__col lh-load-opportunity__col--two');
@@ -1117,7 +1118,7 @@ function createOpportunityHeaderComponent(dom) {
1117
1118
  * @param {DOM} dom
1118
1119
  */
1119
1120
  function createScorescaleComponent(dom) {
1120
- const el0 = dom.document().createDocumentFragment();
1121
+ const el0 = dom.createFragment();
1121
1122
  const el1 = dom.createElement('div', 'lh-scorescale');
1122
1123
  const el2 = dom.createElement('span', 'lh-scorescale-range lh-scorescale-range--fail');
1123
1124
  el2.append('0–49');
@@ -1134,9 +1135,9 @@ function createScorescaleComponent(dom) {
1134
1135
  * @param {DOM} dom
1135
1136
  */
1136
1137
  function createScoresWrapperComponent(dom) {
1137
- const el0 = dom.document().createDocumentFragment();
1138
+ const el0 = dom.createFragment();
1138
1139
  const el1 = dom.createElement('style');
1139
- el1.append('\n .lh-scores-container {\n display: flex;\n flex-direction: column;\n padding: var(--scores-container-padding);\n position: relative;\n width: 100%;\n }\n\n .lh-sticky-header {\n --gauge-circle-size: 36px;\n --plugin-badge-size: 18px;\n --plugin-icon-size: 75%;\n --gauge-wrapper-width: 60px;\n --gauge-percentage-font-size: 13px;\n position: fixed;\n left: 0;\n right: 0;\n top: var(--topbar-height);\n font-weight: 700;\n display: none;\n justify-content: center;\n background-color: var(--sticky-header-background-color);\n border-bottom: 1px solid var(--color-gray-200);\n padding-top: var(--score-container-padding);\n padding-bottom: 4px;\n z-index: 1;\n pointer-events: none;\n }\n\n .lh-devtools .lh-sticky-header {\n /* The report within DevTools is placed in a container with overflow, which changes the placement of this header unless we change `position` to `sticky.` */\n position: sticky;\n }\n\n .lh-sticky-header--visible {\n display: grid;\n grid-auto-flow: column;\n pointer-events: auto;\n }\n\n /* Disable the gauge arc animation for the sticky header, so toggling display: none\n does not play the animation. */\n .lh-sticky-header .lh-gauge-arc {\n animation: none;\n }\n\n .lh-sticky-header .lh-gauge__label {\n display: none;\n }\n\n .lh-highlighter {\n width: var(--gauge-wrapper-width);\n height: 1px;\n background-color: var(--highlighter-background-color);\n /* Position at bottom of first gauge in sticky header. */\n position: absolute;\n grid-column: 1;\n bottom: -1px;\n }\n\n .lh-gauge__wrapper:first-of-type {\n contain: none;\n }\n ');
1140
+ el1.append('\n .lh-scores-container {\n display: flex;\n flex-direction: column;\n padding: var(--default-padding) 0;\n position: relative;\n width: 100%;\n }\n\n .lh-sticky-header {\n --gauge-circle-size: var(--gauge-circle-size-sm);\n --plugin-badge-size: 16px;\n --plugin-icon-size: 75%;\n --gauge-wrapper-width: 60px;\n --gauge-percentage-font-size: 13px;\n position: fixed;\n left: 0;\n right: 0;\n top: var(--topbar-height);\n font-weight: 500;\n display: none;\n justify-content: center;\n background-color: var(--sticky-header-background-color);\n border-bottom: 1px solid var(--color-gray-200);\n padding-top: var(--score-container-padding);\n padding-bottom: 4px;\n z-index: 1;\n pointer-events: none;\n }\n\n .lh-devtools .lh-sticky-header {\n /* The report within DevTools is placed in a container with overflow, which changes the placement of this header unless we change `position` to `sticky.` */\n position: sticky;\n }\n\n .lh-sticky-header--visible {\n display: grid;\n grid-auto-flow: column;\n pointer-events: auto;\n }\n\n /* Disable the gauge arc animation for the sticky header, so toggling display: none\n does not play the animation. */\n .lh-sticky-header .lh-gauge-arc {\n animation: none;\n }\n\n .lh-sticky-header .lh-gauge__label {\n display: none;\n }\n\n .lh-highlighter {\n width: var(--gauge-wrapper-width);\n height: 1px;\n background-color: var(--highlighter-background-color);\n /* Position at bottom of first gauge in sticky header. */\n position: absolute;\n grid-column: 1;\n bottom: -1px;\n }\n\n .lh-gauge__wrapper:first-of-type {\n contain: none;\n }\n ');
1140
1141
  el0.append(el1);
1141
1142
  const el2 = dom.createElement('div', 'lh-scores-wrapper');
1142
1143
  const el3 = dom.createElement('div', 'lh-scores-container');
@@ -1154,7 +1155,7 @@ function createScoresWrapperComponent(dom) {
1154
1155
  * @param {DOM} dom
1155
1156
  */
1156
1157
  function createSnippetComponent(dom) {
1157
- const el0 = dom.document().createDocumentFragment();
1158
+ const el0 = dom.createFragment();
1158
1159
  const el1 = dom.createElement('div', 'lh-snippet');
1159
1160
  const el2 = dom.createElement('style');
1160
1161
  el2.append('\n :root {\n --snippet-highlight-light: #fbf1f2;\n --snippet-highlight-dark: #ffd6d8;\n }\n\n .lh-snippet__header {\n position: relative;\n overflow: hidden;\n padding: 10px;\n border-bottom: none;\n color: var(--snippet-color);\n background-color: var(--snippet-background-color);\n border: 1px solid var(--report-border-color-secondary);\n }\n .lh-snippet__title {\n font-weight: bold;\n float: left;\n }\n .lh-snippet__node {\n float: left;\n margin-left: 4px;\n }\n .lh-snippet__toggle-expand {\n padding: 1px 7px;\n margin-top: -1px;\n margin-right: -7px;\n float: right;\n background: transparent;\n border: none;\n cursor: pointer;\n font-size: 14px;\n color: #0c50c7;\n }\n\n .lh-snippet__snippet {\n overflow: auto;\n border: 1px solid var(--report-border-color-secondary);\n }\n /* Container needed so that all children grow to the width of the scroll container */\n .lh-snippet__snippet-inner {\n display: inline-block;\n min-width: 100%;\n }\n\n .lh-snippet:not(.lh-snippet--expanded) .lh-snippet__show-if-expanded {\n display: none;\n }\n .lh-snippet.lh-snippet--expanded .lh-snippet__show-if-collapsed {\n display: none;\n }\n\n .lh-snippet__line {\n background: white;\n white-space: pre;\n display: flex;\n }\n .lh-snippet__line:not(.lh-snippet__line--message):first-child {\n padding-top: 4px;\n }\n .lh-snippet__line:not(.lh-snippet__line--message):last-child {\n padding-bottom: 4px;\n }\n .lh-snippet__line--content-highlighted {\n background: var(--snippet-highlight-dark);\n }\n .lh-snippet__line--message {\n background: var(--snippet-highlight-light);\n }\n .lh-snippet__line--message .lh-snippet__line-number {\n padding-top: 10px;\n padding-bottom: 10px;\n }\n .lh-snippet__line--message code {\n padding: 10px;\n padding-left: 5px;\n color: var(--color-fail);\n font-family: var(--report-font-family);\n }\n .lh-snippet__line--message code {\n white-space: normal;\n }\n .lh-snippet__line-icon {\n padding-top: 10px;\n display: none;\n }\n .lh-snippet__line--message .lh-snippet__line-icon {\n display: block;\n }\n .lh-snippet__line-icon:before {\n content: "";\n display: inline-block;\n vertical-align: middle;\n margin-right: 4px;\n width: var(--score-icon-size);\n height: var(--score-icon-size);\n background-image: var(--fail-icon-url);\n }\n .lh-snippet__line-number {\n flex-shrink: 0;\n width: 40px;\n text-align: right;\n font-family: monospace;\n padding-right: 5px;\n margin-right: 5px;\n color: var(--color-gray-600);\n user-select: none;\n }\n ');
@@ -1167,7 +1168,7 @@ function createSnippetComponent(dom) {
1167
1168
  * @param {DOM} dom
1168
1169
  */
1169
1170
  function createSnippetContentComponent(dom) {
1170
- const el0 = dom.document().createDocumentFragment();
1171
+ const el0 = dom.createFragment();
1171
1172
  const el1 = dom.createElement('div', 'lh-snippet__snippet');
1172
1173
  const el2 = dom.createElement('div', 'lh-snippet__snippet-inner');
1173
1174
  el1.append(' ', el2, ' ');
@@ -1179,7 +1180,7 @@ function createSnippetContentComponent(dom) {
1179
1180
  * @param {DOM} dom
1180
1181
  */
1181
1182
  function createSnippetHeaderComponent(dom) {
1182
- const el0 = dom.document().createDocumentFragment();
1183
+ const el0 = dom.createFragment();
1183
1184
  const el1 = dom.createElement('div', 'lh-snippet__header');
1184
1185
  const el2 = dom.createElement('div', 'lh-snippet__title');
1185
1186
  const el3 = dom.createElement('div', 'lh-snippet__node');
@@ -1196,7 +1197,7 @@ function createSnippetHeaderComponent(dom) {
1196
1197
  * @param {DOM} dom
1197
1198
  */
1198
1199
  function createSnippetLineComponent(dom) {
1199
- const el0 = dom.document().createDocumentFragment();
1200
+ const el0 = dom.createFragment();
1200
1201
  const el1 = dom.createElement('div', 'lh-snippet__line');
1201
1202
  const el2 = dom.createElement('div', 'lh-snippet__line-number');
1202
1203
  const el3 = dom.createElement('div', 'lh-snippet__line-icon');
@@ -1206,13 +1207,24 @@ function createSnippetLineComponent(dom) {
1206
1207
  return el0;
1207
1208
  }
1208
1209
 
1210
+ /**
1211
+ * @param {DOM} dom
1212
+ */
1213
+ function createStylesComponent(dom) {
1214
+ const el0 = dom.createFragment();
1215
+ const el1 = dom.createElement('style');
1216
+ el1.append('/**\n * @license\n * Copyright 2017 The Lighthouse Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the "License");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an "AS-IS" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n Naming convention:\n\n If a variable is used for a specific component: --{component}-{property name}-{modifier}\n\n Both {component} and {property name} should be kebab-case. If the target is the entire page,\n use \'report\' for the component. The property name should not be abbreviated. Use the\n property name the variable is intended for - if it\'s used for multiple, a common descriptor\n is fine (ex: \'size\' for a variable applied to \'width\' and \'height\'). If a variable is shared\n across multiple components, either create more variables or just drop the "{component}-"\n part of the name. Append any modifiers at the end (ex: \'big\', \'dark\').\n\n For colors: --color-{hue}-{intensity}\n\n {intensity} is the Material Design tag - 700, A700, etc.\n*/\n.lh-vars {\n /* Palette using Material Design Colors\n * https://www.materialui.co/colors */\n --color-amber-50: #FFF8E1;\n --color-blue-200: #90CAF9;\n --color-blue-900: #0D47A1;\n --color-blue-A700: #2962FF;\n --color-blue-primary: #06f;\n --color-cyan-500: #00BCD4;\n --color-gray-100: #F5F5F5;\n --color-gray-300: #CFCFCF;\n --color-gray-200: #E0E0E0;\n --color-gray-400: #BDBDBD;\n --color-gray-50: #FAFAFA;\n --color-gray-500: #9E9E9E;\n --color-gray-600: #757575;\n --color-gray-700: #616161;\n --color-gray-800: #424242;\n --color-gray-900: #212121;\n --color-gray: #000000;\n --color-green-700: #080;\n --color-green: #0c6;\n --color-lime-400: #D3E156;\n --color-orange-50: #FFF3E0;\n --color-orange-700: #C33300;\n --color-orange: #fa3;\n --color-red-700: #c00;\n --color-red: #f33;\n --color-teal-600: #00897B;\n --color-white: #FFFFFF;\n\n /* Context-specific colors */\n --color-average-secondary: var(--color-orange-700);\n --color-average: var(--color-orange);\n --color-fail-secondary: var(--color-red-700);\n --color-fail: var(--color-red);\n --color-hover: var(--color-gray-50);\n --color-informative: var(--color-blue-900);\n --color-pass-secondary: var(--color-green-700);\n --color-pass: var(--color-green);\n --color-not-applicable: var(--color-gray-600);\n\n /* Component variables */\n --audit-description-padding-left: calc(var(--score-icon-size) + var(--score-icon-margin-left) + var(--score-icon-margin-right));\n --audit-explanation-line-height: 16px;\n --audit-group-margin-bottom: calc(var(--default-padding) * 6);\n --audit-group-padding-vertical: 8px;\n --audit-margin-horizontal: 5px;\n --audit-padding-vertical: 8px;\n --category-padding: calc(var(--default-padding) * 6) calc(var(--default-padding) * 4) calc(var(--default-padding) * 4);\n --chevron-line-stroke: var(--color-gray-600);\n --chevron-size: 12px;\n --default-padding: 8px;\n --env-item-background-color: var(--color-gray-100);\n --env-item-font-size: 28px;\n --env-item-line-height: 36px;\n --env-item-padding: 10px 0px;\n --env-name-min-width: 220px;\n --footer-padding-vertical: 16px;\n --gauge-circle-size-big: 96px;\n --gauge-circle-size: 48px;\n --gauge-circle-size-sm: 32px;\n --gauge-label-font-size-big: 18px;\n --gauge-label-font-size: var(--report-font-size-secondary);\n --gauge-label-line-height-big: 24px;\n --gauge-label-line-height: var(--report-line-height-secondary);\n --gauge-percentage-font-size-big: 38px;\n --gauge-percentage-font-size: var(--report-font-size-secondary);\n --gauge-wrapper-width: 120px;\n --header-line-height: 24px;\n --highlighter-background-color: var(--report-text-color);\n --icon-square-size: calc(var(--score-icon-size) * 0.88);\n --image-preview-size: 48px;\n --link-color: var(--color-blue-primary);\n --locale-selector-background-color: var(--color-white);\n --metric-toggle-lines-fill: #7F7F7F;\n --metric-value-font-size: calc(var(--report-font-size) * 1.8);\n --metrics-toggle-background-color: var(--color-gray-200);\n --plugin-badge-background-color: var(--color-white);\n --plugin-badge-size-big: calc(var(--gauge-circle-size-big) / 2.7);\n --plugin-badge-size: calc(var(--gauge-circle-size) / 2.7);\n --plugin-icon-size: 65%;\n --pwa-icon-margin: 0 var(--default-padding);\n --pwa-icon-size: var(--topbar-logo-size);\n --report-background-color: #fff;\n --report-border-color-secondary: #ebebeb;\n --report-font-family-monospace: \'Roboto Mono\', \'Menlo\', \'dejavu sans mono\', \'Consolas\', \'Lucida Console\', monospace;\n --report-font-family: Roboto, Helvetica, Arial, sans-serif;\n --report-font-size: 14px;\n --report-font-size-secondary: 12px;\n --report-icon-size: var(--score-icon-background-size);\n --report-line-height: 24px;\n --report-line-height-secondary: 20px;\n --report-min-width: 360px;\n --report-monospace-font-size: calc(var(--report-font-size) * 0.85);\n --report-text-color-secondary: var(--color-gray-800);\n --report-text-color: var(--color-gray-900);\n --report-content-width: calc(60 * var(--report-font-size)); /* defaults to 840px */\n --score-container-padding: 8px;\n --score-icon-background-size: 24px;\n --score-icon-margin-left: 6px;\n --score-icon-margin-right: 14px;\n --score-icon-margin: 0 var(--score-icon-margin-right) 0 var(--score-icon-margin-left);\n --score-icon-size: 12px;\n --score-icon-size-big: 16px;\n --screenshot-overlay-background: rgba(0, 0, 0, 0.3);\n --section-padding-vertical: calc(var(--default-padding) * 6);\n --snippet-background-color: var(--color-gray-50);\n --snippet-color: #0938C2;\n --sparkline-height: 5px;\n --stackpack-padding-horizontal: 10px;\n --sticky-header-background-color: var(--report-background-color);\n --table-higlight-background-color: hsla(210, 17%, 77%, 0.1);\n --tools-icon-color: var(--color-gray-600);\n --topbar-background-color: var(--color-white);\n --topbar-height: 32px;\n --topbar-logo-size: 24px;\n --topbar-padding: 0 8px;\n --toplevel-warning-background-color: hsla(30, 100%, 75%, 10%);\n --toplevel-warning-message-text-color: var(--color-average-secondary);\n --toplevel-warning-padding: 18px;\n --toplevel-warning-text-color: var(--report-text-color);\n\n /* SVGs */\n --plugin-icon-url-dark: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="%23FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>\');\n --plugin-icon-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="%23757575"><path d="M0 0h24v24H0z" fill="none"/><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>\');\n\n --pass-icon-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><title>check</title><path fill="%23178239" d="M24 4C12.95 4 4 12.95 4 24c0 11.04 8.95 20 20 20 11.04 0 20-8.96 20-20 0-11.05-8.96-20-20-20zm-4 30L10 24l2.83-2.83L20 28.34l15.17-15.17L38 16 20 34z"/></svg>\');\n --average-icon-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><title>info</title><path fill="%23E67700" d="M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm2 30h-4V22h4v12zm0-16h-4v-4h4v4z"/></svg>\');\n --fail-icon-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><title>warn</title><path fill="%23C7221F" d="M2 42h44L24 4 2 42zm24-6h-4v-4h4v4zm0-8h-4v-8h4v8z"/></svg>\');\n\n --pwa-installable-gray-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="nonzero"><circle fill="%23DAE0E3" cx="12" cy="12" r="12"/><path d="M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm3.5 7.7h-2.8v2.8h-1.4v-2.8H8.5v-1.4h2.8V8.5h1.4v2.8h2.8v1.4z" fill="%23FFF"/></g></svg>\');\n --pwa-optimized-gray-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="%23DAE0E3" width="24" height="24" rx="12"/><path fill="%23FFF" d="M12 15.07l3.6 2.18-.95-4.1 3.18-2.76-4.2-.36L12 6.17l-1.64 3.86-4.2.36 3.2 2.76-.96 4.1z"/><path d="M5 5h14v14H5z"/></g></svg>\');\n\n --pwa-installable-gray-url-dark: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="nonzero"><circle fill="%23424242" cx="12" cy="12" r="12"/><path d="M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm3.5 7.7h-2.8v2.8h-1.4v-2.8H8.5v-1.4h2.8V8.5h1.4v2.8h2.8v1.4z" fill="%23FFF"/></g></svg>\');\n --pwa-optimized-gray-url-dark: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="%23424242" width="24" height="24" rx="12"/><path fill="%23FFF" d="M12 15.07l3.6 2.18-.95-4.1 3.18-2.76-4.2-.36L12 6.17l-1.64 3.86-4.2.36 3.2 2.76-.96 4.1z"/><path d="M5 5h14v14H5z"/></g></svg>\');\n\n --pwa-installable-color-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><circle fill="%230CCE6B" cx="12" cy="12" r="12"/><path d="M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm3.5 7.7h-2.8v2.8h-1.4v-2.8H8.5v-1.4h2.8V8.5h1.4v2.8h2.8v1.4z" fill="%23FFF"/></g></svg>\');\n --pwa-optimized-color-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="%230CCE6B" width="24" height="24" rx="12"/><path d="M5 5h14v14H5z"/><path fill="%23FFF" d="M12 15.07l3.6 2.18-.95-4.1 3.18-2.76-4.2-.36L12 6.17l-1.64 3.86-4.2.36 3.2 2.76-.96 4.1z"/></g></svg>\');\n\n --swap-locale-icon-url: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"/></svg>\');\n}\n\n@media not print {\n .lh-dark {\n /* Pallete */\n --color-gray-200: var(--color-gray-800);\n --color-gray-300: #616161;\n --color-gray-400: var(--color-gray-600);\n --color-gray-700: var(--color-gray-400);\n --color-gray-50: #757575;\n --color-gray-600: var(--color-gray-500);\n --color-green-700: var(--color-green);\n --color-orange-700: var(--color-orange);\n --color-red-700: var(--color-red);\n --color-teal-600: var(--color-cyan-500);\n\n /* Context-specific colors */\n --color-hover: rgba(0, 0, 0, 0.2);\n --color-informative: var(--color-blue-200);\n\n /* Component variables */\n --env-item-background-color: #393535;\n --link-color: var(--color-blue-200);\n --locale-selector-background-color: var(--color-gray-200);\n --plugin-badge-background-color: var(--color-gray-800);\n --report-background-color: var(--color-gray-900);\n --report-border-color-secondary: var(--color-gray-200);\n --report-text-color-secondary: var(--color-gray-400);\n --report-text-color: var(--color-gray-100);\n --snippet-color: var(--color-cyan-500);\n --topbar-background-color: var(--color-gray);\n --toplevel-warning-background-color: hsl(33deg 14% 18%);\n --toplevel-warning-message-text-color: var(--color-orange-700);\n --toplevel-warning-text-color: var(--color-gray-100);\n\n /* SVGs */\n --plugin-icon-url: var(--plugin-icon-url-dark);\n --pwa-installable-gray-url: var(--pwa-installable-gray-url-dark);\n --pwa-optimized-gray-url: var(--pwa-optimized-gray-url-dark);\n }\n}\n\n@media only screen and (max-width: 480px) {\n .lh-vars {\n --audit-group-margin-bottom: 20px;\n --category-padding: 12px;\n --env-name-min-width: 120px;\n --gauge-circle-size-big: 96px;\n --gauge-circle-size: 72px;\n --gauge-label-font-size-big: 22px;\n --gauge-label-font-size: 14px;\n --gauge-label-line-height-big: 26px;\n --gauge-label-line-height: 20px;\n --gauge-percentage-font-size-big: 34px;\n --gauge-percentage-font-size: 26px;\n --gauge-wrapper-width: 112px;\n --header-padding: 16px 0 16px 0;\n --image-preview-size: 24px;\n --plugin-icon-size: 75%;\n --pwa-icon-margin: 0 7px 0 -3px;\n --report-font-size: 14px;\n --report-line-height: 20px;\n --score-icon-margin-left: 2px;\n --score-icon-size: 10px;\n --topbar-height: 28px;\n --topbar-logo-size: 20px;\n }\n\n /* Not enough space to adequately show the relative savings bars. */\n .lh-sparkline {\n display: none;\n }\n}\n\n.lh-vars.lh-devtools {\n --audit-explanation-line-height: 14px;\n --audit-group-margin-bottom: 20px;\n --audit-group-padding-vertical: 12px;\n --audit-padding-vertical: 4px;\n --category-padding: 12px;\n --default-padding: 12px;\n --env-name-min-width: 120px;\n --footer-padding-vertical: 8px;\n --gauge-circle-size-big: 72px;\n --gauge-circle-size: 64px;\n --gauge-label-font-size-big: 22px;\n --gauge-label-font-size: 14px;\n --gauge-label-line-height-big: 26px;\n --gauge-label-line-height: 20px;\n --gauge-percentage-font-size-big: 34px;\n --gauge-percentage-font-size: 26px;\n --gauge-wrapper-width: 97px;\n --header-line-height: 20px;\n --header-padding: 16px 0 16px 0;\n --screenshot-overlay-background: transparent;\n --plugin-icon-size: 75%;\n --pwa-icon-margin: 0 7px 0 -3px;\n --report-font-family-monospace: \'Menlo\', \'dejavu sans mono\', \'Consolas\', \'Lucida Console\', monospace;\n --report-font-family: \'.SFNSDisplay-Regular\', \'Helvetica Neue\', \'Lucida Grande\', sans-serif;\n --report-font-size: 12px;\n --report-line-height: 20px;\n --score-icon-margin-left: 2px;\n --score-icon-size: 10px;\n --section-padding-vertical: 8px;\n}\n\n.lh-devtools.lh-root {\n height: 100%;\n}\n.lh-devtools.lh-root img {\n /* Override devtools default \'min-width: 0\' so svg without size in a flexbox isn\'t collapsed. */\n min-width: auto;\n}\n.lh-devtools .lh-container {\n overflow-y: scroll;\n height: calc(100% - var(--topbar-height));\n}\n@media print {\n .lh-devtools .lh-container {\n overflow: unset;\n }\n}\n.lh-devtools .lh-sticky-header {\n /* This is normally the height of the topbar, but we want it to stick to the top of our scroll container .lh-container` */\n top: 0;\n}\n\n@keyframes fadeIn {\n 0% { opacity: 0;}\n 100% { opacity: 0.6;}\n}\n\n.lh-root *, .lh-root *::before, .lh-root *::after {\n box-sizing: border-box;\n}\n\n.lh-root {\n font-family: var(--report-font-family);\n font-size: var(--report-font-size);\n margin: 0;\n line-height: var(--report-line-height);\n background: var(--report-background-color);\n color: var(--report-text-color);\n}\n\n.lh-root :focus {\n outline: -webkit-focus-ring-color auto 3px;\n}\n.lh-root summary:focus {\n outline: none;\n box-shadow: 0 0 0 1px hsl(217, 89%, 61%);\n}\n\n.lh-root [hidden] {\n display: none !important;\n}\n\n.lh-root pre {\n margin: 0;\n}\n\n.lh-root details > summary {\n cursor: pointer;\n}\n\n.lh-hidden {\n display: none !important;\n}\n\n.lh-container {\n /*\n Text wrapping in the report is so much FUN!\n We have a `word-break: break-word;` globally here to prevent a few common scenarios, namely\n long non-breakable text (usually URLs) found in:\n 1. The footer\n 2. .lh-node (outerHTML)\n 3. .lh-code\n\n With that sorted, the next challenge is appropriate column sizing and text wrapping inside our\n .lh-details tables. Even more fun.\n * We don\'t want table headers ("Potential Savings (ms)") to wrap or their column values, but\n we\'d be happy for the URL column to wrap if the URLs are particularly long.\n * We want the narrow columns to remain narrow, providing the most column width for URL\n * We don\'t want the table to extend past 100% width.\n * Long URLs in the URL column can wrap. Util.getURLDisplayName maxes them out at 64 characters,\n but they do not get any overflow:ellipsis treatment.\n */\n word-break: break-word;\n}\n\n.lh-audit-group a,\n.lh-category-header__description a,\n.lh-audit__description a,\n.lh-warnings a,\n.lh-footer a,\n.lh-table-column--link a {\n color: var(--link-color);\n}\n\n.lh-audit__description, .lh-audit__stackpack {\n --inner-audit-padding-right: var(--stackpack-padding-horizontal);\n padding-left: var(--audit-description-padding-left);\n padding-right: var(--inner-audit-padding-right);\n padding-top: 8px;\n padding-bottom: 8px;\n}\n\n.lh-details {\n margin-top: var(--default-padding);\n margin-bottom: var(--default-padding);\n margin-left: var(--audit-description-padding-left);\n /* whatever the .lh-details side margins are */\n width: 100%;\n}\n\n.lh-audit__stackpack {\n display: flex;\n align-items: center;\n}\n\n.lh-audit__stackpack__img {\n max-width: 30px;\n margin-right: var(--default-padding)\n}\n\n/* Report header */\n\n.lh-report-icon {\n display: flex;\n align-items: center;\n padding: 10px 12px;\n cursor: pointer;\n}\n.lh-report-icon[disabled] {\n opacity: 0.3;\n pointer-events: none;\n}\n\n.lh-report-icon::before {\n content: "";\n margin: 4px;\n background-repeat: no-repeat;\n width: var(--report-icon-size);\n height: var(--report-icon-size);\n opacity: 0.7;\n display: inline-block;\n vertical-align: middle;\n}\n.lh-report-icon:hover::before {\n opacity: 1;\n}\n.lh-dark .lh-report-icon::before {\n filter: invert(1);\n}\n.lh-report-icon--print::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm-1-9H6v4h12V3z"/><path fill="none" d="M0 0h24v24H0z"/></svg>\');\n}\n.lh-report-icon--copy::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>\');\n}\n.lh-report-icon--open::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h4v-2H5V8h14v10h-4v2h4c1.1 0 2-.9 2-2V6c0-1.1-.89-2-2-2zm-7 6l-4 4h3v6h2v-6h3l-4-4z"/></svg>\');\n}\n.lh-report-icon--download::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>\');\n}\n.lh-report-icon--dark::before {\n background-image:url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 100 125"><path d="M50 23.587c-16.27 0-22.799 12.574-22.799 21.417 0 12.917 10.117 22.451 12.436 32.471h20.726c2.32-10.02 12.436-19.554 12.436-32.471 0-8.843-6.528-21.417-22.799-21.417zM39.637 87.161c0 3.001 1.18 4.181 4.181 4.181h.426l.41 1.231C45.278 94.449 46.042 95 48.019 95h3.963c1.978 0 2.74-.551 3.365-2.427l.409-1.231h.427c3.002 0 4.18-1.18 4.18-4.181V80.91H39.637v6.251zM50 18.265c1.26 0 2.072-.814 2.072-2.073v-9.12C52.072 5.813 51.26 5 50 5c-1.259 0-2.072.813-2.072 2.073v9.12c0 1.259.813 2.072 2.072 2.072zM68.313 23.727c.994.774 2.135.634 2.91-.357l5.614-7.187c.776-.992.636-2.135-.356-2.909-.992-.776-2.135-.636-2.91.357l-5.613 7.186c-.778.993-.636 2.135.355 2.91zM91.157 36.373c-.306-1.222-1.291-1.815-2.513-1.51l-8.85 2.207c-1.222.305-1.814 1.29-1.51 2.512.305 1.223 1.291 1.814 2.513 1.51l8.849-2.206c1.223-.305 1.816-1.291 1.511-2.513zM86.757 60.48l-8.331-3.709c-1.15-.512-2.225-.099-2.736 1.052-.512 1.151-.1 2.224 1.051 2.737l8.33 3.707c1.15.514 2.225.101 2.736-1.05.513-1.149.1-2.223-1.05-2.737zM28.779 23.37c.775.992 1.917 1.131 2.909.357.992-.776 1.132-1.917.357-2.91l-5.615-7.186c-.775-.992-1.917-1.132-2.909-.357s-1.131 1.917-.356 2.909l5.614 7.187zM21.715 39.583c.305-1.223-.288-2.208-1.51-2.513l-8.849-2.207c-1.222-.303-2.208.289-2.513 1.511-.303 1.222.288 2.207 1.511 2.512l8.848 2.206c1.222.304 2.208-.287 2.513-1.509zM21.575 56.771l-8.331 3.711c-1.151.511-1.563 1.586-1.05 2.735.511 1.151 1.586 1.563 2.736 1.052l8.331-3.711c1.151-.511 1.563-1.586 1.05-2.735-.512-1.15-1.585-1.562-2.736-1.052z"/></svg>\');\n}\n.lh-report-icon--treemap::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="black"><path d="M3 5v14h19V5H3zm2 2h15v4H5V7zm0 10v-4h4v4H5zm6 0v-4h9v4h-9z"/></svg>\');\n}\n.lh-report-icon--date::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 11h2v2H7v-2zm14-5v14a2 2 0 01-2 2H5a2 2 0 01-2-2V6c0-1.1.9-2 2-2h1V2h2v2h8V2h2v2h1a2 2 0 012 2zM5 8h14V6H5v2zm14 12V10H5v10h14zm-4-7h2v-2h-2v2zm-4 0h2v-2h-2v2z"/></svg>\');\n}\n.lh-report-icon--devices::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 6h18V4H4a2 2 0 00-2 2v11H0v3h14v-3H4V6zm19 2h-6a1 1 0 00-1 1v10c0 .6.5 1 1 1h6c.6 0 1-.5 1-1V9c0-.6-.5-1-1-1zm-1 9h-4v-7h4v7z"/></svg>\');\n}\n.lh-report-icon--world::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm7 6h-3c-.3-1.3-.8-2.5-1.4-3.6A8 8 0 0 1 18.9 8zm-7-4a14 14 0 0 1 2 4h-4a14 14 0 0 1 2-4zM4.3 14a8.2 8.2 0 0 1 0-4h3.3a16.5 16.5 0 0 0 0 4H4.3zm.8 2h3a14 14 0 0 0 1.3 3.6A8 8 0 0 1 5.1 16zm3-8H5a8 8 0 0 1 4.3-3.6L8 8zM12 20a14 14 0 0 1-2-4h4a14 14 0 0 1-2 4zm2.3-6H9.7a14.7 14.7 0 0 1 0-4h4.6a14.6 14.6 0 0 1 0 4zm.3 5.6c.6-1.2 1-2.4 1.4-3.6h3a8 8 0 0 1-4.4 3.6zm1.8-5.6a16.5 16.5 0 0 0 0-4h3.3a8.2 8.2 0 0 1 0 4h-3.3z"/></svg>\');\n}\n.lh-report-icon--stopwatch::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15 1H9v2h6V1zm-4 13h2V8h-2v6zm8.1-6.6L20.5 6l-1.4-1.4L17.7 6A9 9 0 0 0 3 13a9 9 0 1 0 16-5.6zm-7 12.6a7 7 0 1 1 0-14 7 7 0 0 1 0 14z"/></svg>\');\n}\n.lh-report-icon--networkspeed::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.9 5c-.2 0-.3 0-.4.2v.2L10.1 17a2 2 0 0 0-.2 1 2 2 0 0 0 4 .4l2.4-12.9c0-.3-.2-.5-.5-.5zM1 9l2 2c2.9-2.9 6.8-4 10.5-3.6l1.2-2.7C10 3.8 4.7 5.3 1 9zm20 2 2-2a15.4 15.4 0 0 0-5.6-3.6L17 8.2c1.5.7 2.9 1.6 4.1 2.8zm-4 4 2-2a9.9 9.9 0 0 0-2.7-1.9l-.5 3 1.2.9zM5 13l2 2a7.1 7.1 0 0 1 4-2l1.3-2.9C9.7 10.1 7 11 5 13z"/></svg>\');\n}\n.lh-report-icon--samples-one::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="7" cy="14" r="3"/><path d="M7 18a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm4-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm5.6 17.6a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>\');\n}\n.lh-report-icon--samples-many::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 18a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm4-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm5.6 17.6a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/><circle cx="7" cy="14" r="3"/><circle cx="11" cy="6" r="3"/></svg>\');\n}\n.lh-report-icon--chrome::before {\n background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-50 -50 562 562"><path d="M256 25.6v25.6a204 204 0 0 1 144.8 60 204 204 0 0 1 60 144.8 204 204 0 0 1-60 144.8 204 204 0 0 1-144.8 60 204 204 0 0 1-144.8-60 204 204 0 0 1-60-144.8 204 204 0 0 1 60-144.8 204 204 0 0 1 144.8-60V0a256 256 0 1 0 0 512 256 256 0 0 0 0-512v25.6z"/><path d="M256 179.2v25.6a51.3 51.3 0 0 1 0 102.4 51.3 51.3 0 0 1 0-102.4v-51.2a102.3 102.3 0 1 0-.1 204.7 102.3 102.3 0 0 0 .1-204.7v25.6z"/><path d="M256 204.8h217.6a25.6 25.6 0 0 0 0-51.2H256a25.6 25.6 0 0 0 0 51.2m44.3 76.8L191.5 470.1a25.6 25.6 0 1 0 44.4 25.6l108.8-188.5a25.6 25.6 0 1 0-44.4-25.6m-88.6 0L102.9 93.2a25.7 25.7 0 0 0-35-9.4 25.7 25.7 0 0 0-9.4 35l108.8 188.5a25.7 25.7 0 0 0 35 9.4 25.9 25.9 0 0 0 9.4-35.1"/></svg>\');\n}\n\n\n\n.lh-buttons {\n display: flex;\n flex-wrap: wrap;\n margin: var(--default-padding) 0;\n}\n.lh-button {\n height: 32px;\n border: 1px solid var(--report-border-color-secondary);\n border-radius: 3px;\n color: var(--link-color);\n background-color: var(--report-background-color);\n margin: 5px;\n}\n\n.lh-button:first-of-type {\n margin-left: 0;\n}\n\n/* Node */\n.lh-node__snippet {\n font-family: var(--report-font-family-monospace);\n color: var(--snippet-color);\n font-size: var(--report-monospace-font-size);\n line-height: 20px;\n}\n\n/* Score */\n\n.lh-audit__score-icon {\n width: var(--score-icon-size);\n height: var(--score-icon-size);\n margin: var(--score-icon-margin);\n}\n\n.lh-audit--pass .lh-audit__display-text {\n color: var(--color-pass-secondary);\n}\n.lh-audit--pass .lh-audit__score-icon,\n.lh-scorescale-range--pass::before {\n border-radius: 100%;\n background: var(--color-pass);\n}\n\n.lh-audit--average .lh-audit__display-text {\n color: var(--color-average-secondary);\n}\n.lh-audit--average .lh-audit__score-icon,\n.lh-scorescale-range--average::before {\n background: var(--color-average);\n width: var(--icon-square-size);\n height: var(--icon-square-size);\n}\n\n.lh-audit--fail .lh-audit__display-text {\n color: var(--color-fail-secondary);\n}\n.lh-audit--fail .lh-audit__score-icon,\n.lh-audit--error .lh-audit__score-icon,\n.lh-scorescale-range--fail::before {\n border-left: calc(var(--score-icon-size) / 2) solid transparent;\n border-right: calc(var(--score-icon-size) / 2) solid transparent;\n border-bottom: var(--score-icon-size) solid var(--color-fail);\n}\n\n.lh-audit--manual .lh-audit__display-text,\n.lh-audit--notapplicable .lh-audit__display-text {\n color: var(--color-gray-600);\n}\n.lh-audit--manual .lh-audit__score-icon,\n.lh-audit--notapplicable .lh-audit__score-icon {\n border: calc(0.2 * var(--score-icon-size)) solid var(--color-gray-400);\n border-radius: 100%;\n background: none;\n}\n\n.lh-audit--informative .lh-audit__display-text {\n color: var(--color-gray-600);\n}\n\n.lh-audit--informative .lh-audit__score-icon {\n border: calc(0.2 * var(--score-icon-size)) solid var(--color-gray-400);\n border-radius: 100%;\n}\n\n.lh-audit__description,\n.lh-audit__stackpack {\n color: var(--report-text-color-secondary);\n}\n.lh-audit__adorn {\n border: 1px solid slategray;\n border-radius: 3px;\n margin: 0 3px;\n padding: 0 2px;\n line-height: 1.1;\n display: inline-block;\n font-size: 90%;\n}\n\n.lh-category-header__description {\n text-align: center;\n color: var(--color-gray-700);\n margin: 0px auto;\n max-width: 400px;\n}\n\n\n.lh-audit__display-text,\n.lh-load-opportunity__sparkline,\n.lh-chevron-container {\n margin: 0 var(--audit-margin-horizontal);\n}\n.lh-chevron-container {\n margin-right: 0;\n}\n\n.lh-audit__title-and-text {\n flex: 1;\n}\n\n.lh-audit__title-and-text code {\n color: var(--snippet-color);\n font-size: var(--report-monospace-font-size);\n}\n\n/* Prepend display text with em dash separator. But not in Opportunities. */\n.lh-audit__display-text:not(:empty):before {\n content: \'—\';\n margin-right: var(--audit-margin-horizontal);\n}\n.lh-audit-group.lh-audit-group--load-opportunities .lh-audit__display-text:not(:empty):before {\n display: none;\n}\n\n/* Expandable Details (Audit Groups, Audits) */\n.lh-audit__header {\n display: flex;\n align-items: center;\n padding: var(--default-padding);\n}\n\n.lh-audit--load-opportunity .lh-audit__header {\n display: block;\n}\n\n\n.lh-metricfilter {\n display: grid;\n justify-content: end;\n align-items: center;\n grid-auto-flow: column;\n gap: 4px;\n color: var(--color-gray-700);\n}\n\n.lh-metricfilter__radio {\n position: absolute;\n left: -9999px;\n}\n.lh-metricfilter input[type=\'radio\']:focus-visible + label {\n outline: -webkit-focus-ring-color auto 1px;\n}\n\n.lh-metricfilter__label {\n display: inline-flex;\n padding: 0 4px;\n height: 16px;\n text-decoration: underline;\n align-items: center;\n cursor: pointer;\n font-size: 90%;\n}\n\n.lh-metricfilter__label--active {\n background: var(--color-blue-primary);\n color: var(--color-white);\n border-radius: 3px;\n text-decoration: none;\n}\n/* Give the \'All\' choice a more muted display */\n.lh-metricfilter__label--active[for="metric-All"] {\n background-color: var(--color-blue-200) !important;\n color: black !important;\n}\n\n.lh-metricfilter__text {\n margin-right: 8px;\n}\n\n/* If audits are filtered, hide the itemcount for Passed Audits… */\n.lh-category--filtered .lh-audit-group .lh-audit-group__itemcount {\n display: none;\n}\n\n\n.lh-audit__header:hover {\n background-color: var(--color-hover);\n}\n\n/* We want to hide the browser\'s default arrow marker on summary elements. Admittedly, it\'s complicated. */\n.lh-root details > summary {\n /* Blink 89+ and Firefox will hide the arrow when display is changed from (new) default of `list-item` to block. https://chromestatus.com/feature/6730096436051968*/\n display: block;\n}\n/* Safari and Blink <=88 require using the -webkit-details-marker selector */\n.lh-root details > summary::-webkit-details-marker {\n display: none;\n}\n\n/* Perf Metric */\n\n.lh-metrics-container {\n display: grid;\n grid-auto-rows: 1fr;\n grid-template-columns: 1fr 1fr;\n grid-column-gap: var(--report-line-height);\n}\n\n.lh-metric {\n border-top: 1px solid var(--report-border-color-secondary);\n}\n\n.lh-metric:nth-last-child(-n+2) {\n border-bottom: 1px solid var(--report-border-color-secondary);\n}\n\n\n.lh-metric__innerwrap {\n display: grid;\n /**\n * Icon -- Metric Name\n * -- Metric Value\n */\n grid-template-columns: calc(var(--score-icon-size) + var(--score-icon-margin-left) + var(--score-icon-margin-right)) 1fr;\n align-items: center;\n padding: var(--default-padding);\n}\n\n.lh-metric__details {\n order: -1;\n}\n\n.lh-metric__title {\n flex: 1;\n}\n\n.lh-calclink {\n padding-left: calc(1ex / 3);\n}\n\n.lh-metric__description {\n display: none;\n grid-column-start: 2;\n grid-column-end: 4;\n color: var(--report-text-color-secondary);\n}\n\n.lh-metric__value {\n font-size: var(--metric-value-font-size);\n margin: calc(var(--default-padding) / 2) 0;\n white-space: nowrap; /* No wrapping between metric value and the icon */\n grid-column-start: 2;\n}\n\n\n@media screen and (max-width: 535px) {\n .lh-metrics-container {\n display: block;\n }\n\n .lh-metric {\n border-bottom: none !important;\n }\n .lh-metric:nth-last-child(1) {\n border-bottom: 1px solid var(--report-border-color-secondary) !important;\n }\n\n /* Change the grid to 3 columns for narrow viewport. */\n .lh-metric__innerwrap {\n /**\n * Icon -- Metric Name -- Metric Value\n */\n grid-template-columns: calc(var(--score-icon-size) + var(--score-icon-margin-left) + var(--score-icon-margin-right)) 2fr 1fr;\n }\n .lh-metric__value {\n justify-self: end;\n grid-column-start: unset;\n }\n}\n\n/* No-JS toggle switch */\n/* Keep this selector sync\'d w/ `magicSelector` in report-ui-features-test.js */\n .lh-metrics-toggle__input:checked ~ .lh-metrics-container .lh-metric__description {\n display: block;\n}\n\n/* TODO get rid of the SVGS and clean up these some more */\n.lh-metrics-toggle__input {\n opacity: 0;\n position: absolute;\n right: 0;\n top: 0px;\n}\n\n.lh-metrics-toggle__input + div > label > .lh-metrics-toggle__labeltext--hide,\n.lh-metrics-toggle__input:checked + div > label > .lh-metrics-toggle__labeltext--show {\n display: none;\n}\n.lh-metrics-toggle__input:checked + div > label > .lh-metrics-toggle__labeltext--hide {\n display: inline;\n}\n.lh-metrics-toggle__input:focus + div > label {\n outline: -webkit-focus-ring-color auto 3px;\n}\n\n.lh-metrics-toggle__label {\n cursor: pointer;\n font-size: var(--report-font-size-secondary);\n line-height: var(--report-line-height-secondary);\n color: var(--color-gray-700);\n}\n\n/* Pushes the metric description toggle button to the right. */\n.lh-audit-group--metrics .lh-audit-group__header {\n display: flex;\n justify-content: space-between;\n}\n\n.lh-metric__icon,\n.lh-scorescale-range::before {\n content: \'\';\n width: var(--score-icon-size);\n height: var(--score-icon-size);\n display: inline-block;\n margin: var(--score-icon-margin);\n}\n\n.lh-metric--pass .lh-metric__value {\n color: var(--color-pass-secondary);\n}\n.lh-metric--pass .lh-metric__icon {\n border-radius: 100%;\n background: var(--color-pass);\n}\n\n.lh-metric--average .lh-metric__value {\n color: var(--color-average-secondary);\n}\n.lh-metric--average .lh-metric__icon {\n background: var(--color-average);\n width: var(--icon-square-size);\n height: var(--icon-square-size);\n}\n\n.lh-metric--fail .lh-metric__value {\n color: var(--color-fail-secondary);\n}\n.lh-metric--fail .lh-metric__icon,\n.lh-metric--error .lh-metric__icon {\n border-left: calc(var(--score-icon-size) / 2) solid transparent;\n border-right: calc(var(--score-icon-size) / 2) solid transparent;\n border-bottom: var(--score-icon-size) solid var(--color-fail);\n}\n\n.lh-metric--error .lh-metric__value,\n.lh-metric--error .lh-metric__description {\n color: var(--color-fail-secondary);\n}\n\n/* Perf load opportunity */\n\n.lh-load-opportunity__cols {\n display: flex;\n align-items: flex-start;\n}\n\n.lh-load-opportunity__header .lh-load-opportunity__col {\n color: var(--color-gray-600);\n display: unset;\n line-height: calc(2.3 * var(--report-font-size));\n}\n\n.lh-load-opportunity__col {\n display: flex;\n}\n\n.lh-load-opportunity__col--one {\n flex: 5;\n align-items: center;\n margin-right: 2px;\n}\n.lh-load-opportunity__col--two {\n flex: 4;\n text-align: right;\n}\n\n.lh-audit--load-opportunity .lh-audit__display-text {\n text-align: right;\n flex: 0 0 calc(3 * var(--report-font-size));\n}\n\n\n/* Sparkline */\n\n.lh-load-opportunity__sparkline {\n flex: 1;\n margin-top: calc((var(--report-line-height) - var(--sparkline-height)) / 2);\n}\n\n.lh-sparkline {\n height: var(--sparkline-height);\n width: 100%;\n}\n\n.lh-sparkline__bar {\n height: 100%;\n float: right;\n}\n\n.lh-audit--pass .lh-sparkline__bar {\n background: var(--color-pass);\n}\n\n.lh-audit--average .lh-sparkline__bar {\n background: var(--color-average);\n}\n\n.lh-audit--fail .lh-sparkline__bar {\n background: var(--color-fail);\n}\n\n/* Filmstrip */\n\n.lh-filmstrip-container {\n /* smaller gap between metrics and filmstrip */\n margin: -8px auto 0 auto;\n}\n\n.lh-filmstrip {\n display: grid;\n justify-content: space-between;\n padding-bottom: var(--default-padding);\n width: 100%;\n grid-template-columns: repeat(auto-fit, 60px);\n}\n\n.lh-filmstrip__frame {\n text-align: right;\n position: relative;\n}\n\n.lh-filmstrip__thumbnail {\n border: 1px solid var(--report-border-color-secondary);\n max-height: 100px;\n max-width: 60px;\n}\n\n/* Audit */\n\n.lh-audit {\n border-bottom: 1px solid var(--report-border-color-secondary);\n}\n\n/* Apply border-top to just the first audit. */\n.lh-audit {\n border-top: 1px solid var(--report-border-color-secondary);\n}\n.lh-audit ~ .lh-audit {\n border-top: none;\n}\n\n\n.lh-audit--error .lh-audit__display-text {\n color: var(--color-fail);\n}\n\n/* Audit Group */\n\n.lh-audit-group {\n margin-bottom: var(--audit-group-margin-bottom);\n position: relative;\n}\n.lh-audit-group--metrics {\n margin-bottom: calc(var(--audit-group-margin-bottom) / 2);\n}\n\n.lh-audit-group__header::before {\n /* By default, groups don\'t get an icon */\n content: none;\n width: var(--pwa-icon-size);\n height: var(--pwa-icon-size);\n margin: var(--pwa-icon-margin);\n display: inline-block;\n vertical-align: middle;\n}\n\n/* Style the "over budget" columns red. */\n.lh-audit-group--budgets #performance-budget tbody tr td:nth-child(4),\n.lh-audit-group--budgets #performance-budget tbody tr td:nth-child(5),\n.lh-audit-group--budgets #timing-budget tbody tr td:nth-child(3) {\n color: var(--color-red-700);\n}\n\n/* Align the "over budget request count" text to be close to the "over budget bytes" column. */\n.lh-audit-group--budgets .lh-table tbody tr td:nth-child(4){\n text-align: right;\n}\n\n.lh-audit-group--budgets .lh-details--budget {\n width: 100%;\n margin: 0 0 var(--default-padding);\n}\n\n.lh-audit-group--pwa-installable .lh-audit-group__header::before {\n content: \'\';\n background-image: var(--pwa-installable-gray-url);\n}\n.lh-audit-group--pwa-optimized .lh-audit-group__header::before {\n content: \'\';\n background-image: var(--pwa-optimized-gray-url);\n}\n.lh-audit-group--pwa-installable.lh-badged .lh-audit-group__header::before {\n background-image: var(--pwa-installable-color-url);\n}\n.lh-audit-group--pwa-optimized.lh-badged .lh-audit-group__header::before {\n background-image: var(--pwa-optimized-color-url);\n}\n\n.lh-audit-group--metrics .lh-audit-group__summary {\n margin-top: 0;\n margin-bottom: 0;\n}\n\n.lh-audit-group__summary {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.lh-audit-group__header .lh-chevron {\n margin-top: calc((var(--report-line-height) - 5px) / 2);\n}\n\n.lh-audit-group__header {\n letter-spacing: 0.8px;\n padding: var(--default-padding);\n padding-left: 0;\n}\n\n.lh-audit-group__header, .lh-audit-group__summary {\n font-size: var(--report-font-size-secondary);\n line-height: var(--report-line-height-secondary);\n color: var(--color-gray-700);\n}\n\n.lh-audit-group__title {\n text-transform: uppercase;\n font-weight: 500;\n}\n\n.lh-audit-group__itemcount {\n color: var(--color-gray-600);\n}\n\n.lh-audit-group__footer {\n color: var(--color-gray-600);\n display: block;\n margin-top: var(--default-padding);\n}\n\n.lh-details,\n.lh-category-header__description,\n.lh-load-opportunity__header,\n.lh-audit-group__footer {\n font-size: var(--report-font-size-secondary);\n line-height: var(--report-line-height-secondary);\n}\n\n.lh-audit-explanation {\n margin: var(--audit-padding-vertical) 0 calc(var(--audit-padding-vertical) / 2) var(--audit-margin-horizontal);\n line-height: var(--audit-explanation-line-height);\n display: inline-block;\n}\n\n.lh-audit--fail .lh-audit-explanation {\n color: var(--color-fail);\n}\n\n/* Report */\n.lh-list > div:not(:last-child) {\n padding-bottom: 20px;\n}\n\n.lh-header-container {\n display: block;\n margin: 0 auto;\n position: relative;\n word-wrap: break-word;\n}\n\n.lh-report {\n min-width: var(--report-min-width);\n}\n\n.lh-exception {\n font-size: large;\n}\n\n.lh-code {\n white-space: normal;\n margin-top: 0;\n font-size: var(--report-monospace-font-size);\n}\n\n.lh-warnings {\n --item-margin: calc(var(--report-line-height) / 6);\n color: var(--color-average-secondary);\n margin: var(--audit-padding-vertical) 0;\n padding: var(--default-padding)\n var(--default-padding)\n var(--default-padding)\n calc(var(--audit-description-padding-left));\n background-color: var(--toplevel-warning-background-color);\n}\n.lh-warnings span {\n font-weight: bold;\n}\n\n.lh-warnings--toplevel {\n --item-margin: calc(var(--header-line-height) / 4);\n color: var(--toplevel-warning-text-color);\n margin-left: auto;\n margin-right: auto;\n --content-width-minus-category-padding-sides: calc(var(--report-content-width) - calc(var(--default-padding) * 4) * 2);\n max-width: var(--content-width-minus-category-padding-sides);\n padding: var(--toplevel-warning-padding);\n border-radius: 8px;\n}\n\n.lh-warnings__msg {\n color: var(--toplevel-warning-message-text-color);\n margin: 0;\n}\n\n.lh-warnings ul {\n margin: 0;\n}\n.lh-warnings li {\n margin: var(--item-margin) 0;\n}\n.lh-warnings li:last-of-type {\n margin-bottom: 0;\n}\n\n.lh-scores-header {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n}\n.lh-scores-header__solo {\n padding: 0;\n border: 0;\n}\n\n/* Gauge */\n\n.lh-gauge__wrapper--pass {\n color: var(--color-pass-secondary);\n fill: var(--color-pass);\n stroke: var(--color-pass);\n}\n\n.lh-gauge__wrapper--average {\n color: var(--color-average-secondary);\n fill: var(--color-average);\n stroke: var(--color-average);\n}\n\n.lh-gauge__wrapper--fail {\n color: var(--color-fail-secondary);\n fill: var(--color-fail);\n stroke: var(--color-fail);\n}\n\n.lh-gauge__wrapper--not-applicable {\n color: var(--color-not-applicable);\n fill: var(--color-not-applicable);\n stroke: var(--color-not-applicable);\n}\n\n.lh-fraction__wrapper .lh-fraction__content::before {\n content: \'\';\n height: var(--score-icon-size);\n width: var(--score-icon-size);\n margin: var(--score-icon-margin);\n display: inline-block;\n}\n.lh-fraction__wrapper--pass .lh-fraction__content {\n color: var(--color-pass-secondary);\n}\n.lh-fraction__wrapper--pass .lh-fraction__background {\n background-color: var(--color-pass);\n}\n.lh-fraction__wrapper--pass .lh-fraction__content::before {\n background-color: var(--color-pass);\n border-radius: 50%;\n}\n.lh-fraction__wrapper--average .lh-fraction__content {\n color: var(--color-average-secondary);\n}\n.lh-fraction__wrapper--average .lh-fraction__background,\n.lh-fraction__wrapper--average .lh-fraction__content::before {\n background-color: var(--color-average);\n}\n.lh-fraction__wrapper--fail .lh-fraction__content {\n color: var(--color-fail);\n}\n.lh-fraction__wrapper--fail .lh-fraction__background {\n background-color: var(--color-fail);\n}\n.lh-fraction__wrapper--fail .lh-fraction__content::before {\n border-left: calc(var(--score-icon-size) / 2) solid transparent;\n border-right: calc(var(--score-icon-size) / 2) solid transparent;\n border-bottom: var(--score-icon-size) solid var(--color-fail);\n}\n.lh-fraction__wrapper--null .lh-fraction__content {\n color: var(--color-gray-700);\n}\n.lh-fraction__wrapper--null .lh-fraction__background {\n background-color: var(--color-gray-700);\n}\n.lh-fraction__wrapper--null .lh-fraction__content::before {\n border-radius: 50%;\n border: calc(0.2 * var(--score-icon-size)) solid var(--color-gray-700);\n}\n\n.lh-fraction__background {\n position: absolute;\n height: 100%;\n width: 100%;\n border-radius: calc(var(--gauge-circle-size) / 2);\n opacity: 0.1;\n z-index: -1;\n}\n\n.lh-fraction__content-wrapper {\n height: var(--gauge-circle-size);\n display: flex;\n align-items: center;\n}\n\n.lh-fraction__content {\n display: flex;\n position: relative;\n align-items: center;\n justify-content: center;\n font-size: calc(0.3 * var(--gauge-circle-size));\n line-height: calc(0.4 * var(--gauge-circle-size));\n width: max-content;\n min-width: calc(1.5 * var(--gauge-circle-size));\n padding: calc(0.1 * var(--gauge-circle-size)) calc(0.2 * var(--gauge-circle-size));\n --score-icon-size: calc(0.21 * var(--gauge-circle-size));\n --score-icon-margin: 0 calc(0.15 * var(--gauge-circle-size)) 0 0;\n}\n\n.lh-gauge {\n stroke-linecap: round;\n width: var(--gauge-circle-size);\n height: var(--gauge-circle-size);\n}\n\n.lh-category .lh-gauge {\n --gauge-circle-size: var(--gauge-circle-size-big);\n}\n\n.lh-gauge-base {\n opacity: 0.1;\n}\n\n.lh-gauge-arc {\n fill: none;\n transform-origin: 50% 50%;\n animation: load-gauge var(--transition-length) ease forwards;\n animation-delay: 250ms;\n}\n\n.lh-gauge__svg-wrapper {\n position: relative;\n height: var(--gauge-circle-size);\n}\n.lh-category .lh-gauge__svg-wrapper,\n.lh-category .lh-fraction__wrapper {\n --gauge-circle-size: var(--gauge-circle-size-big);\n}\n\n/* The plugin badge overlay */\n.lh-gauge__wrapper--plugin .lh-gauge__svg-wrapper::before {\n width: var(--plugin-badge-size);\n height: var(--plugin-badge-size);\n background-color: var(--plugin-badge-background-color);\n background-image: var(--plugin-icon-url);\n background-repeat: no-repeat;\n background-size: var(--plugin-icon-size);\n background-position: 58% 50%;\n content: "";\n position: absolute;\n right: -6px;\n bottom: 0px;\n display: block;\n z-index: 100;\n box-shadow: 0 0 4px rgba(0,0,0,.2);\n border-radius: 25%;\n}\n.lh-category .lh-gauge__wrapper--plugin .lh-gauge__svg-wrapper::before {\n width: var(--plugin-badge-size-big);\n height: var(--plugin-badge-size-big);\n}\n\n@keyframes load-gauge {\n from { stroke-dasharray: 0 352; }\n}\n\n.lh-gauge__percentage {\n width: 100%;\n height: var(--gauge-circle-size);\n position: absolute;\n font-family: var(--report-font-family-monospace);\n font-size: calc(var(--gauge-circle-size) * 0.34 + 1.3px);\n line-height: 0;\n text-align: center;\n top: calc(var(--score-container-padding) + var(--gauge-circle-size) / 2);\n}\n\n.lh-category .lh-gauge__percentage {\n --gauge-circle-size: var(--gauge-circle-size-big);\n --gauge-percentage-font-size: var(--gauge-percentage-font-size-big);\n}\n\n.lh-gauge__wrapper,\n.lh-fraction__wrapper {\n position: relative;\n display: flex;\n align-items: center;\n flex-direction: column;\n text-decoration: none;\n padding: var(--score-container-padding);\n\n --transition-length: 1s;\n\n /* Contain the layout style paint & layers during animation*/\n contain: content;\n will-change: opacity; /* Only using for layer promotion */\n}\n\n.lh-gauge__label,\n.lh-fraction__label {\n font-size: var(--gauge-label-font-size);\n font-weight: 500;\n line-height: var(--gauge-label-line-height);\n margin-top: 10px;\n text-align: center;\n color: var(--report-text-color);\n word-break: keep-all;\n}\n\n/* TODO(#8185) use more BEM (.lh-gauge__label--big) instead of relying on descendant selector */\n.lh-category .lh-gauge__label,\n.lh-category .lh-fraction__label {\n --gauge-label-font-size: var(--gauge-label-font-size-big);\n --gauge-label-line-height: var(--gauge-label-line-height-big);\n margin-top: 14px;\n}\n\n.lh-scores-header .lh-gauge__wrapper,\n.lh-scores-header .lh-fraction__wrapper,\n.lh-scores-header .lh-gauge--pwa__wrapper,\n.lh-sticky-header .lh-gauge__wrapper,\n.lh-sticky-header .lh-fraction__wrapper,\n.lh-sticky-header .lh-gauge--pwa__wrapper {\n width: var(--gauge-wrapper-width);\n}\n\n.lh-scorescale {\n display: inline-flex;\n\n gap: calc(var(--default-padding) * 4);\n margin: 16px auto 0 auto;\n font-size: var(--report-font-size-secondary);\n color: var(--color-gray-700);\n\n}\n\n.lh-scorescale-range {\n display: flex;\n align-items: center;\n font-family: var(--report-font-family-monospace);\n white-space: nowrap;\n}\n\n.lh-category-header__finalscreenshot .lh-scorescale {\n border: 0;\n display: flex;\n justify-content: center;\n}\n\n.lh-category-header__finalscreenshot .lh-scorescale-range {\n font-family: unset;\n font-size: 12px;\n}\n\n.lh-scorescale-wrap {\n display: contents;\n}\n\n/* Hide category score gauages if it\'s a single category report */\n.lh-header--solo-category .lh-scores-wrapper {\n display: none;\n}\n\n\n.lh-categories {\n width: 100%;\n overflow: hidden;\n}\n\n.lh-category {\n padding: var(--category-padding);\n max-width: var(--report-content-width);\n margin: 0 auto;\n\n --sticky-header-height: calc(var(--gauge-circle-size-sm) + var(--score-container-padding) * 2);\n --topbar-plus-sticky-header: calc(var(--topbar-height) + var(--sticky-header-height));\n scroll-margin-top: var(--topbar-plus-sticky-header);\n\n /* Faster recalc style & layout of the report. https://web.dev/content-visibility/ */\n content-visibility: auto;\n contain-intrinsic-size: 1000px;\n}\n\n.lh-category-wrapper {\n border-bottom: 1px solid var(--color-gray-200);\n}\n\n.lh-category-wrapper:first-of-type {\n border-top: 1px solid var(--color-gray-200);\n}\n\n.lh-category-header {\n margin-bottom: var(--section-padding-vertical);\n}\n\n.lh-category-header .lh-score__gauge {\n max-width: 400px;\n width: auto;\n margin: 0px auto;\n}\n\n.lh-category-header__finalscreenshot {\n display: grid;\n grid-template: none / 1fr 1px 1fr;\n justify-items: center;\n align-items: center;\n gap: var(--report-line-height);\n min-height: 288px;\n margin-bottom: var(--default-padding);\n}\n\n.lh-final-ss-image {\n /* constrain the size of the image to not be too large */\n max-height: calc(var(--gauge-circle-size-big) * 2.8);\n max-width: calc(var(--gauge-circle-size-big) * 3.5);\n border: 1px solid var(--color-gray-200);\n padding: 4px;\n border-radius: 3px;\n display: block;\n}\n\n.lh-category-headercol--separator {\n background: var(--color-gray-200);\n width: 1px;\n height: var(--gauge-circle-size-big);\n}\n\n@media screen and (max-width: 780px) {\n .lh-category-header__finalscreenshot {\n grid-template: 1fr 1fr / none\n }\n .lh-category-headercol--separator {\n display: none;\n }\n}\n\n\n/* 964 fits the min-width of the filmstrip */\n@media screen and (max-width: 964px) {\n .lh-report {\n margin-left: 0;\n width: 100%;\n }\n}\n\n@media print {\n body {\n -webkit-print-color-adjust: exact; /* print background colors */\n }\n .lh-container {\n display: block;\n }\n .lh-report {\n margin-left: 0;\n padding-top: 0;\n }\n .lh-categories {\n margin-top: 0;\n }\n}\n\n.lh-table {\n border-collapse: collapse;\n /* Can\'t assign padding to table, so shorten the width instead. */\n width: calc(100% - var(--audit-description-padding-left) - var(--stackpack-padding-horizontal));\n border: 1px solid var(--report-border-color-secondary);\n\n}\n\n.lh-table thead th {\n font-weight: normal;\n color: var(--color-gray-600);\n /* See text-wrapping comment on .lh-container. */\n word-break: normal;\n}\n\n.lh-row--even {\n background-color: var(--table-higlight-background-color);\n}\n.lh-row--hidden {\n display: none;\n}\n\n.lh-table th,\n.lh-table td {\n padding: var(--default-padding);\n}\n\n.lh-table tr {\n vertical-align: middle;\n}\n\n/* Looks unnecessary, but mostly for keeping the <th>s left-aligned */\n.lh-table-column--text,\n.lh-table-column--source-location,\n.lh-table-column--url,\n/* .lh-table-column--thumbnail, */\n/* .lh-table-column--empty,*/\n.lh-table-column--code,\n.lh-table-column--node {\n text-align: left;\n}\n\n.lh-table-column--code {\n min-width: 100px;\n}\n\n.lh-table-column--bytes,\n.lh-table-column--timespanMs,\n.lh-table-column--ms,\n.lh-table-column--numeric {\n text-align: right;\n word-break: normal;\n}\n\n\n\n.lh-table .lh-table-column--thumbnail {\n width: var(--image-preview-size);\n}\n\n.lh-table-column--url {\n min-width: 250px;\n}\n\n.lh-table-column--text {\n min-width: 80px;\n}\n\n/* Keep columns narrow if they follow the URL column */\n/* 12% was determined to be a decent narrow width, but wide enough for column headings */\n.lh-table-column--url + th.lh-table-column--bytes,\n.lh-table-column--url + .lh-table-column--bytes + th.lh-table-column--bytes,\n.lh-table-column--url + .lh-table-column--ms,\n.lh-table-column--url + .lh-table-column--ms + th.lh-table-column--bytes,\n.lh-table-column--url + .lh-table-column--bytes + th.lh-table-column--timespanMs {\n width: 12%;\n}\n\n.lh-text__url-host {\n display: inline;\n}\n\n.lh-text__url-host {\n margin-left: calc(var(--report-font-size) / 2);\n opacity: 0.6;\n font-size: 90%\n}\n\n.lh-thumbnail {\n object-fit: cover;\n width: var(--image-preview-size);\n height: var(--image-preview-size);\n display: block;\n}\n\n.lh-unknown pre {\n overflow: scroll;\n border: solid 1px var(--color-gray-200);\n}\n\n.lh-text__url > a {\n color: inherit;\n text-decoration: none;\n}\n\n.lh-text__url > a:hover {\n text-decoration: underline dotted #999;\n}\n\n.lh-sub-item-row {\n margin-left: 20px;\n margin-bottom: 0;\n color: var(--color-gray-700);\n}\n.lh-sub-item-row td {\n padding-top: 4px;\n padding-bottom: 4px;\n padding-left: 20px;\n}\n\n/* Chevron\n https://codepen.io/paulirish/pen/LmzEmK\n */\n.lh-chevron {\n --chevron-angle: 42deg;\n /* Edge doesn\'t support transform: rotate(calc(...)), so we define it here */\n --chevron-angle-right: -42deg;\n width: var(--chevron-size);\n height: var(--chevron-size);\n margin-top: calc((var(--report-line-height) - 12px) / 2);\n}\n\n.lh-chevron__lines {\n transition: transform 0.4s;\n transform: translateY(var(--report-line-height));\n}\n.lh-chevron__line {\n stroke: var(--chevron-line-stroke);\n stroke-width: var(--chevron-size);\n stroke-linecap: square;\n transform-origin: 50%;\n transform: rotate(var(--chevron-angle));\n transition: transform 300ms, stroke 300ms;\n}\n\n.lh-expandable-details .lh-chevron__line-right,\n.lh-expandable-details[open] .lh-chevron__line-left {\n transform: rotate(var(--chevron-angle-right));\n}\n\n.lh-expandable-details[open] .lh-chevron__line-right {\n transform: rotate(var(--chevron-angle));\n}\n\n\n.lh-expandable-details[open] .lh-chevron__lines {\n transform: translateY(calc(var(--chevron-size) * -1));\n}\n\n.lh-expandable-details[open] {\n animation: 300ms openDetails forwards;\n padding-bottom: var(--default-padding);\n}\n\n@keyframes openDetails {\n from {\n outline: 1px solid var(--report-background-color);\n }\n to {\n outline: 1px solid;\n box-shadow: 0 2px 4px rgba(0, 0, 0, .24);\n }\n}\n\n@media screen and (max-width: 780px) {\n /* no black outline if we\'re not confident the entire table can be displayed within bounds */\n .lh-expandable-details[open] {\n animation: none;\n }\n}\n\n.lh-expandable-details[open] summary, details.lh-clump > summary {\n border-bottom: 1px solid var(--report-border-color-secondary);\n}\ndetails.lh-clump[open] > summary {\n border-bottom-width: 0;\n}\n\n\n\ndetails .lh-clump-toggletext--hide,\ndetails[open] .lh-clump-toggletext--show { display: none; }\ndetails[open] .lh-clump-toggletext--hide { display: block;}\n\n\n/* Tooltip */\n.lh-tooltip-boundary {\n position: relative;\n}\n\n.lh-tooltip {\n position: absolute;\n display: none; /* Don\'t retain these layers when not needed */\n opacity: 0;\n background: #ffffff;\n white-space: pre-line; /* Render newlines in the text */\n min-width: 246px;\n max-width: 275px;\n padding: 15px;\n border-radius: 5px;\n text-align: initial;\n line-height: 1.4;\n}\n/* shrink tooltips to not be cutoff on left edge of narrow viewports\n 45vw is chosen to be ~= width of the left column of metrics\n*/\n@media screen and (max-width: 535px) {\n .lh-tooltip {\n min-width: 45vw;\n padding: 3vw;\n }\n}\n\n.lh-tooltip-boundary:hover .lh-tooltip {\n display: block;\n animation: fadeInTooltip 250ms;\n animation-fill-mode: forwards;\n animation-delay: 850ms;\n bottom: 100%;\n z-index: 1;\n will-change: opacity;\n right: 0;\n pointer-events: none;\n}\n\n.lh-tooltip::before {\n content: "";\n border: solid transparent;\n border-bottom-color: #fff;\n border-width: 10px;\n position: absolute;\n bottom: -20px;\n right: 6px;\n transform: rotate(180deg);\n pointer-events: none;\n}\n\n@keyframes fadeInTooltip {\n 0% { opacity: 0; }\n 75% { opacity: 1; }\n 100% { opacity: 1; filter: drop-shadow(1px 0px 1px #aaa) drop-shadow(0px 2px 4px hsla(206, 6%, 25%, 0.15)); pointer-events: auto; }\n}\n\n/* Element screenshot */\n.lh-element-screenshot {\n position: relative;\n overflow: hidden;\n float: left;\n margin-right: 20px;\n}\n.lh-element-screenshot__content {\n overflow: hidden;\n}\n.lh-element-screenshot__image {\n /* Set by ElementScreenshotRenderer.installFullPageScreenshotCssVariable */\n background-image: var(--element-screenshot-url);\n outline: 2px solid #777;\n background-color: white;\n background-repeat: no-repeat;\n}\n.lh-element-screenshot__mask {\n position: absolute;\n background: #555;\n opacity: 0.8;\n}\n.lh-element-screenshot__element-marker {\n position: absolute;\n outline: 2px solid var(--color-lime-400);\n}\n.lh-element-screenshot__overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 2000; /* .lh-topbar is 1000 */\n background: var(--screenshot-overlay-background);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: zoom-out;\n}\n\n.lh-element-screenshot__overlay .lh-element-screenshot {\n margin-right: 0; /* clearing margin used in thumbnail case */\n outline: 1px solid var(--color-gray-700);\n}\n\n.lh-screenshot-overlay--enabled .lh-element-screenshot {\n cursor: zoom-out;\n}\n.lh-screenshot-overlay--enabled .lh-node .lh-element-screenshot {\n cursor: zoom-in;\n}\n\n\n.lh-meta__items {\n --meta-icon-size: calc(var(--report-icon-size) * 0.667);\n padding: var(--default-padding);\n display: grid;\n grid-template-columns: 1fr 1fr 1fr;\n background-color: var(--env-item-background-color);\n border-radius: 3px;\n margin: 0 0 var(--default-padding) 0;\n font-size: 12px;\n column-gap: var(--default-padding);\n color: var(--color-gray-700);\n}\n\n.lh-meta__item {\n display: block;\n list-style-type: none;\n position: relative;\n padding: 0 0 0 calc(var(--meta-icon-size) + var(--default-padding) * 2);\n cursor: unset; /* disable pointer cursor from report-icon */\n}\n\n.lh-meta__item.lh-tooltip-boundary {\n text-decoration: dotted underline var(--color-gray-500);\n cursor: help;\n}\n\n.lh-meta__item.lh-report-icon::before {\n position: absolute;\n left: var(--default-padding);\n width: var(--meta-icon-size);\n height: var(--meta-icon-size);\n}\n\n.lh-meta__item.lh-report-icon:hover::before {\n opacity: 0.7;\n}\n\n.lh-meta__item .lh-tooltip {\n color: var(--color-gray-800);\n}\n\n.lh-meta__item .lh-tooltip::before {\n right: auto; /* Set the tooltip arrow to the leftside */\n left: 6px;\n}\n\n/* Change the grid for narrow viewport. */\n@media screen and (max-width: 640px) {\n .lh-meta__items {\n grid-template-columns: 1fr 1fr;\n }\n}\n@media screen and (max-width: 535px) {\n .lh-meta__items {\n display: block;\n }\n}\n\n\n/*# sourceURL=report-styles.css */\n');
1217
+ el0.append(el1);
1218
+ return el0;
1219
+ }
1220
+
1209
1221
  /**
1210
1222
  * @param {DOM} dom
1211
1223
  */
1212
1224
  function createTopbarComponent(dom) {
1213
- const el0 = dom.document().createDocumentFragment();
1225
+ const el0 = dom.createFragment();
1214
1226
  const el1 = dom.createElement('style');
1215
- el1.append('\n .lh-topbar {\n position: sticky;\n top: 0;\n left: 0;\n right: 0;\n z-index: 1000;\n display: flex;\n align-items: center;\n height: var(--topbar-height);\n background-color: var(--topbar-background-color);\n padding: var(--topbar-padding);\n }\n\n .lh-topbar__logo {\n width: var(--topbar-logo-size);\n height: var(--topbar-logo-size);\n user-select: none;\n flex: none;\n }\n\n .lh-topbar__url {\n margin: var(--topbar-padding);\n text-decoration: none;\n color: var(--report-text-color);\n text-overflow: ellipsis;\n overflow: hidden;\n white-space: nowrap;\n }\n\n .lh-tools {\n margin-left: auto;\n will-change: transform;\n min-width: var(--report-icon-size);\n }\n .lh-tools__button {\n width: var(--report-icon-size);\n height: var(--report-icon-size);\n cursor: pointer;\n margin-right: 5px;\n /* This is actually a button element, but we want to style it like a transparent div. */\n display: flex;\n background: none;\n color: inherit;\n border: none;\n padding: 0;\n font: inherit;\n outline: inherit;\n }\n .lh-tools__button svg {\n fill: var(--tools-icon-color);\n }\n .lh-dark .lh-tools__button svg {\n filter: invert(1);\n }\n .lh-tools__button.lh-active + .lh-tools__dropdown {\n opacity: 1;\n clip: rect(-1px, 194px, 242px, -3px);\n visibility: visible;\n }\n .lh-tools__dropdown {\n position: absolute;\n background-color: var(--report-background-color);\n border: 1px solid var(--report-border-color);\n border-radius: 3px;\n padding: calc(var(--default-padding) / 2) 0;\n cursor: pointer;\n top: 36px;\n right: 0;\n box-shadow: 1px 1px 3px #ccc;\n min-width: 125px;\n clip: rect(0, 164px, 0, 0);\n visibility: hidden;\n opacity: 0;\n transition: all 200ms cubic-bezier(0,0,0.2,1);\n }\n .lh-tools__dropdown a {\n color: currentColor;\n text-decoration: none;\n white-space: nowrap;\n padding: 0 12px;\n line-height: 2;\n }\n .lh-tools__dropdown a:hover,\n .lh-tools__dropdown a:focus {\n background-color: var(--color-gray-200);\n outline: none;\n }\n /* save-gist option hidden in report. */\n .lh-tools__dropdown a[data-action=\'save-gist\'] {\n display: none;\n }\n\n @media screen and (max-width: 964px) {\n .lh-tools__dropdown {\n right: 0;\n left: initial;\n }\n }\n @media print {\n .lh-topbar {\n position: static;\n margin-left: 0;\n }\n\n .lh-tools__dropdown {\n display: none;\n }\n }\n ');
1227
+ el1.append('\n .lh-topbar {\n position: sticky;\n top: 0;\n left: 0;\n right: 0;\n z-index: 1000;\n display: flex;\n align-items: center;\n height: var(--topbar-height);\n padding: var(--topbar-padding);\n font-size: var(--report-font-size-secondary);\n background-color: var(--topbar-background-color);\n border-bottom: 1px solid var(--color-gray-200);\n }\n\n .lh-topbar__logo {\n width: var(--topbar-logo-size);\n height: var(--topbar-logo-size);\n user-select: none;\n flex: none;\n }\n\n .lh-topbar__url {\n margin: var(--topbar-padding);\n text-decoration: none;\n color: var(--report-text-color);\n text-overflow: ellipsis;\n overflow: hidden;\n white-space: nowrap;\n }\n\n .lh-tools {\n display: flex;\n align-items: center;\n margin-left: auto;\n will-change: transform;\n min-width: var(--report-icon-size);\n }\n .lh-tools__button {\n width: var(--report-icon-size);\n min-width: 24px;\n height: var(--report-icon-size);\n cursor: pointer;\n margin-right: 5px;\n /* This is actually a button element, but we want to style it like a transparent div. */\n display: flex;\n background: none;\n color: inherit;\n border: none;\n padding: 0;\n font: inherit;\n outline: inherit;\n }\n .lh-tools__button svg {\n fill: var(--tools-icon-color);\n }\n .lh-dark .lh-tools__button svg {\n filter: invert(1);\n }\n .lh-tools__button.lh-active + .lh-tools__dropdown {\n opacity: 1;\n clip: rect(-1px, 194px, 242px, -3px);\n visibility: visible;\n }\n .lh-tools__dropdown {\n position: absolute;\n background-color: var(--report-background-color);\n border: 1px solid var(--report-border-color);\n border-radius: 3px;\n padding: calc(var(--default-padding) / 2) 0;\n cursor: pointer;\n top: 36px;\n right: 0;\n box-shadow: 1px 1px 3px #ccc;\n min-width: 125px;\n clip: rect(0, 164px, 0, 0);\n visibility: hidden;\n opacity: 0;\n transition: all 200ms cubic-bezier(0,0,0.2,1);\n }\n .lh-tools__dropdown a {\n color: currentColor;\n text-decoration: none;\n white-space: nowrap;\n padding: 0 6px;\n line-height: 2;\n }\n .lh-tools__dropdown a:hover,\n .lh-tools__dropdown a:focus {\n background-color: var(--color-gray-200);\n outline: none;\n }\n /* save-gist option hidden in report. */\n .lh-tools__dropdown a[data-action=\'save-gist\'] {\n display: none;\n }\n\n .lh-locale-selector {\n width: 100%;\n color: var(--report-text-color);\n background-color: var(--locale-selector-background-color);\n padding: 2px;\n }\n .lh-tools-locale {\n display: flex;\n align-items: center;\n flex-direction: row-reverse;\n }\n .lh-tools-locale__selector-wrapper {\n transition: opacity 0.15s;\n opacity: 0;\n max-width: 200px;\n }\n .lh-button.lh-tool-locale__button {\n height: var(--topbar-height);\n color: var(--tools-icon-color);\n padding: calc(var(--default-padding) / 2);\n }\n .lh-tool-locale__button.lh-active + .lh-tools-locale__selector-wrapper {\n opacity: 1;\n clip: rect(-1px, 194px, 242px, -3px);\n visibility: visible;\n margin: 0 4px;\n }\n\n @media screen and (max-width: 964px) {\n .lh-tools__dropdown {\n right: 0;\n left: initial;\n }\n }\n @media print {\n .lh-topbar {\n position: static;\n margin-left: 0;\n }\n\n .lh-tools__dropdown {\n display: none;\n }\n }\n ');
1216
1228
  el0.append(el1);
1217
1229
  const el2 = dom.createElement('div', 'lh-topbar');
1218
1230
  const el3 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg', 'lh-topbar__logo');
@@ -1319,78 +1331,105 @@ function createTopbarComponent(dom) {
1319
1331
  el25.setAttribute('target', '_blank');
1320
1332
  el25.setAttribute('rel', 'noopener');
1321
1333
  const el26 = dom.createElement('div', 'lh-tools');
1322
- const el27 = dom.createElement('button', 'lh-tools__button');
1323
- el27.setAttribute('id', 'lh-tools-button');
1324
- el27.setAttribute('title', 'Tools menu');
1325
- el27.setAttribute('aria-label', 'Toggle report tools menu');
1326
- el27.setAttribute('aria-haspopup', 'menu');
1327
- el27.setAttribute('aria-expanded', 'false');
1328
- el27.setAttribute('aria-controls', 'lh-tools-dropdown');
1329
- const el28 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg');
1330
- el28.setAttribute('width', '100%');
1331
- el28.setAttribute('height', '100%');
1332
- el28.setAttribute('viewBox', '0 0 24 24');
1333
- const el29 = dom.createElementNS('http://www.w3.org/2000/svg', 'path');
1334
- el29.setAttribute('d', 'M0 0h24v24H0z');
1335
- el29.setAttribute('fill', 'none');
1334
+ const el27 = dom.createElement('div', 'lh-tools-locale lh-hidden');
1335
+ const el28 = dom.createElement('button', 'lh-button lh-tool-locale__button');
1336
+ el28.setAttribute('id', 'lh-button__swap-locales');
1337
+ el28.setAttribute('title', 'Show Language Picker');
1338
+ el28.setAttribute('aria-label', 'Toggle language picker');
1339
+ el28.setAttribute('aria-haspopup', 'menu');
1340
+ el28.setAttribute('aria-expanded', 'false');
1341
+ el28.setAttribute('aria-controls', 'lh-tools-locale__selector-wrapper');
1342
+ const el29 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg');
1343
+ el29.setAttribute('width', '20px');
1344
+ el29.setAttribute('height', '20px');
1345
+ el29.setAttribute('viewBox', '0 0 24 24');
1346
+ el29.setAttribute('fill', 'currentColor');
1336
1347
  const el30 = dom.createElementNS('http://www.w3.org/2000/svg', 'path');
1337
- el30.setAttribute('d', 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z');
1338
- el28.append(' ', el29, ' ', el30, ' ');
1339
- el27.append(' ', el28, ' ');
1340
- const el31 = dom.createElement('div', 'lh-tools__dropdown');
1341
- el31.setAttribute('id', 'lh-tools-dropdown');
1342
- el31.setAttribute('role', 'menu');
1343
- el31.setAttribute('aria-labelledby', 'lh-tools-button');
1344
- const el32 = dom.createElement('a', 'lh-report-icon lh-report-icon--print');
1345
- el32.setAttribute('role', 'menuitem');
1346
- el32.setAttribute('tabindex', '-1');
1347
- el32.setAttribute('href', '#');
1348
- el32.setAttribute('data-i18n', 'dropdownPrintSummary');
1349
- el32.setAttribute('data-action', 'print-summary');
1350
- const el33 = dom.createElement('a', 'lh-report-icon lh-report-icon--print');
1351
- el33.setAttribute('role', 'menuitem');
1352
- el33.setAttribute('tabindex', '-1');
1353
- el33.setAttribute('href', '#');
1354
- el33.setAttribute('data-i18n', 'dropdownPrintExpanded');
1355
- el33.setAttribute('data-action', 'print-expanded');
1356
- const el34 = dom.createElement('a', 'lh-report-icon lh-report-icon--copy');
1357
- el34.setAttribute('role', 'menuitem');
1358
- el34.setAttribute('tabindex', '-1');
1359
- el34.setAttribute('href', '#');
1360
- el34.setAttribute('data-i18n', 'dropdownCopyJSON');
1361
- el34.setAttribute('data-action', 'copy');
1362
- const el35 = dom.createElement('a', 'lh-report-icon lh-report-icon--download');
1363
- el35.setAttribute('role', 'menuitem');
1364
- el35.setAttribute('tabindex', '-1');
1365
- el35.setAttribute('href', '#');
1366
- el35.setAttribute('data-i18n', 'dropdownSaveHTML');
1367
- el35.setAttribute('data-action', 'save-html');
1368
- const el36 = dom.createElement('a', 'lh-report-icon lh-report-icon--download');
1369
- el36.setAttribute('role', 'menuitem');
1370
- el36.setAttribute('tabindex', '-1');
1371
- el36.setAttribute('href', '#');
1372
- el36.setAttribute('data-i18n', 'dropdownSaveJSON');
1373
- el36.setAttribute('data-action', 'save-json');
1374
- const el37 = dom.createElement('a', 'lh-report-icon lh-report-icon--open');
1375
- el37.setAttribute('role', 'menuitem');
1376
- el37.setAttribute('tabindex', '-1');
1377
- el37.setAttribute('href', '#');
1378
- el37.setAttribute('data-i18n', 'dropdownViewer');
1379
- el37.setAttribute('data-action', 'open-viewer');
1380
- const el38 = dom.createElement('a', 'lh-report-icon lh-report-icon--open');
1348
+ el30.setAttribute('d', 'M0 0h24v24H0V0z');
1349
+ el30.setAttribute('fill', 'none');
1350
+ const el31 = dom.createElementNS('http://www.w3.org/2000/svg', 'path');
1351
+ el31.setAttribute('d', 'M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z');
1352
+ el29.append(el30, el31);
1353
+ el28.append(' ', el29, ' ');
1354
+ const el32 = dom.createElement('div', 'lh-tools-locale__selector-wrapper');
1355
+ el32.setAttribute('id', 'lh-tools-locale__selector-wrapper');
1356
+ el32.setAttribute('role', 'menu');
1357
+ el32.setAttribute('aria-labelledby', 'lh-button__swap-locales');
1358
+ el32.setAttribute('aria-hidden', 'true');
1359
+ el32.append(' ', ' ');
1360
+ el27.append(' ', el28, ' ', el32, ' ');
1361
+ const el33 = dom.createElement('button', 'lh-tools__button');
1362
+ el33.setAttribute('id', 'lh-tools-button');
1363
+ el33.setAttribute('title', 'Tools menu');
1364
+ el33.setAttribute('aria-label', 'Toggle report tools menu');
1365
+ el33.setAttribute('aria-haspopup', 'menu');
1366
+ el33.setAttribute('aria-expanded', 'false');
1367
+ el33.setAttribute('aria-controls', 'lh-tools-dropdown');
1368
+ const el34 = dom.createElementNS('http://www.w3.org/2000/svg', 'svg');
1369
+ el34.setAttribute('width', '100%');
1370
+ el34.setAttribute('height', '100%');
1371
+ el34.setAttribute('viewBox', '0 0 24 24');
1372
+ const el35 = dom.createElementNS('http://www.w3.org/2000/svg', 'path');
1373
+ el35.setAttribute('d', 'M0 0h24v24H0z');
1374
+ el35.setAttribute('fill', 'none');
1375
+ const el36 = dom.createElementNS('http://www.w3.org/2000/svg', 'path');
1376
+ el36.setAttribute('d', 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z');
1377
+ el34.append(' ', el35, ' ', el36, ' ');
1378
+ el33.append(' ', el34, ' ');
1379
+ const el37 = dom.createElement('div', 'lh-tools__dropdown');
1380
+ el37.setAttribute('id', 'lh-tools-dropdown');
1381
+ el37.setAttribute('role', 'menu');
1382
+ el37.setAttribute('aria-labelledby', 'lh-tools-button');
1383
+ const el38 = dom.createElement('a', 'lh-report-icon lh-report-icon--print');
1381
1384
  el38.setAttribute('role', 'menuitem');
1382
1385
  el38.setAttribute('tabindex', '-1');
1383
1386
  el38.setAttribute('href', '#');
1384
- el38.setAttribute('data-i18n', 'dropdownSaveGist');
1385
- el38.setAttribute('data-action', 'save-gist');
1386
- const el39 = dom.createElement('a', 'lh-report-icon lh-report-icon--dark');
1387
+ el38.setAttribute('data-i18n', 'dropdownPrintSummary');
1388
+ el38.setAttribute('data-action', 'print-summary');
1389
+ const el39 = dom.createElement('a', 'lh-report-icon lh-report-icon--print');
1387
1390
  el39.setAttribute('role', 'menuitem');
1388
1391
  el39.setAttribute('tabindex', '-1');
1389
1392
  el39.setAttribute('href', '#');
1390
- el39.setAttribute('data-i18n', 'dropdownDarkTheme');
1391
- el39.setAttribute('data-action', 'toggle-dark');
1392
- el31.append(' ', el32, ' ', el33, ' ', el34, ' ', el35, ' ', el36, ' ', el37, ' ', el38, ' ', el39, ' ');
1393
- el26.append(' ', el27, ' ', el31, ' ');
1393
+ el39.setAttribute('data-i18n', 'dropdownPrintExpanded');
1394
+ el39.setAttribute('data-action', 'print-expanded');
1395
+ const el40 = dom.createElement('a', 'lh-report-icon lh-report-icon--copy');
1396
+ el40.setAttribute('role', 'menuitem');
1397
+ el40.setAttribute('tabindex', '-1');
1398
+ el40.setAttribute('href', '#');
1399
+ el40.setAttribute('data-i18n', 'dropdownCopyJSON');
1400
+ el40.setAttribute('data-action', 'copy');
1401
+ const el41 = dom.createElement('a', 'lh-report-icon lh-report-icon--download');
1402
+ el41.setAttribute('role', 'menuitem');
1403
+ el41.setAttribute('tabindex', '-1');
1404
+ el41.setAttribute('href', '#');
1405
+ el41.setAttribute('data-i18n', 'dropdownSaveHTML');
1406
+ el41.setAttribute('data-action', 'save-html');
1407
+ const el42 = dom.createElement('a', 'lh-report-icon lh-report-icon--download');
1408
+ el42.setAttribute('role', 'menuitem');
1409
+ el42.setAttribute('tabindex', '-1');
1410
+ el42.setAttribute('href', '#');
1411
+ el42.setAttribute('data-i18n', 'dropdownSaveJSON');
1412
+ el42.setAttribute('data-action', 'save-json');
1413
+ const el43 = dom.createElement('a', 'lh-report-icon lh-report-icon--open');
1414
+ el43.setAttribute('role', 'menuitem');
1415
+ el43.setAttribute('tabindex', '-1');
1416
+ el43.setAttribute('href', '#');
1417
+ el43.setAttribute('data-i18n', 'dropdownViewer');
1418
+ el43.setAttribute('data-action', 'open-viewer');
1419
+ const el44 = dom.createElement('a', 'lh-report-icon lh-report-icon--open');
1420
+ el44.setAttribute('role', 'menuitem');
1421
+ el44.setAttribute('tabindex', '-1');
1422
+ el44.setAttribute('href', '#');
1423
+ el44.setAttribute('data-i18n', 'dropdownSaveGist');
1424
+ el44.setAttribute('data-action', 'save-gist');
1425
+ const el45 = dom.createElement('a', 'lh-report-icon lh-report-icon--dark');
1426
+ el45.setAttribute('role', 'menuitem');
1427
+ el45.setAttribute('tabindex', '-1');
1428
+ el45.setAttribute('href', '#');
1429
+ el45.setAttribute('data-i18n', 'dropdownDarkTheme');
1430
+ el45.setAttribute('data-action', 'toggle-dark');
1431
+ el37.append(' ', el38, ' ', el39, ' ', el40, ' ', el41, ' ', el42, ' ', el43, ' ', el44, ' ', el45, ' ');
1432
+ el26.append(' ', el27, ' ', el33, ' ', el37, ' ');
1394
1433
  el2.append(' ', ' ', el3, ' ', el25, ' ', el26, ' ');
1395
1434
  el0.append(el2);
1396
1435
  return el0;
@@ -1400,7 +1439,7 @@ function createTopbarComponent(dom) {
1400
1439
  * @param {DOM} dom
1401
1440
  */
1402
1441
  function createWarningsToplevelComponent(dom) {
1403
- const el0 = dom.document().createDocumentFragment();
1442
+ const el0 = dom.createFragment();
1404
1443
  const el1 = dom.createElement('div', 'lh-warnings lh-warnings--toplevel');
1405
1444
  const el2 = dom.createElement('p', 'lh-warnings__msg');
1406
1445
  const el3 = dom.createElement('ul');
@@ -1410,7 +1449,7 @@ function createWarningsToplevelComponent(dom) {
1410
1449
  }
1411
1450
 
1412
1451
 
1413
- /** @typedef {'3pFilter'|'audit'|'categoryHeader'|'chevron'|'clump'|'crc'|'crcChain'|'elementScreenshot'|'envItem'|'footer'|'fraction'|'gauge'|'gaugePwa'|'heading'|'metric'|'metricsToggle'|'opportunity'|'opportunityHeader'|'scorescale'|'scoresWrapper'|'snippet'|'snippetContent'|'snippetHeader'|'snippetLine'|'topbar'|'warningsToplevel'} ComponentName */
1452
+ /** @typedef {'3pFilter'|'audit'|'categoryHeader'|'chevron'|'clump'|'crc'|'crcChain'|'elementScreenshot'|'footer'|'fraction'|'gauge'|'gaugePwa'|'heading'|'metric'|'opportunity'|'opportunityHeader'|'scorescale'|'scoresWrapper'|'snippet'|'snippetContent'|'snippetHeader'|'snippetLine'|'styles'|'topbar'|'warningsToplevel'} ComponentName */
1414
1453
  /**
1415
1454
  * @param {DOM} dom
1416
1455
  * @param {ComponentName} componentName
@@ -1426,14 +1465,12 @@ function createComponent(dom, componentName) {
1426
1465
  case 'crc': return createCrcComponent(dom);
1427
1466
  case 'crcChain': return createCrcChainComponent(dom);
1428
1467
  case 'elementScreenshot': return createElementScreenshotComponent(dom);
1429
- case 'envItem': return createEnvItemComponent(dom);
1430
1468
  case 'footer': return createFooterComponent(dom);
1431
1469
  case 'fraction': return createFractionComponent(dom);
1432
1470
  case 'gauge': return createGaugeComponent(dom);
1433
1471
  case 'gaugePwa': return createGaugePwaComponent(dom);
1434
1472
  case 'heading': return createHeadingComponent(dom);
1435
1473
  case 'metric': return createMetricComponent(dom);
1436
- case 'metricsToggle': return createMetricsToggleComponent(dom);
1437
1474
  case 'opportunity': return createOpportunityComponent(dom);
1438
1475
  case 'opportunityHeader': return createOpportunityHeaderComponent(dom);
1439
1476
  case 'scorescale': return createScorescaleComponent(dom);
@@ -1442,6 +1479,7 @@ function createComponent(dom, componentName) {
1442
1479
  case 'snippetContent': return createSnippetContentComponent(dom);
1443
1480
  case 'snippetHeader': return createSnippetHeaderComponent(dom);
1444
1481
  case 'snippetLine': return createSnippetLineComponent(dom);
1482
+ case 'styles': return createStylesComponent(dom);
1445
1483
  case 'topbar': return createTopbarComponent(dom);
1446
1484
  case 'warningsToplevel': return createWarningsToplevelComponent(dom);
1447
1485
  }
@@ -1468,14 +1506,18 @@ function createComponent(dom, componentName) {
1468
1506
  class DOM {
1469
1507
  /**
1470
1508
  * @param {Document} document
1509
+ * @param {HTMLElement} rootEl
1471
1510
  */
1472
- constructor(document) {
1511
+ constructor(document, rootEl) {
1473
1512
  /** @type {Document} */
1474
1513
  this._document = document;
1475
1514
  /** @type {string} */
1476
1515
  this._lighthouseChannel = 'unknown';
1477
1516
  /** @type {Map<string, DocumentFragment>} */
1478
1517
  this._componentCache = new Map();
1518
+ /** @type {HTMLElement} */
1519
+ // For legacy Report API users, this'll be undefined, but set in renderReport
1520
+ this.rootEl = rootEl;
1479
1521
  }
1480
1522
 
1481
1523
  /**
@@ -1517,6 +1559,15 @@ class DOM {
1517
1559
  return this._document.createDocumentFragment();
1518
1560
  }
1519
1561
 
1562
+ /**
1563
+ * @param {string} data
1564
+ * @return {!Node}
1565
+ */
1566
+ createTextNode(data) {
1567
+ return this._document.createTextNode(data);
1568
+ }
1569
+
1570
+
1520
1571
  /**
1521
1572
  * @template {string} T
1522
1573
  * @param {Element} parentElem
@@ -1657,6 +1708,8 @@ class DOM {
1657
1708
  }
1658
1709
 
1659
1710
  /**
1711
+ * ONLY use if `dom.rootEl` isn't sufficient for your needs. `dom.rootEl` is preferred
1712
+ * for all scoping, because a document can have multiple reports within it.
1660
1713
  * @return {Document}
1661
1714
  */
1662
1715
  document() {
@@ -1712,6 +1765,25 @@ class DOM {
1712
1765
  const event = new CustomEvent(name, detail ? {detail} : undefined);
1713
1766
  target.dispatchEvent(event);
1714
1767
  }
1768
+
1769
+ /**
1770
+ * Downloads a file (blob) using a[download].
1771
+ * @param {Blob|File} blob The file to save.
1772
+ * @param {string} filename
1773
+ */
1774
+ saveFile(blob, filename) {
1775
+ const ext = blob.type.match('json') ? '.json' : '.html';
1776
+
1777
+ const a = this.createElement('a');
1778
+ a.download = `${filename}${ext}`;
1779
+ this.safelySetBlobHref(a, blob);
1780
+ this._document.body.appendChild(a); // Firefox requires anchor to be in the DOM.
1781
+ a.click();
1782
+
1783
+ // cleanup.
1784
+ this._document.body.removeChild(a);
1785
+ setTimeout(() => URL.revokeObjectURL(a.href), 500);
1786
+ }
1715
1787
  }
1716
1788
 
1717
1789
  /**
@@ -1841,7 +1913,7 @@ class CategoryRenderer {
1841
1913
  const warningsEl = this.dom.createChildOf(summaryEl, 'div', 'lh-warnings');
1842
1914
  this.dom.createChildOf(warningsEl, 'span').textContent = strings.warningHeader;
1843
1915
  if (warnings.length === 1) {
1844
- warningsEl.appendChild(this.dom.document().createTextNode(warnings.join('')));
1916
+ warningsEl.appendChild(this.dom.createTextNode(warnings.join('')));
1845
1917
  } else {
1846
1918
  const warningsUl = this.dom.createChildOf(warningsEl, 'ul');
1847
1919
  for (const warning of warnings) {
@@ -1852,6 +1924,35 @@ class CategoryRenderer {
1852
1924
  return auditEl;
1853
1925
  }
1854
1926
 
1927
+ /**
1928
+ * Inject the final screenshot next to the score gauge of the first category (likely Performance)
1929
+ * @param {HTMLElement} categoriesEl
1930
+ * @param {LH.ReportResult['audits']} audits
1931
+ * @param {Element} scoreScaleEl
1932
+ */
1933
+ injectFinalScreenshot(categoriesEl, audits, scoreScaleEl) {
1934
+ const audit = audits['final-screenshot'];
1935
+ if (!audit || audit.scoreDisplayMode === 'error') return null;
1936
+ if (!audit.details || audit.details.type !== 'screenshot') return null;
1937
+
1938
+ const imgEl = this.dom.createElement('img', 'lh-final-ss-image');
1939
+ const finalScreenshotDataUri = audit.details.data;
1940
+ imgEl.src = finalScreenshotDataUri;
1941
+ imgEl.alt = audit.title;
1942
+
1943
+ const firstCatHeaderEl = this.dom.find('.lh-category .lh-category-header', categoriesEl);
1944
+ const leftColEl = this.dom.createElement('div', 'lh-category-headercol');
1945
+ const separatorEl = this.dom.createElement('div',
1946
+ 'lh-category-headercol lh-category-headercol--separator');
1947
+ const rightColEl = this.dom.createElement('div', 'lh-category-headercol');
1948
+
1949
+ leftColEl.append(...firstCatHeaderEl.childNodes);
1950
+ leftColEl.append(scoreScaleEl);
1951
+ rightColEl.append(imgEl);
1952
+ firstCatHeaderEl.append(leftColEl, separatorEl, rightColEl);
1953
+ firstCatHeaderEl.classList.add('lh-category-header__finalscreenshot');
1954
+ }
1955
+
1855
1956
  /**
1856
1957
  * @return {Element}
1857
1958
  */
@@ -1901,7 +2002,7 @@ class CategoryRenderer {
1901
2002
  * Renders the group container for a group of audits. Individual audit elements can be added
1902
2003
  * directly to the returned element.
1903
2004
  * @param {LH.Result.ReportGroup} group
1904
- * @return {Element}
2005
+ * @return {[Element, Element | null]}
1905
2006
  */
1906
2007
  renderAuditGroup(group) {
1907
2008
  const groupEl = this.dom.createElement('div', 'lh-audit-group');
@@ -1910,14 +2011,16 @@ class CategoryRenderer {
1910
2011
 
1911
2012
  this.dom.createChildOf(auditGroupHeader, 'span', 'lh-audit-group__title')
1912
2013
  .textContent = group.title;
2014
+ groupEl.appendChild(auditGroupHeader);
2015
+
2016
+ let footerEl = null;
1913
2017
  if (group.description) {
1914
- const descriptionEl = this.dom.convertMarkdownLinkSnippets(group.description);
1915
- descriptionEl.classList.add('lh-audit-group__description');
1916
- auditGroupHeader.appendChild(descriptionEl);
2018
+ footerEl = this.dom.convertMarkdownLinkSnippets(group.description);
2019
+ footerEl.classList.add('lh-audit-group__description', 'lh-audit-group__footer');
2020
+ groupEl.appendChild(footerEl);
1917
2021
  }
1918
- groupEl.appendChild(auditGroupHeader);
1919
2022
 
1920
- return groupEl;
2023
+ return [groupEl, footerEl];
1921
2024
  }
1922
2025
 
1923
2026
  /**
@@ -1957,9 +2060,9 @@ class CategoryRenderer {
1957
2060
 
1958
2061
  // Push grouped audits as a group.
1959
2062
  const groupDef = groupDefinitions[groupId];
1960
- const auditGroupElem = this.renderAuditGroup(groupDef);
2063
+ const [auditGroupElem, auditGroupFooterEl] = this.renderAuditGroup(groupDef);
1961
2064
  for (const auditRef of groupAuditRefs) {
1962
- auditGroupElem.appendChild(this.renderAudit(auditRef));
2065
+ auditGroupElem.insertBefore(this.renderAudit(auditRef), auditGroupFooterEl);
1963
2066
  }
1964
2067
  auditGroupElem.classList.add(`lh-audit-group--${groupId}`);
1965
2068
  auditElements.push(auditGroupElem);
@@ -1997,17 +2100,9 @@ class CategoryRenderer {
1997
2100
  clumpElement.setAttribute('open', '');
1998
2101
  }
1999
2102
 
2000
- const summaryInnerEl = this.dom.find('div.lh-audit-group__summary', clumpElement);
2001
- summaryInnerEl.appendChild(this._createChevron());
2002
-
2003
2103
  const headerEl = this.dom.find('.lh-audit-group__header', clumpElement);
2004
2104
  const title = this._clumpTitles[clumpId];
2005
2105
  this.dom.find('.lh-audit-group__title', headerEl).textContent = title;
2006
- if (description) {
2007
- const descriptionEl = this.dom.convertMarkdownLinkSnippets(description);
2008
- descriptionEl.classList.add('lh-audit-group__description');
2009
- headerEl.appendChild(descriptionEl);
2010
- }
2011
2106
 
2012
2107
  const itemCountEl = this.dom.find('.lh-audit-group__itemcount', clumpElement);
2013
2108
  itemCountEl.textContent = `(${auditRefs.length})`;
@@ -2016,8 +2111,18 @@ class CategoryRenderer {
2016
2111
  const auditElements = auditRefs.map(this.renderAudit.bind(this));
2017
2112
  clumpElement.append(...auditElements);
2018
2113
 
2114
+ const el = this.dom.find('.lh-audit-group', clumpComponent);
2115
+ if (description) {
2116
+ const descriptionEl = this.dom.convertMarkdownLinkSnippets(description);
2117
+ descriptionEl.classList.add('lh-audit-group__description', 'lh-audit-group__footer');
2118
+ el.appendChild(descriptionEl);
2119
+ }
2120
+
2121
+ this.dom.find('.lh-clump-toggletext--show', el).textContent = Util.i18n.strings.show;
2122
+ this.dom.find('.lh-clump-toggletext--hide', el).textContent = Util.i18n.strings.hide;
2123
+
2019
2124
  clumpElement.classList.add(`lh-clump--${clumpId.toLowerCase()}`);
2020
- return clumpElement;
2125
+ return el;
2021
2126
  }
2022
2127
 
2023
2128
  /**
@@ -2027,7 +2132,7 @@ class CategoryRenderer {
2027
2132
  * @return {DocumentFragment}
2028
2133
  */
2029
2134
  renderCategoryScore(category, groupDefinitions, options) {
2030
- if (options && (options.gatherMode === 'snapshot' || options.gatherMode === 'timespan')) {
2135
+ if (options && Util.shouldDisplayAsFraction(options.gatherMode)) {
2031
2136
  return this.renderCategoryFraction(category);
2032
2137
  }
2033
2138
  return this.renderScoreGauge(category, groupDefinitions);
@@ -2041,7 +2146,6 @@ class CategoryRenderer {
2041
2146
  renderScoreGauge(category, groupDefinitions) { // eslint-disable-line no-unused-vars
2042
2147
  const tmpl = this.dom.createComponent('gauge');
2043
2148
  const wrapper = this.dom.find('a.lh-gauge__wrapper', tmpl);
2044
- this.dom.safelySetHref(wrapper, `#${category.id}`);
2045
2149
 
2046
2150
  if (Util.isPluginCategory(category.id)) {
2047
2151
  wrapper.classList.add('lh-gauge__wrapper--plugin');
@@ -2082,21 +2186,13 @@ class CategoryRenderer {
2082
2186
  renderCategoryFraction(category) {
2083
2187
  const tmpl = this.dom.createComponent('fraction');
2084
2188
  const wrapper = this.dom.find('a.lh-fraction__wrapper', tmpl);
2085
- this.dom.safelySetHref(wrapper, `#${category.id}`);
2086
2189
 
2087
- const numAudits = category.auditRefs.length;
2190
+ const {numPassed, numPassableAudits, totalWeight} = Util.calculateCategoryFraction(category);
2088
2191
 
2089
- let numPassed = 0;
2090
- let totalWeight = 0;
2091
- for (const auditRef of category.auditRefs) {
2092
- totalWeight += auditRef.weight;
2093
- if (Util.showAsPassed(auditRef.result)) numPassed++;
2094
- }
2095
-
2096
- const fraction = numPassed / numAudits;
2192
+ const fraction = numPassed / numPassableAudits;
2097
2193
  const content = this.dom.find('.lh-fraction__content', tmpl);
2098
2194
  const text = this.dom.createElement('span');
2099
- text.textContent = `${numPassed}/${numAudits}`;
2195
+ text.textContent = `${numPassed}/${numPassableAudits}`;
2100
2196
  content.appendChild(text);
2101
2197
 
2102
2198
  let rating = Util.calculateRating(fraction);
@@ -2195,12 +2291,12 @@ class CategoryRenderer {
2195
2291
  * ⋮
2196
2292
  * @param {LH.ReportResult.Category} category
2197
2293
  * @param {Object<string, LH.Result.ReportGroup>=} groupDefinitions
2198
- * @param {{environment?: 'PSI', gatherMode: LH.Result.GatherMode}=} options
2294
+ * @param {{gatherMode: LH.Result.GatherMode}=} options
2199
2295
  * @return {Element}
2200
2296
  */
2201
2297
  render(category, groupDefinitions = {}, options) {
2202
2298
  const element = this.dom.createElement('div', 'lh-category');
2203
- this.createPermalinkSpan(element, category.id);
2299
+ element.id = category.id;
2204
2300
  element.appendChild(this.renderCategoryHeader(category, groupDefinitions, options));
2205
2301
 
2206
2302
  // Top level clumps for audits, in order they will appear in the report.
@@ -2220,6 +2316,13 @@ class CategoryRenderer {
2220
2316
  clumps.set(clumpId, clump);
2221
2317
  }
2222
2318
 
2319
+ // Sort audits by weight.
2320
+ for (const auditRefs of clumps.values()) {
2321
+ auditRefs.sort((a, b) => {
2322
+ return b.weight - a.weight;
2323
+ });
2324
+ }
2325
+
2223
2326
  // Render each clump.
2224
2327
  for (const [clumpId, auditRefs] of clumps) {
2225
2328
  if (auditRefs.length === 0) continue;
@@ -2238,16 +2341,6 @@ class CategoryRenderer {
2238
2341
 
2239
2342
  return element;
2240
2343
  }
2241
-
2242
- /**
2243
- * Create a non-semantic span used for hash navigation of categories
2244
- * @param {Element} element
2245
- * @param {string} id
2246
- */
2247
- createPermalinkSpan(element, id) {
2248
- const permalinkEl = this.dom.createChildOf(element, 'span', 'lh-permalink');
2249
- permalinkEl.id = id;
2250
- }
2251
2344
  }
2252
2345
 
2253
2346
  /**
@@ -2267,6 +2360,18 @@ class CategoryRenderer {
2267
2360
  * limitations under the License.
2268
2361
  */
2269
2362
 
2363
+ /** @typedef {import('./dom.js').DOM} DOM */
2364
+ /** @typedef {import('./details-renderer.js').DetailsRenderer} DetailsRenderer */
2365
+ /**
2366
+ * @typedef CRCSegment
2367
+ * @property {LH.Audit.Details.SimpleCriticalRequestNode[string]} node
2368
+ * @property {boolean} isLastChild
2369
+ * @property {boolean} hasChildren
2370
+ * @property {number} startTime
2371
+ * @property {number} transferSize
2372
+ * @property {boolean[]} treeMarkers
2373
+ */
2374
+
2270
2375
  class CriticalRequestChainRenderer {
2271
2376
  /**
2272
2377
  * Create render context for critical-request-chain tree display.
@@ -2432,16 +2537,6 @@ class CriticalRequestChainRenderer {
2432
2537
  // Alias b/c the name is really long.
2433
2538
  const CRCRenderer = CriticalRequestChainRenderer;
2434
2539
 
2435
- /** @typedef {{
2436
- node: LH.Audit.Details.SimpleCriticalRequestNode[string],
2437
- isLastChild: boolean,
2438
- hasChildren: boolean,
2439
- startTime: number,
2440
- transferSize: number,
2441
- treeMarkers: Array<boolean>
2442
- }} CRCSegment
2443
- */
2444
-
2445
2540
  /**
2446
2541
  * @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
2447
2542
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
@@ -2824,7 +2919,7 @@ function clamp(value, min, max) {
2824
2919
  /**
2825
2920
  * @param {Rect} rect
2826
2921
  */
2827
- function getRectCenterPoint(rect) {
2922
+ function getElementRectCenterPoint(rect) {
2828
2923
  return {
2829
2924
  x: rect.left + rect.width / 2,
2830
2925
  y: rect.top + rect.height / 2,
@@ -2841,7 +2936,7 @@ class ElementScreenshotRenderer {
2841
2936
  * @param {Size} screenshotSize
2842
2937
  */
2843
2938
  static getScreenshotPositions(elementRectSC, elementPreviewSizeSC, screenshotSize) {
2844
- const elementRectCenter = getRectCenterPoint(elementRectSC);
2939
+ const elementRectCenter = getElementRectCenterPoint(elementRectSC);
2845
2940
 
2846
2941
  // Try to center clipped region.
2847
2942
  const screenshotLeftVisibleEdge = clamp(
@@ -2908,7 +3003,7 @@ class ElementScreenshotRenderer {
2908
3003
  * @param {LH.Audit.Details.FullPageScreenshot['screenshot']} screenshot
2909
3004
  */
2910
3005
  static installFullPageScreenshot(el, screenshot) {
2911
- el.style.setProperty('--element-screenshot-url', `url(${screenshot.data})`);
3006
+ el.style.setProperty('--element-screenshot-url', `url('${screenshot.data}')`);
2912
3007
  }
2913
3008
 
2914
3009
  /**
@@ -2916,14 +3011,14 @@ class ElementScreenshotRenderer {
2916
3011
  * @param {InstallOverlayFeatureParams} opts
2917
3012
  */
2918
3013
  static installOverlayFeature(opts) {
2919
- const {dom, reportEl, overlayContainerEl, fullPageScreenshot} = opts;
3014
+ const {dom, rootEl, overlayContainerEl, fullPageScreenshot} = opts;
2920
3015
  const screenshotOverlayClass = 'lh-screenshot-overlay--enabled';
2921
3016
  // Don't install the feature more than once.
2922
- if (reportEl.classList.contains(screenshotOverlayClass)) return;
2923
- reportEl.classList.add(screenshotOverlayClass);
3017
+ if (rootEl.classList.contains(screenshotOverlayClass)) return;
3018
+ rootEl.classList.add(screenshotOverlayClass);
2924
3019
 
2925
3020
  // Add a single listener to the provided element to handle all clicks within (event delegation).
2926
- reportEl.addEventListener('click', e => {
3021
+ rootEl.addEventListener('click', e => {
2927
3022
  const target = /** @type {?HTMLElement} */ (e.target);
2928
3023
  if (!target) return;
2929
3024
  // Only activate the overlay for clicks on the screenshot *preview* of an element, not the full-size too.
@@ -3994,53 +4089,75 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
3994
4089
  return url.href;
3995
4090
  }
3996
4091
 
4092
+ /**
4093
+ * For performance, audits with no group should be a diagnostic or opportunity.
4094
+ * The audit details type will determine which of the two groups an audit is in.
4095
+ *
4096
+ * @param {LH.ReportResult.AuditRef} audit
4097
+ * @return {'load-opportunity'|'diagnostic'|null}
4098
+ */
4099
+ _classifyPerformanceAudit(audit) {
4100
+ if (audit.group) return null;
4101
+ if (audit.result.details && audit.result.details.type === 'opportunity') {
4102
+ return 'load-opportunity';
4103
+ }
4104
+ return 'diagnostic';
4105
+ }
4106
+
3997
4107
  /**
3998
4108
  * @param {LH.ReportResult.Category} category
3999
4109
  * @param {Object<string, LH.Result.ReportGroup>} groups
4000
- * @param {{gatherMode: LH.Result.GatherMode, environment?: 'PSI'}=} options
4110
+ * @param {{gatherMode: LH.Result.GatherMode}=} options
4001
4111
  * @return {Element}
4002
4112
  * @override
4003
4113
  */
4004
4114
  render(category, groups, options) {
4005
4115
  const strings = Util.i18n.strings;
4006
4116
  const element = this.dom.createElement('div', 'lh-category');
4007
- if (options && options.environment === 'PSI') {
4008
- const gaugeEl = this.dom.createElement('div', 'lh-score__gauge');
4009
- gaugeEl.appendChild(this.renderCategoryScore(category, groups, options));
4010
- element.appendChild(gaugeEl);
4011
- } else {
4012
- this.createPermalinkSpan(element, category.id);
4013
- element.appendChild(this.renderCategoryHeader(category, groups, options));
4014
- }
4117
+ element.id = category.id;
4118
+ element.appendChild(this.renderCategoryHeader(category, groups, options));
4015
4119
 
4016
4120
  // Metrics.
4017
- const metricAuditsEl = this.renderAuditGroup(groups.metrics);
4018
-
4019
- // Metric descriptions toggle.
4020
- const toggleTmpl = this.dom.createComponent('metricsToggle');
4021
- const _toggleEl = this.dom.find('.lh-metrics-toggle', toggleTmpl);
4022
- metricAuditsEl.append(..._toggleEl.childNodes);
4023
-
4024
4121
  const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
4025
- const metricsBoxesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metrics-container');
4026
-
4027
- metricAudits.forEach(item => {
4028
- metricsBoxesEl.appendChild(this._renderMetric(item));
4029
- });
4122
+ if (metricAudits.length) {
4123
+ const [metricsGroupEl, metricsFooterEl] = this.renderAuditGroup(groups.metrics);
4124
+
4125
+ // Metric descriptions toggle.
4126
+ const checkboxEl = this.dom.createElement('input', 'lh-metrics-toggle__input');
4127
+ const checkboxId = `lh-metrics-toggle${Util.getUniqueSuffix()}`;
4128
+ checkboxEl.setAttribute('aria-label', 'Toggle the display of metric descriptions');
4129
+ checkboxEl.type = 'checkbox';
4130
+ checkboxEl.id = checkboxId;
4131
+ metricsGroupEl.prepend(checkboxEl);
4132
+ const metricHeaderEl = this.dom.find('.lh-audit-group__header', metricsGroupEl);
4133
+ const labelEl = this.dom.createChildOf(metricHeaderEl, 'label', 'lh-metrics-toggle__label');
4134
+ labelEl.htmlFor = checkboxId;
4135
+ const showEl = this.dom.createChildOf(labelEl, 'span', 'lh-metrics-toggle__labeltext--show');
4136
+ const hideEl = this.dom.createChildOf(labelEl, 'span', 'lh-metrics-toggle__labeltext--hide');
4137
+ showEl.textContent = Util.i18n.strings.expandView;
4138
+ hideEl.textContent = Util.i18n.strings.collapseView;
4139
+
4140
+ const metricsBoxesEl = this.dom.createElement('div', 'lh-metrics-container');
4141
+ metricsGroupEl.insertBefore(metricsBoxesEl, metricsFooterEl);
4142
+ metricAudits.forEach(item => {
4143
+ metricsBoxesEl.appendChild(this._renderMetric(item));
4144
+ });
4030
4145
 
4031
- const estValuesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metrics__disclaimer');
4032
- const disclaimerEl = this.dom.convertMarkdownLinkSnippets(strings.varianceDisclaimer);
4033
- estValuesEl.appendChild(disclaimerEl);
4146
+ const descriptionEl = this.dom.find('.lh-category-header__description', element);
4147
+ const estValuesEl = this.dom.createChildOf(descriptionEl, 'div', 'lh-metrics__disclaimer');
4148
+ const disclaimerEl = this.dom.convertMarkdownLinkSnippets(strings.varianceDisclaimer);
4149
+ estValuesEl.appendChild(disclaimerEl);
4034
4150
 
4035
- // Add link to score calculator.
4036
- const calculatorLink = this.dom.createChildOf(estValuesEl, 'a', 'lh-calclink');
4037
- calculatorLink.target = '_blank';
4038
- calculatorLink.textContent = strings.calculatorLink;
4039
- this.dom.safelySetHref(calculatorLink, this._getScoringCalculatorHref(category.auditRefs));
4151
+ // Add link to score calculator.
4152
+ const calculatorLink = this.dom.createChildOf(estValuesEl, 'a', 'lh-calclink');
4153
+ calculatorLink.target = '_blank';
4154
+ calculatorLink.textContent = strings.calculatorLink;
4155
+ this.dom.safelySetHref(calculatorLink, this._getScoringCalculatorHref(category.auditRefs));
4040
4156
 
4041
4157
 
4042
- metricAuditsEl.classList.add('lh-audit-group--metrics');
4043
- element.appendChild(metricAuditsEl);
4158
+ metricsGroupEl.classList.add('lh-audit-group--metrics');
4159
+ element.appendChild(metricsGroupEl);
4160
+ }
4044
4161
 
4045
4162
  // Filmstrip
4046
4163
  const timelineEl = this.dom.createChildOf(element, 'div', 'lh-filmstrip-container');
@@ -4054,10 +4171,10 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
4054
4171
 
4055
4172
  // Opportunities
4056
4173
  const opportunityAudits = category.auditRefs
4057
- .filter(audit => audit.group === 'load-opportunities' && !Util.showAsPassed(audit.result))
4174
+ .filter(audit => this._classifyPerformanceAudit(audit) === 'load-opportunity')
4175
+ .filter(audit => !Util.showAsPassed(audit.result))
4058
4176
  .sort((auditA, auditB) => this._getWastedMs(auditB) - this._getWastedMs(auditA));
4059
4177
 
4060
-
4061
4178
  const filterableMetrics = metricAudits.filter(a => !!a.relevantAudits);
4062
4179
  // TODO: only add if there are opportunities & diagnostics rendered.
4063
4180
  if (filterableMetrics.length) {
@@ -4070,7 +4187,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
4070
4187
  const wastedMsValues = opportunityAudits.map(audit => this._getWastedMs(audit));
4071
4188
  const maxWaste = Math.max(...wastedMsValues);
4072
4189
  const scale = Math.max(Math.ceil(maxWaste / 1000) * 1000, minimumScale);
4073
- const groupEl = this.renderAuditGroup(groups['load-opportunities']);
4190
+ const [groupEl, footerEl] = this.renderAuditGroup(groups['load-opportunities']);
4074
4191
  const tmpl = this.dom.createComponent('opportunityHeader');
4075
4192
 
4076
4193
  this.dom.find('.lh-load-opportunity__col--one', tmpl).textContent =
@@ -4079,15 +4196,17 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
4079
4196
  strings.opportunitySavingsColumnLabel;
4080
4197
 
4081
4198
  const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
4082
- groupEl.appendChild(headerEl);
4083
- opportunityAudits.forEach(item => groupEl.appendChild(this._renderOpportunity(item, scale)));
4199
+ groupEl.insertBefore(headerEl, footerEl);
4200
+ opportunityAudits.forEach(item =>
4201
+ groupEl.insertBefore(this._renderOpportunity(item, scale), footerEl));
4084
4202
  groupEl.classList.add('lh-audit-group--load-opportunities');
4085
4203
  element.appendChild(groupEl);
4086
4204
  }
4087
4205
 
4088
4206
  // Diagnostics
4089
4207
  const diagnosticAudits = category.auditRefs
4090
- .filter(audit => audit.group === 'diagnostics' && !Util.showAsPassed(audit.result))
4208
+ .filter(audit => this._classifyPerformanceAudit(audit) === 'diagnostic')
4209
+ .filter(audit => !Util.showAsPassed(audit.result))
4091
4210
  .sort((a, b) => {
4092
4211
  const scoreA = a.result.scoreDisplayMode === 'informative' ? 100 : Number(a.result.score);
4093
4212
  const scoreB = b.result.scoreDisplayMode === 'informative' ? 100 : Number(b.result.score);
@@ -4095,16 +4214,15 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
4095
4214
  });
4096
4215
 
4097
4216
  if (diagnosticAudits.length) {
4098
- const groupEl = this.renderAuditGroup(groups['diagnostics']);
4099
- diagnosticAudits.forEach(item => groupEl.appendChild(this.renderAudit(item)));
4217
+ const [groupEl, footerEl] = this.renderAuditGroup(groups['diagnostics']);
4218
+ diagnosticAudits.forEach(item => groupEl.insertBefore(this.renderAudit(item), footerEl));
4100
4219
  groupEl.classList.add('lh-audit-group--diagnostics');
4101
4220
  element.appendChild(groupEl);
4102
4221
  }
4103
4222
 
4104
4223
  // Passed audits
4105
4224
  const passedAudits = category.auditRefs
4106
- .filter(audit => (audit.group === 'load-opportunities' || audit.group === 'diagnostics') &&
4107
- Util.showAsPassed(audit.result));
4225
+ .filter(audit => this._classifyPerformanceAudit(audit) && Util.showAsPassed(audit.result));
4108
4226
 
4109
4227
  if (!passedAudits.length) return element;
4110
4228
 
@@ -4124,16 +4242,16 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
4124
4242
  const table = this.detailsRenderer.render(audit.result.details);
4125
4243
  if (table) {
4126
4244
  table.id = id;
4127
- table.classList.add('lh-audit');
4245
+ table.classList.add('lh-details', 'lh-details--budget', 'lh-audit');
4128
4246
  budgetTableEls.push(table);
4129
4247
  }
4130
4248
  }
4131
4249
  });
4132
4250
  if (budgetTableEls.length > 0) {
4133
- const budgetsGroupEl = this.renderAuditGroup(groups.budgets);
4134
- budgetTableEls.forEach(table => budgetsGroupEl.appendChild(table));
4135
- budgetsGroupEl.classList.add('lh-audit-group--budgets');
4136
- element.appendChild(budgetsGroupEl);
4251
+ const [groupEl, footerEl] = this.renderAuditGroup(groups.budgets);
4252
+ budgetTableEls.forEach(table => groupEl.insertBefore(table, footerEl));
4253
+ groupEl.classList.add('lh-audit-group--budgets');
4254
+ element.appendChild(groupEl);
4137
4255
  }
4138
4256
 
4139
4257
  return element;
@@ -4232,7 +4350,7 @@ class PwaCategoryRenderer extends CategoryRenderer {
4232
4350
  */
4233
4351
  render(category, groupDefinitions = {}) {
4234
4352
  const categoryElem = this.dom.createElement('div', 'lh-category');
4235
- this.createPermalinkSpan(categoryElem, category.id);
4353
+ categoryElem.id = category.id;
4236
4354
  categoryElem.appendChild(this.renderCategoryHeader(category, groupDefinitions));
4237
4355
 
4238
4356
  const auditRefs = category.auditRefs;
@@ -4252,16 +4370,6 @@ class PwaCategoryRenderer extends CategoryRenderer {
4252
4370
  return categoryElem;
4253
4371
  }
4254
4372
 
4255
- /**
4256
- * Alias for backcompat.
4257
- * @param {LH.ReportResult.Category} category
4258
- * @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
4259
- * @return {DocumentFragment}
4260
- */
4261
- renderScoreGauge(category, groupDefinitions) {
4262
- return this.renderCategoryScore(category, groupDefinitions);
4263
- }
4264
-
4265
4373
  /**
4266
4374
  * @param {LH.ReportResult.Category} category
4267
4375
  * @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
@@ -4275,7 +4383,6 @@ class PwaCategoryRenderer extends CategoryRenderer {
4275
4383
 
4276
4384
  const tmpl = this.dom.createComponent('gaugePwa');
4277
4385
  const wrapper = this.dom.find('a.lh-gauge--pwa__wrapper', tmpl);
4278
- this.dom.safelySetHref(wrapper, `#${category.id}`);
4279
4386
 
4280
4387
  // Correct IDs in case multiple instances end up in the page.
4281
4388
  const svgRoot = tmpl.querySelector('svg');
@@ -4428,22 +4535,43 @@ class ReportRenderer {
4428
4535
  constructor(dom) {
4429
4536
  /** @type {DOM} */
4430
4537
  this._dom = dom;
4538
+ /** @type {LH.Renderer.Options} */
4539
+ this._opts = {};
4431
4540
  }
4432
4541
 
4433
4542
  /**
4434
4543
  * @param {LH.Result} lhr
4435
- * @param {Element} container Parent element to render the report into.
4544
+ * @param {HTMLElement?} rootEl Report root element containing the report
4545
+ * @param {LH.Renderer.Options=} opts
4436
4546
  * @return {!Element}
4437
4547
  */
4438
- renderReport(lhr, container) {
4548
+ renderReport(lhr, rootEl, opts) {
4549
+ // Allow legacy report rendering API
4550
+ if (!this._dom.rootEl && rootEl) {
4551
+ console.warn('Please adopt the new report API in renderer/api.js.');
4552
+ const closestRoot = rootEl.closest('.lh-root');
4553
+ if (closestRoot) {
4554
+ this._dom.rootEl = /** @type {HTMLElement} */ (closestRoot);
4555
+ } else {
4556
+ rootEl.classList.add('lh-root', 'lh-vars');
4557
+ this._dom.rootEl = rootEl;
4558
+ }
4559
+ } else if (this._dom.rootEl && rootEl) {
4560
+ // Handle legacy flow-report case
4561
+ this._dom.rootEl = rootEl;
4562
+ }
4563
+ if (opts) {
4564
+ this._opts = opts;
4565
+ }
4566
+
4439
4567
  this._dom.setLighthouseChannel(lhr.configSettings.channel || 'unknown');
4440
4568
 
4441
4569
  const report = Util.prepareReportResult(lhr);
4442
4570
 
4443
- container.textContent = ''; // Remove previous report.
4444
- container.appendChild(this._renderReport(report));
4571
+ this._dom.rootEl.textContent = ''; // Remove previous report.
4572
+ this._dom.rootEl.appendChild(this._renderReport(report));
4445
4573
 
4446
- return container;
4574
+ return this._dom.rootEl;
4447
4575
  }
4448
4576
 
4449
4577
  /**
@@ -4477,44 +4605,64 @@ class ReportRenderer {
4477
4605
  _renderReportFooter(report) {
4478
4606
  const footer = this._dom.createComponent('footer');
4479
4607
 
4480
- const env = this._dom.find('.lh-env__items', footer);
4481
- env.id = 'runtime-settings';
4482
- this._dom.find('.lh-env__title', footer).textContent = Util.i18n.strings.runtimeSettingsTitle;
4483
-
4484
- const envValues = Util.getEnvironmentDisplayValues(report.configSettings || {});
4485
- const runtimeValues = [
4486
- {name: Util.i18n.strings.runtimeSettingsUrl, description: report.finalUrl},
4487
- {name: Util.i18n.strings.runtimeSettingsFetchTime,
4488
- description: Util.i18n.formatDateTime(report.fetchTime)},
4489
- ...envValues,
4490
- {name: Util.i18n.strings.runtimeSettingsChannel, description: report.configSettings.channel},
4491
- {name: Util.i18n.strings.runtimeSettingsUA, description: report.userAgent},
4492
- {name: Util.i18n.strings.runtimeSettingsUANetwork, description: report.environment &&
4493
- report.environment.networkUserAgent},
4494
- {name: Util.i18n.strings.runtimeSettingsBenchmark, description: report.environment &&
4495
- report.environment.benchmarkIndex.toFixed(0)},
4496
- ];
4497
- if (report.environment.credits && report.environment.credits['axe-core']) {
4498
- runtimeValues.push({
4499
- name: Util.i18n.strings.runtimeSettingsAxeVersion,
4500
- description: report.environment.credits['axe-core'],
4501
- });
4502
- }
4503
-
4504
- for (const runtime of runtimeValues) {
4505
- if (!runtime.description) continue;
4506
-
4507
- const item = this._dom.createComponent('envItem');
4508
- this._dom.find('.lh-env__name', item).textContent = runtime.name;
4509
- this._dom.find('.lh-env__description', item).textContent = runtime.description;
4510
- env.appendChild(item);
4511
- }
4608
+ this._renderMetaBlock(report, footer);
4512
4609
 
4513
4610
  this._dom.find('.lh-footer__version_issue', footer).textContent = Util.i18n.strings.footerIssue;
4514
4611
  this._dom.find('.lh-footer__version', footer).textContent = report.lighthouseVersion;
4515
4612
  return footer;
4516
4613
  }
4517
4614
 
4615
+ /**
4616
+ * @param {LH.ReportResult} report
4617
+ * @param {DocumentFragment} footer
4618
+ */
4619
+ _renderMetaBlock(report, footer) {
4620
+ const envValues = Util.getEmulationDescriptions(report.configSettings || {});
4621
+
4622
+
4623
+ const match = report.userAgent.match(/(\w*Chrome\/[\d.]+)/); // \w* to include 'HeadlessChrome'
4624
+ const chromeVer = Array.isArray(match)
4625
+ ? match[1].replace('/', ' ').replace('Chrome', 'Chromium')
4626
+ : 'Chromium';
4627
+ const channel = report.configSettings.channel;
4628
+ const benchmarkIndex = report.environment.benchmarkIndex.toFixed(0);
4629
+ const axeVersion = report.environment.credits?.['axe-core'];
4630
+
4631
+ // [CSS icon class, textContent, tooltipText]
4632
+ const metaItems = [
4633
+ ['date',
4634
+ `Captured at ${Util.i18n.formatDateTime(report.fetchTime)}`],
4635
+ ['devices',
4636
+ `${envValues.deviceEmulation} with Lighthouse ${report.lighthouseVersion}`,
4637
+ `${Util.i18n.strings.runtimeSettingsBenchmark}: ${benchmarkIndex}` +
4638
+ `\n${Util.i18n.strings.runtimeSettingsCPUThrottling}: ${envValues.cpuThrottling}` +
4639
+ (axeVersion ? `\n${Util.i18n.strings.runtimeSettingsAxeVersion}: ${axeVersion}` : '')],
4640
+ ['samples-one',
4641
+ Util.i18n.strings.runtimeSingleLoad,
4642
+ Util.i18n.strings.runtimeSingleLoadTooltip],
4643
+ ['stopwatch',
4644
+ Util.i18n.strings.runtimeAnalysisWindow],
4645
+ ['networkspeed',
4646
+ `${envValues.summary}`,
4647
+ `${Util.i18n.strings.runtimeSettingsNetworkThrottling}: ${envValues.networkThrottling}`],
4648
+ ['chrome',
4649
+ `Using ${chromeVer}` + (channel ? ` with ${channel}` : ''),
4650
+ `${Util.i18n.strings.runtimeSettingsUANetwork}: "${report.environment.networkUserAgent}"`],
4651
+ ];
4652
+
4653
+ const metaItemsEl = this._dom.find('.lh-meta__items', footer);
4654
+ for (const [iconname, text, tooltip] of metaItems) {
4655
+ const itemEl = this._dom.createChildOf(metaItemsEl, 'li', 'lh-meta__item');
4656
+ itemEl.textContent = text;
4657
+ if (tooltip) {
4658
+ itemEl.classList.add('lh-tooltip-boundary');
4659
+ const tooltipEl = this._dom.createChildOf(itemEl, 'div', 'lh-tooltip');
4660
+ tooltipEl.textContent = tooltip;
4661
+ }
4662
+ itemEl.classList.add('lh-report-icon', `lh-report-icon--${iconname}`);
4663
+ }
4664
+ }
4665
+
4518
4666
  /**
4519
4667
  * Returns a div with a list of top-level warnings, or an empty div if no warnings.
4520
4668
  * @param {LH.ReportResult} report
@@ -4558,13 +4706,33 @@ class ReportRenderer {
4558
4706
  {gatherMode: report.gatherMode}
4559
4707
  );
4560
4708
 
4709
+ const gaugeWrapperEl = this._dom.find('a.lh-gauge__wrapper, a.lh-fraction__wrapper',
4710
+ categoryGauge);
4711
+ if (gaugeWrapperEl) {
4712
+ this._dom.safelySetHref(gaugeWrapperEl, `#${category.id}`);
4713
+ // Handle navigation clicks by scrolling to target without changing the page's URL.
4714
+ // Why? Some report embedding clients have their own routing and updating the location.hash
4715
+ // can introduce problems. Others may have an unpredictable `<base>` URL which ensures
4716
+ // navigation to `${baseURL}#categoryid` will be unintended.
4717
+ gaugeWrapperEl.addEventListener('click', e => {
4718
+ if (!gaugeWrapperEl.matches('[href^="#"]')) return;
4719
+ const selector = gaugeWrapperEl.getAttribute('href');
4720
+ const reportRoot = this._dom.rootEl;
4721
+ if (!selector || !reportRoot) return;
4722
+ const destEl = this._dom.find(selector, reportRoot);
4723
+ e.preventDefault();
4724
+ destEl.scrollIntoView();
4725
+ });
4726
+ }
4727
+
4728
+
4561
4729
  if (Util.isPluginCategory(category.id)) {
4562
4730
  pluginGauges.push(categoryGauge);
4563
4731
  } else if (renderer.renderCategoryScore === categoryRenderer.renderCategoryScore) {
4564
4732
  // The renderer for default categories is just the default CategoryRenderer.
4565
4733
  // If the functions are equal, then renderer is an instance of CategoryRenderer.
4566
4734
  // For example, the PWA category uses PwaCategoryRenderer, which overrides
4567
- // CategoryRenderer.renderScoreGauge, so it would fail this check and be placed
4735
+ // CategoryRenderer.renderCategoryScore, so it would fail this check and be placed
4568
4736
  // in the customGauges bucket.
4569
4737
  defaultGauges.push(categoryGauge);
4570
4738
  } else {
@@ -4619,8 +4787,10 @@ class ReportRenderer {
4619
4787
  headerContainer.classList.add('lh-header--solo-category');
4620
4788
  }
4621
4789
 
4790
+ const scoreScale = this._dom.createElement('div');
4791
+ scoreScale.classList.add('lh-scorescale-wrap');
4792
+ scoreScale.append(this._dom.createComponent('scorescale'));
4622
4793
  if (scoreHeader) {
4623
- const scoreScale = this._dom.createComponent('scorescale');
4624
4794
  const scoresContainer = this._dom.find('.lh-scores-container', headerContainer);
4625
4795
  scoreHeader.append(
4626
4796
  ...this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers));
@@ -4638,7 +4808,7 @@ class ReportRenderer {
4638
4808
  for (const category of Object.values(report.categories)) {
4639
4809
  const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
4640
4810
  // .lh-category-wrapper is full-width and provides horizontal rules between categories.
4641
- // .lh-category within has the max-width: var(--report-width);
4811
+ // .lh-category within has the max-width: var(--report-content-width);
4642
4812
  const wrapper = renderer.dom.createChildOf(categories, 'div', 'lh-category-wrapper');
4643
4813
  wrapper.appendChild(renderer.render(
4644
4814
  category,
@@ -4647,10 +4817,15 @@ class ReportRenderer {
4647
4817
  ));
4648
4818
  }
4649
4819
 
4820
+ categoryRenderer.injectFinalScreenshot(categories, report.audits, scoreScale);
4821
+
4650
4822
  const reportFragment = this._dom.createFragment();
4651
- const topbarDocumentFragment = this._renderReportTopbar(report);
4823
+ reportFragment.append(this._dom.createComponent('styles'));
4824
+
4825
+ if (!this._opts.omitTopbar) {
4826
+ reportFragment.appendChild(this._renderReportTopbar(report));
4827
+ }
4652
4828
 
4653
- reportFragment.appendChild(topbarDocumentFragment);
4654
4829
  reportFragment.appendChild(reportContainer);
4655
4830
  reportContainer.appendChild(headerContainer);
4656
4831
  reportContainer.appendChild(reportSection);
@@ -4658,7 +4833,7 @@ class ReportRenderer {
4658
4833
 
4659
4834
  if (fullPageScreenshot) {
4660
4835
  ElementScreenshotRenderer.installFullPageScreenshot(
4661
- reportContainer, fullPageScreenshot.screenshot);
4836
+ this._dom.rootEl, fullPageScreenshot.screenshot);
4662
4837
  }
4663
4838
 
4664
4839
  return reportFragment;
@@ -4680,7 +4855,7 @@ class ReportRenderer {
4680
4855
  * @param {boolean} [force]
4681
4856
  */
4682
4857
  function toggleDarkTheme(dom, force) {
4683
- const el = dom.find('.lh-vars', dom.document());
4858
+ const el = dom.rootEl;
4684
4859
  // This seems unnecessary, but in DevTools, passing "undefined" as the second
4685
4860
  // parameter acts like passing "false".
4686
4861
  // https://github.com/ChromeDevTools/devtools-frontend/blob/dd6a6d4153647c2a4203c327c595692c5e0a4256/front_end/dom_extension/DOMExtension.js#L809-L819
@@ -4937,11 +5112,11 @@ class DropDownMenu {
4937
5112
  * @param {function(MouseEvent): any} menuClickHandler
4938
5113
  */
4939
5114
  setup(menuClickHandler) {
4940
- this._toggleEl = this._dom.find('button.lh-tools__button', this._dom.document());
5115
+ this._toggleEl = this._dom.find('.lh-topbar button.lh-tools__button', this._dom.rootEl);
4941
5116
  this._toggleEl.addEventListener('click', this.onToggleClick);
4942
5117
  this._toggleEl.addEventListener('keydown', this.onToggleKeydown);
4943
5118
 
4944
- this._menuEl = this._dom.find('div.lh-tools__dropdown', this._dom.document());
5119
+ this._menuEl = this._dom.find('.lh-topbar div.lh-tools__dropdown', this._dom.rootEl);
4945
5120
  this._menuEl.addEventListener('keydown', this.onMenuKeydown);
4946
5121
  this._menuEl.addEventListener('click', menuClickHandler);
4947
5122
  }
@@ -5129,15 +5304,13 @@ class TopbarFeatures {
5129
5304
  this.lhr; // eslint-disable-line no-unused-expressions
5130
5305
  this._reportUIFeatures = reportUIFeatures;
5131
5306
  this._dom = dom;
5132
- /** @type {Document} */
5133
- this._document = this._dom.document();
5134
5307
  this._dropDownMenu = new DropDownMenu(this._dom);
5135
5308
  this._copyAttempt = false;
5136
5309
  /** @type {HTMLElement} */
5137
5310
  this.topbarEl; // eslint-disable-line no-unused-expressions
5138
5311
  /** @type {HTMLElement} */
5139
- this.scoreScaleEl; // eslint-disable-line no-unused-expressions
5140
- /** @type {HTMLElement} */
5312
+ this.categoriesEl; // eslint-disable-line no-unused-expressions
5313
+ /** @type {HTMLElement?} */
5141
5314
  this.stickyHeaderEl; // eslint-disable-line no-unused-expressions
5142
5315
  /** @type {HTMLElement} */
5143
5316
  this.highlightEl; // eslint-disable-line no-unused-expressions
@@ -5145,7 +5318,6 @@ class TopbarFeatures {
5145
5318
  this.onKeyUp = this.onKeyUp.bind(this);
5146
5319
  this.onCopy = this.onCopy.bind(this);
5147
5320
  this.collapseAllDetails = this.collapseAllDetails.bind(this);
5148
- this._updateStickyHeaderOnScroll = this._updateStickyHeaderOnScroll.bind(this);
5149
5321
  }
5150
5322
 
5151
5323
  /**
@@ -5153,33 +5325,15 @@ class TopbarFeatures {
5153
5325
  */
5154
5326
  enable(lhr) {
5155
5327
  this.lhr = lhr;
5156
- this._document.addEventListener('keyup', this.onKeyUp);
5157
- this._document.addEventListener('copy', this.onCopy);
5328
+ this._dom.rootEl.addEventListener('keyup', this.onKeyUp);
5329
+ this._dom.document().addEventListener('copy', this.onCopy);
5158
5330
  this._dropDownMenu.setup(this.onDropDownMenuClick);
5159
5331
  this._setUpCollapseDetailsAfterPrinting();
5160
5332
 
5161
- const topbarLogo = this._dom.find('.lh-topbar__logo', this._document);
5333
+ const topbarLogo = this._dom.find('.lh-topbar__logo', this._dom.rootEl);
5162
5334
  topbarLogo.addEventListener('click', () => toggleDarkTheme(this._dom));
5163
5335
 
5164
- // There is only a sticky header when at least 2 categories are present.
5165
- if (Object.keys(this.lhr.categories).length >= 2) {
5166
- this._setupStickyHeaderElements();
5167
- const containerEl = this._dom.find('.lh-container', this._document);
5168
- const elToAddScrollListener = this._getScrollParent(containerEl);
5169
- elToAddScrollListener.addEventListener('scroll', this._updateStickyHeaderOnScroll);
5170
-
5171
- // Use ResizeObserver where available.
5172
- // TODO: there is an issue with incorrect position numbers and, as a result, performance
5173
- // issues due to layout thrashing.
5174
- // See https://github.com/GoogleChrome/lighthouse/pull/9023/files#r288822287 for details.
5175
- // For now, limit to DevTools.
5176
- if (this._dom.isDevTools()) {
5177
- const resizeObserver = new window.ResizeObserver(this._updateStickyHeaderOnScroll);
5178
- resizeObserver.observe(containerEl);
5179
- } else {
5180
- window.addEventListener('resize', this._updateStickyHeaderOnScroll);
5181
- }
5182
- }
5336
+ this._setupStickyHeader();
5183
5337
  }
5184
5338
 
5185
5339
  /**
@@ -5217,7 +5371,7 @@ class TopbarFeatures {
5217
5371
  try {
5218
5372
  this._reportUIFeatures._saveFile(new Blob([htmlStr], {type: 'text/html'}));
5219
5373
  } catch (e) {
5220
- this._dom.fireEventOn('lh-log', this._document, {
5374
+ this._dom.fireEventOn('lh-log', this._dom.document(), {
5221
5375
  cmd: 'error', msg: 'Could not export as HTML. ' + e.message,
5222
5376
  });
5223
5377
  }
@@ -5257,7 +5411,7 @@ class TopbarFeatures {
5257
5411
  e.preventDefault();
5258
5412
  e.clipboardData.setData('text/plain', JSON.stringify(this.lhr, null, 2));
5259
5413
 
5260
- this._dom.fireEventOn('lh-log', this._document, {
5414
+ this._dom.fireEventOn('lh-log', this._dom.document(), {
5261
5415
  cmd: 'log', msg: 'Report JSON copied to clipboard',
5262
5416
  });
5263
5417
  }
@@ -5269,28 +5423,28 @@ class TopbarFeatures {
5269
5423
  * Copies the report JSON to the clipboard (if supported by the browser).
5270
5424
  */
5271
5425
  onCopyButtonClick() {
5272
- this._dom.fireEventOn('lh-analytics', this._document, {
5426
+ this._dom.fireEventOn('lh-analytics', this._dom.document(), {
5273
5427
  cmd: 'send',
5274
5428
  fields: {hitType: 'event', eventCategory: 'report', eventAction: 'copy'},
5275
5429
  });
5276
5430
 
5277
5431
  try {
5278
- if (this._document.queryCommandSupported('copy')) {
5432
+ if (this._dom.document().queryCommandSupported('copy')) {
5279
5433
  this._copyAttempt = true;
5280
5434
 
5281
5435
  // Note: In Safari 10.0.1, execCommand('copy') returns true if there's
5282
5436
  // a valid text selection on the page. See http://caniuse.com/#feat=clipboard.
5283
- if (!this._document.execCommand('copy')) {
5437
+ if (!this._dom.document().execCommand('copy')) {
5284
5438
  this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt.
5285
5439
 
5286
- this._dom.fireEventOn('lh-log', this._document, {
5440
+ this._dom.fireEventOn('lh-log', this._dom.document(), {
5287
5441
  cmd: 'warn', msg: 'Your browser does not support copy to clipboard.',
5288
5442
  });
5289
5443
  }
5290
5444
  }
5291
5445
  } catch (e) {
5292
5446
  this._copyAttempt = false;
5293
- this._dom.fireEventOn('lh-log', this._document, {cmd: 'log', msg: e.message});
5447
+ this._dom.fireEventOn('lh-log', this._dom.document(), {cmd: 'log', msg: e.message});
5294
5448
  }
5295
5449
  }
5296
5450
 
@@ -5311,7 +5465,7 @@ class TopbarFeatures {
5311
5465
  * open a `<details>` element.
5312
5466
  */
5313
5467
  expandAllDetails() {
5314
- const details = this._dom.findAll('.lh-categories details', this._document);
5468
+ const details = this._dom.findAll('.lh-categories details', this._dom.rootEl);
5315
5469
  details.map(detail => detail.open = true);
5316
5470
  }
5317
5471
 
@@ -5320,7 +5474,7 @@ class TopbarFeatures {
5320
5474
  * open a `<details>` element.
5321
5475
  */
5322
5476
  collapseAllDetails() {
5323
- const details = this._dom.findAll('.lh-categories details', this._document);
5477
+ const details = this._dom.findAll('.lh-categories details', this._dom.rootEl);
5324
5478
  details.map(detail => detail.open = false);
5325
5479
  }
5326
5480
 
@@ -5340,7 +5494,7 @@ class TopbarFeatures {
5340
5494
  /**
5341
5495
  * Finds the first scrollable ancestor of `element`. Falls back to the document.
5342
5496
  * @param {Element} element
5343
- * @return {Node}
5497
+ * @return {Element | Document}
5344
5498
  */
5345
5499
  _getScrollParent(element) {
5346
5500
  const {overflowY} = window.getComputedStyle(element);
@@ -5378,24 +5532,50 @@ class TopbarFeatures {
5378
5532
  }
5379
5533
  }
5380
5534
 
5381
- _setupStickyHeaderElements() {
5382
- this.topbarEl = this._dom.find('div.lh-topbar', this._document);
5383
- this.scoreScaleEl = this._dom.find('div.lh-scorescale', this._document);
5384
- this.stickyHeaderEl = this._dom.find('div.lh-sticky-header', this._document);
5535
+ _setupStickyHeader() {
5536
+ // Cache these elements to avoid qSA on each onscroll.
5537
+ this.topbarEl = this._dom.find('div.lh-topbar', this._dom.rootEl);
5538
+ this.categoriesEl = this._dom.find('div.lh-categories', this._dom.rootEl);
5539
+
5540
+ // Defer behind rAF to avoid forcing layout.
5541
+ window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
5542
+ // Only present in the DOM if it'll be used (>=2 categories)
5543
+ try {
5544
+ this.stickyHeaderEl = this._dom.find('div.lh-sticky-header', this._dom.rootEl);
5545
+ } catch {
5546
+ return;
5547
+ }
5385
5548
 
5386
- // Highlighter will be absolutely positioned at first gauge, then transformed on scroll.
5387
- this.highlightEl = this._dom.createChildOf(this.stickyHeaderEl, 'div', 'lh-highlighter');
5549
+ // Highlighter will be absolutely positioned at first gauge, then transformed on scroll.
5550
+ this.highlightEl = this._dom.createChildOf(this.stickyHeaderEl, 'div', 'lh-highlighter');
5551
+
5552
+ // Update sticky header visibility and highlight when page scrolls/resizes.
5553
+ const scrollParent = this._getScrollParent(
5554
+ this._dom.find('.lh-container', this._dom.rootEl));
5555
+ // The 'scroll' handler must be should be on {Element | Document}...
5556
+ scrollParent.addEventListener('scroll', () => this._updateStickyHeader());
5557
+ // However resizeObserver needs an element, *not* the document.
5558
+ const resizeTarget = scrollParent instanceof window.Document
5559
+ ? document.documentElement
5560
+ : scrollParent;
5561
+ new window.ResizeObserver(() => this._updateStickyHeader()).observe(resizeTarget);
5562
+ }));
5388
5563
  }
5389
5564
 
5390
- _updateStickyHeaderOnScroll() {
5391
- // Show sticky header when the score scale begins to go underneath the topbar.
5565
+ /**
5566
+ * Toggle visibility and update highlighter position
5567
+ */
5568
+ _updateStickyHeader() {
5569
+ if (!this.stickyHeaderEl) return;
5570
+
5571
+ // Show sticky header when the main 5 gauges clear the topbar.
5392
5572
  const topbarBottom = this.topbarEl.getBoundingClientRect().bottom;
5393
- const scoreScaleTop = this.scoreScaleEl.getBoundingClientRect().top;
5394
- const showStickyHeader = topbarBottom >= scoreScaleTop;
5573
+ const categoriesTop = this.categoriesEl.getBoundingClientRect().top;
5574
+ const showStickyHeader = topbarBottom >= categoriesTop;
5395
5575
 
5396
5576
  // Highlight mini gauge when section is in view.
5397
5577
  // In view = the last category that starts above the middle of the window.
5398
- const categoryEls = Array.from(this._document.querySelectorAll('.lh-category'));
5578
+ const categoryEls = Array.from(this._dom.rootEl.querySelectorAll('.lh-category'));
5399
5579
  const categoriesAboveTheMiddle =
5400
5580
  categoryEls.filter(el => el.getBoundingClientRect().top - window.innerHeight / 2 < 0);
5401
5581
  const highlightIndex =
@@ -5425,15 +5605,14 @@ class TopbarFeatures {
5425
5605
  */
5426
5606
 
5427
5607
  /**
5428
- * Generate a filenamePrefix of hostname_YYYY-MM-DD_HH-MM-SS
5608
+ * Generate a filenamePrefix of name_YYYY-MM-DD_HH-MM-SS
5429
5609
  * Date/time uses the local timezone, however Node has unreliable ICU
5430
5610
  * support, so we must construct a YYYY-MM-DD date format manually. :/
5431
- * @param {{finalUrl: string, fetchTime: string}} lhr
5432
- * @return {string}
5611
+ * @param {string} name
5612
+ * @param {string|undefined} fetchTime
5433
5613
  */
5434
- function getFilenamePrefix(lhr) {
5435
- const hostname = new URL(lhr.finalUrl).hostname;
5436
- const date = (lhr.fetchTime && new Date(lhr.fetchTime)) || new Date();
5614
+ function getFilenamePrefix(name, fetchTime) {
5615
+ const date = fetchTime ? new Date(fetchTime) : new Date();
5437
5616
 
5438
5617
  const timeStr = date.toLocaleTimeString('en-US', {hour12: false});
5439
5618
  const dateParts = date.toLocaleDateString('en-US', {
@@ -5443,13 +5622,22 @@ function getFilenamePrefix(lhr) {
5443
5622
  dateParts.unshift(dateParts.pop());
5444
5623
  const dateStr = dateParts.join('-');
5445
5624
 
5446
- const filenamePrefix = `${hostname}_${dateStr}_${timeStr}`;
5625
+ const filenamePrefix = `${name}_${dateStr}_${timeStr}`;
5447
5626
  // replace characters that are unfriendly to filenames
5448
5627
  return filenamePrefix.replace(/[/?<>\\:*|"]/g, '-');
5449
5628
  }
5450
5629
 
5451
- var fileNamer = {getFilenamePrefix};
5452
- var fileNamer_1 = fileNamer.getFilenamePrefix;
5630
+ /**
5631
+ * Generate a filenamePrefix of hostname_YYYY-MM-DD_HH-MM-SS.
5632
+ * @param {{finalUrl: string, fetchTime: string}} lhr
5633
+ * @return {string}
5634
+ */
5635
+ function getLhrFilenamePrefix(lhr) {
5636
+ const hostname = new URL(lhr.finalUrl).hostname;
5637
+ return getFilenamePrefix(hostname, lhr.fetchTime);
5638
+ }
5639
+
5640
+ var fileNamer = {getLhrFilenamePrefix, getFilenamePrefix};
5453
5641
 
5454
5642
  /**
5455
5643
  * @license
@@ -5478,16 +5666,17 @@ function getTableRows(tableEl) {
5478
5666
  class ReportUIFeatures {
5479
5667
  /**
5480
5668
  * @param {DOM} dom
5669
+ * @param {LH.Renderer.Options} opts
5481
5670
  */
5482
- constructor(dom) {
5671
+ constructor(dom, opts = {}) {
5483
5672
  /** @type {LH.Result} */
5484
5673
  this.json; // eslint-disable-line no-unused-expressions
5485
5674
  /** @type {DOM} */
5486
5675
  this._dom = dom;
5487
- /** @type {Document} */
5488
- this._document = this._dom.document();
5489
- this._topbar = new TopbarFeatures(this, dom);
5490
5676
 
5677
+ this._opts = opts;
5678
+
5679
+ this._topbar = opts.omitTopbar ? null : new TopbarFeatures(this, dom);
5491
5680
  this.onMediaQueryChange = this.onMediaQueryChange.bind(this);
5492
5681
  }
5493
5682
 
@@ -5499,16 +5688,19 @@ class ReportUIFeatures {
5499
5688
  initFeatures(lhr) {
5500
5689
  this.json = lhr;
5501
5690
 
5502
- this._topbar.enable(lhr);
5503
- this._topbar.resetUIState();
5691
+ if (this._topbar) {
5692
+ this._topbar.enable(lhr);
5693
+ this._topbar.resetUIState();
5694
+ }
5504
5695
  this._setupMediaQueryListeners();
5505
5696
  this._setupThirdPartyFilter();
5506
- this._setupElementScreenshotOverlay(this._dom.find('.lh-container', this._document));
5697
+ this._setupElementScreenshotOverlay(this._dom.rootEl);
5507
5698
 
5508
5699
  let turnOffTheLights = false;
5509
5700
  // Do not query the system preferences for DevTools - DevTools should only apply dark theme
5510
5701
  // if dark is selected in the settings panel.
5511
- if (!this._dom.isDevTools() && window.matchMedia('(prefers-color-scheme: dark)').matches) {
5702
+ const disableDarkMode = this._dom.isDevTools() || this._opts.disableAutoDarkModeAndFireworks;
5703
+ if (!disableDarkMode && window.matchMedia('(prefers-color-scheme: dark)').matches) {
5512
5704
  turnOffTheLights = true;
5513
5705
  }
5514
5706
 
@@ -5532,7 +5724,7 @@ class ReportUIFeatures {
5532
5724
  const hasMetricError = lhr.categories.performance && lhr.categories.performance.auditRefs
5533
5725
  .some(audit => Boolean(audit.group === 'metrics' && lhr.audits[audit.id].errorMessage));
5534
5726
  if (hasMetricError) {
5535
- const toggleInputEl = this._dom.find('input.lh-metrics-toggle__input', this._document);
5727
+ const toggleInputEl = this._dom.find('input.lh-metrics-toggle__input', this._dom.rootEl);
5536
5728
  toggleInputEl.checked = true;
5537
5729
  }
5538
5730
 
@@ -5547,7 +5739,7 @@ class ReportUIFeatures {
5547
5739
  }
5548
5740
 
5549
5741
  // Fill in all i18n data.
5550
- for (const node of this._dom.findAll('[data-i18n]', this._dom.document())) {
5742
+ for (const node of this._dom.findAll('[data-i18n]', this._dom.rootEl)) {
5551
5743
  // These strings are guaranteed to (at least) have a default English string in Util.UIStrings,
5552
5744
  // so this cannot be undefined as long as `report-ui-features.data-i18n` test passes.
5553
5745
  const i18nKey = node.getAttribute('data-i18n');
@@ -5557,18 +5749,15 @@ class ReportUIFeatures {
5557
5749
  }
5558
5750
 
5559
5751
  /**
5560
- * @param {{container?: Element, text: string, icon?: string, onClick: () => void}} opts
5752
+ * @param {{text: string, icon?: string, onClick: () => void}} opts
5561
5753
  */
5562
5754
  addButton(opts) {
5563
- // report-ui-features doesn't have a reference to the root report el, and PSI has
5564
- // 2 reports on the page (and not even attached to DOM when installFeatures is called..)
5565
- // so we need a container option to specify where the element should go.
5566
- const metricsEl = this._document.querySelector('.lh-audit-group--metrics');
5567
- const containerEl = opts.container || metricsEl;
5568
- if (!containerEl) return;
5755
+ // Use qSA directly to as we don't want to throw (if this element is missing).
5756
+ const metricsEl = this._dom.rootEl.querySelector('.lh-audit-group--metrics');
5757
+ if (!metricsEl) return;
5569
5758
 
5570
- let buttonsEl = containerEl.querySelector('.lh-buttons');
5571
- if (!buttonsEl) buttonsEl = this._dom.createChildOf(containerEl, 'div', 'lh-buttons');
5759
+ let buttonsEl = metricsEl.querySelector('.lh-buttons');
5760
+ if (!buttonsEl) buttonsEl = this._dom.createChildOf(metricsEl, 'div', 'lh-buttons');
5572
5761
 
5573
5762
  const classes = [
5574
5763
  'lh-button',
@@ -5588,8 +5777,10 @@ class ReportUIFeatures {
5588
5777
  * @return {string}
5589
5778
  */
5590
5779
  getReportHtml() {
5591
- this._topbar.resetUIState();
5592
- return this._document.documentElement.outerHTML;
5780
+ if (this._topbar) {
5781
+ this._topbar.resetUIState();
5782
+ }
5783
+ return `<!doctype html><body>${this._dom.rootEl.outerHTML}`;
5593
5784
  }
5594
5785
 
5595
5786
  /**
@@ -5601,7 +5792,7 @@ class ReportUIFeatures {
5601
5792
  }
5602
5793
 
5603
5794
  _enableFireworks() {
5604
- const scoresContainer = this._dom.find('.lh-scores-container', this._document);
5795
+ const scoresContainer = this._dom.find('.lh-scores-container', this._dom.rootEl);
5605
5796
  scoresContainer.classList.add('lh-score100');
5606
5797
  scoresContainer.addEventListener('click', _ => {
5607
5798
  scoresContainer.classList.toggle('lh-fireworks-paused');
@@ -5621,7 +5812,9 @@ class ReportUIFeatures {
5621
5812
  * be in their closed state (not opened) and the templates should be unstamped.
5622
5813
  */
5623
5814
  _resetUIState() {
5624
- this._topbar.resetUIState();
5815
+ if (this._topbar) {
5816
+ this._topbar.resetUIState();
5817
+ }
5625
5818
  }
5626
5819
 
5627
5820
  /**
@@ -5629,8 +5822,7 @@ class ReportUIFeatures {
5629
5822
  * @param {MediaQueryList|MediaQueryListEvent} mql
5630
5823
  */
5631
5824
  onMediaQueryChange(mql) {
5632
- const root = this._dom.find('.lh-root', this._document);
5633
- root.classList.toggle('lh-narrow', mql.matches);
5825
+ this._dom.rootEl.classList.toggle('lh-narrow', mql.matches);
5634
5826
  }
5635
5827
 
5636
5828
  _setupThirdPartyFilter() {
@@ -5647,7 +5839,7 @@ class ReportUIFeatures {
5647
5839
  ];
5648
5840
 
5649
5841
  // Get all tables with a text url column.
5650
- const tables = Array.from(this._document.querySelectorAll('table.lh-table'));
5842
+ const tables = Array.from(this._dom.rootEl.querySelectorAll('table.lh-table'));
5651
5843
  const tablesWithUrls = tables
5652
5844
  .filter(el =>
5653
5845
  el.querySelector('td.lh-table-column--url, td.lh-table-column--source-location'))
@@ -5694,10 +5886,9 @@ class ReportUIFeatures {
5694
5886
  const allThirdParty = thirdPartyRows.length === rowEls.length;
5695
5887
  const allFirstParty = !thirdPartyRows.length;
5696
5888
 
5697
- // If all or none of the rows are 3rd party, disable the checkbox.
5889
+ // If all or none of the rows are 3rd party, hide the control.
5698
5890
  if (allThirdParty || allFirstParty) {
5699
- filterInput.disabled = true;
5700
- filterInput.checked = allThirdParty;
5891
+ this._dom.find('div.lh-3p-filter', filterTemplate).hidden = true;
5701
5892
  }
5702
5893
 
5703
5894
  // Add checkbox to the DOM.
@@ -5714,9 +5905,9 @@ class ReportUIFeatures {
5714
5905
  }
5715
5906
 
5716
5907
  /**
5717
- * @param {Element} el
5908
+ * @param {Element} rootEl
5718
5909
  */
5719
- _setupElementScreenshotOverlay(el) {
5910
+ _setupElementScreenshotOverlay(rootEl) {
5720
5911
  const fullPageScreenshot =
5721
5912
  this.json.audits['full-page-screenshot'] &&
5722
5913
  this.json.audits['full-page-screenshot'].details &&
@@ -5726,8 +5917,8 @@ class ReportUIFeatures {
5726
5917
 
5727
5918
  ElementScreenshotRenderer.installOverlayFeature({
5728
5919
  dom: this._dom,
5729
- reportEl: el,
5730
- overlayContainerEl: el,
5920
+ rootEl: rootEl,
5921
+ overlayContainerEl: rootEl,
5731
5922
  fullPageScreenshot,
5732
5923
  });
5733
5924
  }
@@ -5762,27 +5953,42 @@ class ReportUIFeatures {
5762
5953
  }
5763
5954
 
5764
5955
  /**
5765
- * Downloads a file (blob) using a[download].
5766
- * @param {Blob|File} blob The file to save.
5956
+ * DevTools uses its own file manager to download files, so it redefines this function.
5957
+ * Wrapper is necessary so DevTools can still override this function.
5958
+ *
5959
+ * @param {Blob|File} blob
5767
5960
  */
5768
5961
  _saveFile(blob) {
5769
- const filename = fileNamer_1({
5770
- finalUrl: this.json.finalUrl,
5771
- fetchTime: this.json.fetchTime,
5772
- });
5962
+ const filename = fileNamer.getLhrFilenamePrefix(this.json);
5963
+ this._dom.saveFile(blob, filename);
5964
+ }
5965
+ }
5773
5966
 
5774
- const ext = blob.type.match('json') ? '.json' : '.html';
5967
+ /**
5968
+ * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
5969
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
5970
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5971
+ */
5775
5972
 
5776
- const a = this._dom.createElement('a');
5777
- a.download = `${filename}${ext}`;
5778
- this._dom.safelySetBlobHref(a, blob);
5779
- this._document.body.appendChild(a); // Firefox requires anchor to be in the DOM.
5780
- a.click();
5973
+ /**
5974
+ * @param {LH.Result} lhr
5975
+ * @param {LH.Renderer.Options} opts
5976
+ * @return {HTMLElement}
5977
+ */
5978
+ function renderReport(lhr, opts = {}) {
5979
+ const rootEl = document.createElement('article');
5980
+ rootEl.classList.add('lh-root', 'lh-vars');
5781
5981
 
5782
- // cleanup.
5783
- this._document.body.removeChild(a);
5784
- setTimeout(() => URL.revokeObjectURL(a.href), 500);
5785
- }
5982
+ const dom = new DOM(rootEl.ownerDocument, rootEl);
5983
+ const renderer = new ReportRenderer(dom);
5984
+
5985
+ renderer.renderReport(lhr, rootEl, opts);
5986
+
5987
+ // Hook in JS features and page-level event listeners after the report
5988
+ // is in the document.
5989
+ const features = new ReportUIFeatures(dom, opts);
5990
+ features.initFeatures(lhr);
5991
+ return rootEl;
5786
5992
  }
5787
5993
 
5788
- export { DOM, ReportRenderer, ReportUIFeatures };
5994
+ export { DOM, ReportRenderer, ReportUIFeatures, renderReport };