CVE-2023-52081: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection') (CWE-74)
value: ffcss is a CLI interface to apply and configure Firefox CSS themes. Prior to 0.2.0, the function `lookupPreprocess()` is meant to apply some transformations to a string by disabling characters in the regex `[-_ .]`. However, due to the use of late Unicode normalization of type NFKD, it is possible to bypass that validation and re-introduce all the characters in the regex `[-_ .]`. The `lookupPreprocess()` can be easily bypassed with equivalent Unicode characters like U+FE4D (?), which would result in the omitted U+005F (_), for instance. The `lookupPreprocess()` function is only ever used to search for themes loosely (case insensitively, while ignoring dashes, underscores and dots), so the actual security impact is classified as low. This vulnerability is fixed in 0.2.0. There are no known workarounds.
CWE description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
CVSS v3 base score: 5.3
Published: 2023-12-28T16:16Z
Repository: https://github.com/ewen-lbh/ffcss
Language: Other
Files changed: .deliverit.yaml, .editorconfig, .github/ISSUE_TEMPLATE/adding-a-theme-to-the-registry.md, .github/PULL_REQUEST_TEMPLATE/adding-a-theme-to-the-registry.md, .github/workflows/test.yaml, .gitignore, .goreleaser.yml, CHANGELOG.md, LICENSE, Makefile, README.md, addons.go, addons_test.go, assets.go, assets_test.go, cache.go, cache_test.go, catalog.go, catalog_test.go, cmd/ffcss/USAGE, cmd/ffcss/commands.go, cmd/ffcss/main.go, cmd/ffcss/utils.go, currenttheme.go, currenttheme_test.go, datadirs.go, datadirs_test.go, download.go, download_test.go, ffcss.go, ffcss_test.go, firefoxversion.go, firefoxversion_test.go, git.go, git_test.go, go.mod, go.sum, hooks.go, hooks_test.go, installation.go, installation_test.go, manifest.go, manifest_test.go, profiles.go, profiles_test.go, testdata/catalogs/various/.yaml, testdata/catalogs/various/.yml, testdata/catalogs/various/alpenblue.yaml, testdata/catalogs/various/flyingfox.yaml, testdata/catalogs/various/montereyfox.yaml, testdata/catalogs/various/noext, testdata/catalogs/various/sometheme.yml, testdata/home/.cache/ffcss/materialfox/_/.github/ISSUE_TEMPLATE/bug_report.md, testdata/home/.cache/ffcss/materialfox/_/.gitignore, testdata/home/.cache/ffcss/materialfox/_/LICENSE, testdata/home/.cache/ffcss/materialfox/_/README.md, testdata/home/.cache/ffcss/materialfox/_/chrome/findbar/findbar.css, testdata/home/.cache/ffcss/materialfox/_/chrome/global/circle.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/global/global.css, testdata/home/.cache/ffcss/materialfox/_/chrome/global/variables.css, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/addons.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/arrow-back-1x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/arrow-back-2x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/audio-muted.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/audio.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/bookmark.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/bug.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/close.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/copy.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/cut.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/developer.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/email.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/folder.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/forget.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/globe.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/history.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/home-1x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/home-2x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/hyperlink.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/icons.css, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/incognito.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/key.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/library-1x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/library-2x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/media-blocked.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/menu-update.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/menu.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/new-tab.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/paste.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/profile-inactive.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/profile.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/refresh-1x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/refresh-2x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/screenshot.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/search.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/settings.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/share.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/star-1x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/star-2x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/stop-1x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/stop-2x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/sync.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/icons/tracking-protection.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-1x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-2x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-warning-1x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-warning-2x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/identity-icons-brand.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/info-1x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/info-2x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/naughty-shield-1x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/naughty-shield-2x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/navbar.css, testdata/home/.cache/ffcss/materialfox/_/chrome/personalbar/personalbar.css, testdata/home/.cache/ffcss/materialfox/_/chrome/popup/checkmark-18dp.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/popup/menu-right-1x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/popup/menu-right-2x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/popup/popup.css, testdata/home/.cache/ffcss/materialfox/_/chrome/popup/urlbar-results.css, testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/close-tab.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/spinner-busy.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/spinner-progress.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/tabbar.css, testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-1x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-2x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-warning-1x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-warning-2x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/go.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/identity-icons-brand.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/info-1x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/info-2x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/naughty-shield-1x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/naughty-shield-2x.svg, testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/urlbar.css, testdata/home/.cache/ffcss/materialfox/_/chrome/userChrome.css, testdata/home/.cache/ffcss/materialfox/_/user.js, testdata/home/.cache/ffcss/simplerentfox/_/.github/FUNDING.yml, testdata/home/.cache/ffcss/simplerentfox/_/Linux/README.md, testdata/home/.cache/ffcss/simplerentfox/_/Linux/userChrome__HideURLBar.css, testdata/home/.cache/ffcss/simplerentfox/_/Linux/userChrome__OneLine.css, testdata/home/.cache/ffcss/simplerentfox/_/Linux/userChrome__WithURLBar.css, testdata/home/.cache/ffcss/simplerentfox/_/Linux/userContent.css, testdata/home/.cache/ffcss/simplerentfox/_/README.md, testdata/home/.cache/ffcss/simplerentfox/_/Windows/README.md, testdata/home/.cache/ffcss/simplerentfox/_/Windows/dark_additional_windows.css, testdata/home/.cache/ffcss/simplerentfox/_/Windows/dark_checkboxes_and_radios.css, testdata/home/.cache/ffcss/simplerentfox/_/Windows/dark_context_menus.css, testdata/home/.cache/ffcss/simplerentfox/_/Windows/userChrome__HideURLBar.css, testdata/home/.cache/ffcss/simplerentfox/_/Windows/userChrome__OneLine.css, testdata/home/.cache/ffcss/simplerentfox/_/Windows/userChrome__WithURLBar.css, testdata/home/.cache/ffcss/simplerentfox/_/Windows/userContent.css, testdata/home/.config/ffcss/currently.yaml, testdata/home/.mozilla/firefox/667ekipp.default-release/prefs.js, testdata/htmlfile.html, testdata/manifests/ffcss_version_negative.yaml, testdata/manifests/ffcssversionnegative.yaml, testdata/manifests/fine.yaml, testdata/manifests/invalid_firefox_constraint.yaml, testdata/manifests/no_name.yaml, testdata/manifests/root_variant_name.yaml, testdata/manifests/temp_download_dir_as_name.yaml, testdata/manifests/temp_download_dir_as_name_via_github_remote.yaml, testdata/manifests/unknown_os_key.yaml, testdata/manifests/wrong_casing_os_key.yaml, testdata/nomanifest/.deliverit.yaml, testdata/nomanifest/.editorconfig, testdata/nomanifest/.gitignore, testdata/nomanifest/.goreleaser.yml, testdata/nomanifest/CHANGELOG.md, testdata/nomanifest/fffcss.yaml, themes/.future/.current-progress-firefoxcss-store.md, themes/.future/aminomancer.yaml, themes/.future/moonlight.yaml, themes/.future/pseudo-fullscreen.yaml, themes/.future/rainfox.yaml, themes/.future/uwp.yaml, themes/.future/whitesur.yaml, themes/alpenblue.yaml, themes/australis-tabs.yaml, themes/blurredfox.yaml, themes/chameleons-beauty.yaml, themes/compactmode.yaml, themes/diamondfucsia.yaml, themes/flyingfox.yaml, themes/frozenfox.yaml, themes/fxcompact.yaml, themes/halo.yaml, themes/lepton.yaml, themes/martinfox.yaml, themes/materialfox-plus.yaml, themes/materialfox.yaml, themes/modoki.yaml, themes/montereyfox.yaml, themes/pro-fox.yaml, themes/proton-connected-rounded-tabs.yaml, themes/simplerentfox.yaml, themes/technetium.yaml, themes/verticaltabs.yaml, themes/wavefox.yaml, tools/make_release_notes.rb, tools/movefunc.rb, tools/ootb-themes.rb, tools/untested.rb, tools/wherefunc.rb, ui.go, ui_test.go, userprefs.go, userprefs_test.go, utils.go, utils_test.go, utils_unix_test.go, utils_windows_test.go
Commit date: 2023-10-17 21:20:45 +0100
Commit message: Perform the Unicode Normalization first.

Diff: diff --git a/.deliverit.yaml b/.deliverit.yaml
new file mode 100644
index 0000000..f7fb40f
--- /dev/null
+++ b/.deliverit.yaml
@@ -0,0 +1,2 @@
+language: go
+commit message: 🔖 Release {new}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..769bcb5
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,24 @@
+root = true
+
+[*]
+indent_style = tab
+indent_size = 4
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.{yml,yaml}]
+indent_style = space
+indent_size = 2
+
+[*.go]
+indent_style = tab
+
+[*.md]
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = false
+
+[*.rb]
+indent_style = space
+indent_size = 2
diff --git a/.github/ISSUE_TEMPLATE/adding-a-theme-to-the-registry.md b/.github/ISSUE_TEMPLATE/adding-a-theme-to-the-registry.md
new file mode 100644
index 0000000..548ef03
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/adding-a-theme-to-the-registry.md
@@ -0,0 +1,27 @@
+---
+name: Adding a theme to the registry
+about: To add a theme's manifest to the built-in ones, so that users can do `ffcss use YOUR-THEME`. 
+title: Add <your theme’s name> to the registry
+labels: registry
+assignees: ''
+---
+
+<!-- If you are familiar with git, please open a Pull Request, it'll be quicker. -->
+
+<!-- Choose one of the two following sections, and delete the other: -->
+
+## Manifest file (if you have one)
+
+```yaml
+# copy-paste the contents of your manifest here
+```
+
+## URL to the theme (if you don't have a manifest and want me to write one)
+
+
+
+## Author name
+
+<!-- If you don't have a github repository in the “download” field, tell me your author name & URL (optional) -->
+
+
diff --git a/.github/PULL_REQUEST_TEMPLATE/adding-a-theme-to-the-registry.md b/.github/PULL_REQUEST_TEMPLATE/adding-a-theme-to-the-registry.md
new file mode 100644
index 0000000..70530bb
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/adding-a-theme-to-the-registry.md
@@ -0,0 +1,18 @@
+---
+name: Adding a theme to the registry
+about: To add a theme's manifest to the built-in ones, so that users can do `ffcss
+  use YOUR-THEME`
+title: Add <your theme's name> to the registry
+labels: registry
+assignees: ''
+
+---
+
+Steps:
+
+1. Upload your manifest as `themes/<your theme's name>.yaml`
+1. If you want your username to appear in the "Built-in themes" section of the README, make sure that you either
+  - ~~Included a `by` field in the manifest~~ _Will be available once #33 is closed_
+  - Used a github repository in your `download` field
+  - Told me your author name/URL in this PR, so that I can add it manually when I release the next version of `ffcss`
+1. Ran `chachacha added new theme <your theme's name> in the registry` (if you don't have `chachacha`, run `pip install chachacha` (you need Python for this) _Note: if you have problems installing chachacha, don't worry, I'll modify the changelog myself_
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 0000000..dd3936b
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,41 @@
+on: [push, pull_request]
+name: test
+
+jobs:
+  test:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        go: [1.16.x]
+        os:
+          - ubuntu-latest
+          # - macos-latest
+          # - windows-latest
+    steps:
+      - name: Skip if commit type is skippable
+        uses: mstachniuk/ci-skip@v1
+        with:
+          commit-filter: >
+            📝 :memo:
+            ✏️ :pencil2:
+            📄 :page_facing_up:
+            💡 :bulb:
+            💬 :speech_baloon:
+            🔊 :loud_sound:
+            🔇 :mute:
+            👥 :busts_in_silhouette:
+            🙈 :see_no_evil:
+            🧪 :test_tube:
+          commit-filter-separator: ' '
+      - name: Install Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: ${{ matrix.go }}
+      - name: Checkout code
+        uses: actions/checkout@v2
+      - name: Test
+        run: make tests
+      - name: Upload coverage
+        uses: codecov/codecov-action@v2
+        with:
+          verbose: yes
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d16c507
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,56 @@
+
+# Created by https://www.toptal.com/developers/gitignore/api/go
+# Edit at https://www.toptal.com/developers/gitignore?templates=go
+
+### Go ###
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+### Go Patch ###
+/vendor/
+/Godeps/
+
+# End of https://www.toptal.com/developers/gitignore/api/go
+
+# Universal Package Manager
+.upm/
+
+# dotENV file
+.env
+
+# vscode
+.vscode/
+
+commit-types-distribution.png
+
+# coverage
+profile.out
+coverage.txt
+coverage/lcov.info
+
+# compiled binary
+/ffcss
+
+# Release artifacts & token files
+dist/
+release_notes.md
+.github_token
+
+# Testing artifacts
+testarea/
+
+cmd/ffcss/ffcss
+cmd/ffcss/ffcss
\ No newline at end of file
diff --git a/.goreleaser.yml b/.goreleaser.yml
new file mode 100644
index 0000000..1c9288d
--- /dev/null
+++ b/.goreleaser.yml
@@ -0,0 +1,57 @@
+# This is an example .goreleaser.yml file with some sane defaults.
+# Make sure to check the documentation at http://goreleaser.com
+
+project_name: ffcss
+
+before:
+  hooks:
+    # You may remove this if you don't use go modules.
+    - go mod tidy
+    # you may remove this if you don't need go generate
+    - go generate ./...
+    # so that github does not create a tag in another place
+    - git push
+    # push the tag
+    - git push origin {{ .Tag }}
+
+builds:
+  -
+    goos: [windows, linux, darwin]
+    goarch: [amd64, 386]
+    binary: ffcss
+
+archives:
+  -
+    format: tar.gz
+    format_overrides:
+      - goos: windows
+        format: zip
+
+    name_template: '{{.Os}}-{{.Arch}}'
+
+    replacements:
+      darwin: macos
+      386: 32bit
+      amd64: 64bit
+
+    files:
+      - LICENSE
+      - README.md
+      - CHANGELOG.md
+      - themes/**
+checksum:
+  name_template: 'checksums.txt'
+
+# TODO: Homebrew, NFPM, Scoop, Snapcraft (eww), Signing
+
+release:
+  prerelease: auto
+  name_template: "{{ .Version }}"
+
+changelog:
+  skip: true
+
+milestones:
+  -
+    name_template: "{{ .Version }}"
+    close: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..f38f13a
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,108 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [0.2.0] - 2021-07-25
+
+### Added
+
+- new `firefox` manifest entry: can be used to declare which versions of firefox are compatible with your theme: `version+` means "`version` and up", `up to version` means, well, you guessed it, `version1-version2` is an (inclusive) range and finally just `version` means exactly _that_ version. Use `major.minor` to specify the minor version, or omit it (or use ".x") to ignore it.
+- prompt to show the manifest source of the theme you're installing. The prompt can be turned off with a new flag `--skip-manifest-source`
+- command _reapply_ to reapply themes after firefox updates. The current themes for each profile are stored in a YAML file at `~/.config/ffcss/currently.yaml`. If ffcss tells you that a profile has no ffcss theme applied, try re-applying it manually with `ffcss use` so that this file gets created & filled.
+- hooks to run custom shell commands before and after theme installations, via manifest entries `run.before` and `run.after`. Variants can override them.
+- "did you mean ...?" message when a theme with a similar name is found
+- command _get_ to download a theme without applying it
+- manifest entry `commit` to specify a commit SHA to use for the theme. Can be overriden by variants.
+- manifest entry `tag` to specify a git tag to use for the theme. Can be overriden by variants.
+- manifest entry `description` that will be shown at the start of the installation
+- manifest entry `by` to set an author name to be shown at the start of the installation (for github repos, defaults to the owner in the URL)
+- shorthand flag `-a` for `--all-profiles`
+- flag `-p`/`--profiles` to specify profile paths to use (and therefore not prompt during installation)
+
+### Changed
+
+- `ffcss use <theme name>` is now case-insensitive, punctuation-insensitive (`-`, `_` and `.`), whitespace-insensitive and unicode-insensitive (NFC normalization is applied before searching)
+- UI improvements: log what is happening with bullet points, don't show the full profile path when selecting
+
+### Fixed
+
+- inconsistent indentation in the `--help` screen
+
+## [0.1.2] - 2021-07-10
+
+### Added
+
+- new theme in registry: [alpenblue](https://github.com/Godiesc/AlpenBlue) by [Godiesc](https://github.com/Godiesc)
+- new theme in registry: [australis-tabs](https://github.com/sagars007/Australis-like-tabs-FF-ProtonUI-changes) by [sagars007](https://github.com/sagars007)
+- new theme in registry: [blurredfox](https://github.com/manilarome/blurredfox) by [manilarome](https://github.com/manilarome)
+- new theme in registry: [compactmode](https://github.com/Godiesc/compactmodefirefoxcss) by [Godiesc](https://github.com/Godiesc)
+- new theme in registry: [diamondfucsia](https://github.com/Godiesc/DiamondFucsia) by [Godiesc](https://github.com/Godiesc)
+- new theme in registry: [frozenfox](https://github.com/tortious/FrozenFox) by [tortious](https://github.com/tortious)
+- new theme in registry: [halo](https://github.com/seirin-blu/Firefox-Halo) by [seirin-blu](https://github.com/seirin-blu)
+- new theme in registry: [martinfox](https://github.com/arp242/MartinFox) by [arp242](https://github.com/arp242)
+- new theme in registry: [montereyfox](https://github.com/FirefoxCSSThemers/Monterey-Fox) by [FirefoxCSSThemers](https://github.com/FirefoxCSSThemers)
+- new theme in registry: [pro-fox](https://github.com/xmansyx/Pro-Fox) by [xmansyx](https://github.com/xmansyx)
+- new theme in registry: [proton-connected-rounded-tabs](https://github.com/sagars007/Proton-UI-connected-rounded-tabs) by [sagars007](https://github.com/sagars007)
+- new theme in registry: [technetium](https://github.com/edo0/Technetium) by [edo0](https://github.com/edo0)
+- new theme in registry: [wavefox](https://github.com/QNetITQ/WaveFox) by [QNetITQ](https://github.com/QNetITQ)
+
+### Fixed
+
+- `ffcss init` was generating a file with a repository setting instead of download [[#36](https://github.com/ewen-lbh/ffcss/issues/36)]
+- `ffcss init` added a .git suffix in the pre-filled value for `name`
+
+## [0.1.1] - 2021-07-10
+
+### Fixed
+
+- crashes related to path handling on Windows
+
+## [0.1.0] - 2021-07-09
+
+### Added
+
+#### For users
+- Works on MacOS, GNU/Linux and Windows, tested on:
+- Manjaro Linux Omara 21.0.7 (with kernel 5.12.9-1-MANJARO)
+- MacOS Catalina 10.15.7
+- Windows 10 20H2 (Build 19042.1083) (Please use the new Windows Terminal or something else that support ANSI escape sequences)
+- a `use` commands to download & install themes
+- works with any remote git repository
+- works with any URL poiting to a .zip file
+- shorthand for github repositories: OWNER/REPO
+- a `init` command to add a `ffcss.yaml` manifest in your current repository
+- basic for now, [a smarter version is planned](https://github.com/ewen-lbh/ffcss/issues/20)
+- a `cache clear` command to clear the cache of downloaded repositories
+- 8 themes available out-of-the-box (use them by typing their name only, it works)
+- [chameleons-beauty](https://github.com/Godiesc/Chameleons-Beauty) by [Godiesc](https://github.com/Godiesc)
+- [fxcompact](https://github.com/dannycolin/fx-compact-mode) by [dannycolin](https://github.com/dannycolin)
+- [lepton](https://github.com/black7375/Firefox-UI-Fix) by [black7375](https://github.com/black7375)
+- [materialfox](https://github.com/muckSponge/MaterialFox) by [muckSponge](https://github.com/muckSponge)
+- [modoki](https://github.com/soup-bowl/Modoki-FirefoxCSS) by [soup-bowl](https://github.com/soup-bowl)
+- [simplerentfox](https://github.com/MiguelRAvila/SimplerentFox) by [MiguelRAvila](https://github.com/MiguelRAvila)
+- [verticaltabs](https://github.com/ranmaru22/firefox-vertical-tabs) by [ranmaru22](https://github.com/ranmaru22)
+#### For theme makers
+- a mechanism to handle theme variants:
+- Variants can be declared in the same manifest file under the `variants` entry to override other entries
+- per-OS paths: the {{ variant }} and {{ os }} placeholders get replaced with their values
+- the value {{ os }} gets replaced with can be customized in the manifest file under the `os` entry, use `null` to mark an OS as incompatible
+- Support for helper addons:
+- Declare URLs to open after installation under the `addons` manifest entry (I plan to auto-install them in the future)
+- Easy way to write about:config changes without writing a `user.js` file:
+- Use the `config` manifest entry
+- If you also use a `user.js`, you can combine both, they'll be written as a single `user.js` file
+- Support for custom assets:
+- Use the `assets` manifest entry to list out your assets
+- Supports glob patterns
+- If you store them under a `chrome` directory, you can use `copy from: chrome/` so that they don't get copied to `<profile directory>/chrome/chrome`
+
+[Unreleased]: https://github.com/ewen-lbh/ffcss/compare/v0.2.0...HEAD
+[0.2.0]: https://github.com/ewen-lbh/ffcss/compare/v0.1.2...v0.2.0
+[0.1.2]: https://github.com/ewen-lbh/ffcss/compare/v0.1.1...v0.1.2
+[0.1.1]: https://github.com/ewen-lbh/ffcss/compare/v0.1.0...v0.1.1
+[0.1.0]: https://github.com/ewen-lbh/ffcss/releases/tag/v0.1.0
+
+[//]: # (C3-2-DKAC:GGH:Rewen-lbh/ffcss:Tv{t})
diff --git a/DEMO.gif b/DEMO.gif
new file mode 100644
index 0000000..026c17a
Binary files /dev/null and b/DEMO.gif differ
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e142a52
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,625 @@
+GNU GENERAL PUBLIC LICENSE
+
+Version 3, 29 June 2007
+
+Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
+
+Everyone is permitted to copy and distribute verbatim copies of this license
+document, but changing it is not allowed.
+
+Preamble
+
+The GNU General Public License is a free, copyleft license for software and
+other kinds of works.
+
+The licenses for most software and other practical works are designed to take
+away your freedom to share and change the works. By contrast, the GNU General
+Public License is intended to guarantee your freedom to share and change all
+versions of a program--to make sure it remains free software for all its users.
+We, the Free Software Foundation, use the GNU General Public License for most
+of our software; it applies also to any other work released this way by its
+authors. You can apply it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our
+General Public Licenses are designed to make sure that you have the freedom
+to distribute copies of free software (and charge for them if you wish), that
+you receive source code or can get it if you want it, that you can change
+the software or use pieces of it in new free programs, and that you know you
+can do these things.
+
+To protect your rights, we need to prevent others from denying you these rights
+or asking you to surrender the rights. Therefore, you have certain responsibilities
+if you distribute copies of the software, or if you modify it: responsibilities
+to respect the freedom of others.
+
+For example, if you distribute copies of such a program, whether gratis or
+for a fee, you must pass on to the recipients the same freedoms that you received.
+You must make sure that they, too, receive or can get the source code. And
+you must show them these terms so they know their rights.
+
+Developers that use the GNU GPL protect your rights with two steps: (1) assert
+copyright on the software, and (2) offer you this License giving you legal
+permission to copy, distribute and/or modify it.
+
+For the developers' and authors' protection, the GPL clearly explains that
+there is no warranty for this free software. For both users' and authors'
+sake, the GPL requires that modified versions be marked as changed, so that
+their problems will not be attributed erroneously to authors of previous versions.
+
+Some devices are designed to deny users access to install or run modified
+versions of the software inside them, although the manufacturer can do so.
+This is fundamentally incompatible with the aim of protecting users' freedom
+to change the software. The systematic pattern of such abuse occurs in the
+area of products for individuals to use, which is precisely where it is most
+unacceptable. Therefore, we have designed this version of the GPL to prohibit
+the practice for those products. If such problems arise substantially in other
+domains, we stand ready to extend this provision to those domains in future
+versions of the GPL, as needed to protect the freedom of users.
+
+Finally, every program is threatened constantly by software patents. States
+should not allow patents to restrict development and use of software on general-purpose
+computers, but in those that do, we wish to avoid the special danger that
+patents applied to a free program could make it effectively proprietary. To
+prevent this, the GPL assures that patents cannot be used to render the program
+non-free.
+
+The precise terms and conditions for copying, distribution and modification
+follow.
+
+TERMS AND CONDITIONS
+
+   0. Definitions.
+
+   "This License" refers to version 3 of the GNU General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds of works,
+such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this License.
+Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals
+or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work in
+a fashion requiring copyright permission, other than the making of an exact
+copy. The resulting work is called a "modified version" of the earlier work
+or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based on the
+Program.
+
+To "propagate" a work means to do anything with it that, without permission,
+would make you directly or secondarily liable for infringement under applicable
+copyright law, except executing it on a computer or modifying a private copy.
+Propagation includes copying, distribution (with or without modification),
+making available to the public, and in some countries other activities as
+well.
+
+To "convey" a work means any kind of propagation that enables other parties
+to make or receive copies. Mere interaction with a user through a computer
+network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to the
+extent that it includes a convenient and prominently visible feature that
+(1) displays an appropriate copyright notice, and (2) tells the user that
+there is no warranty for the work (except to the extent that warranties are
+provided), that licensees may convey the work under this License, and how
+to view a copy of this License. If the interface presents a list of user commands
+or options, such as a menu, a prominent item in the list meets this criterion.
+
+   1. Source Code.
+
+The "source code" for a work means the preferred form of the work for making
+modifications to it. "Object code" means any non-source form of a work.
+
+A "Standard Interface" means an interface that either is an official standard
+defined by a recognized standards body, or, in the case of interfaces specified
+for a particular programming language, one that is widely used among developers
+working in that language.
+
+The "System Libraries" of an executable work include anything, other than
+the work as a whole, that (a) is included in the normal form of packaging
+a Major Component, but which is not part of that Major Component, and (b)
+serves only to enable use of the work with that Major Component, or to implement
+a Standard Interface for which an implementation is available to the public
+in source code form. A "Major Component", in this context, means a major essential
+component (kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to produce
+the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all the source
+code needed to generate, install, and (for an executable work) run the object
+code and to modify the work, including scripts to control those activities.
+However, it does not include the work's System Libraries, or general-purpose
+tools or generally available free programs which are used unmodified in performing
+those activities but which are not part of the work. For example, Corresponding
+Source includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically linked
+subprograms that the work is specifically designed to require, such as by
+intimate data communication or control flow between those subprograms and
+other parts of the work.
+
+The Corresponding Source need not include anything that users can regenerate
+automatically from other parts of the Corresponding Source.
+
+   The Corresponding Source for a work in source code form is that same work.
+
+   2. Basic Permissions.
+
+All rights granted under this License are granted for the term of copyright
+on the Program, and are irrevocable provided the stated conditions are met.
+This License explicitly affirms your unlimited permission to run the unmodified
+Program. The output from running a covered work is covered by this License
+only if the output, given its content, constitutes a covered work. This License
+acknowledges your rights of fair use or other equivalent, as provided by copyright
+law.
+
+You may make, run and propagate covered works that you do not convey, without
+conditions so long as your license otherwise remains in force. You may convey
+covered works to others for the sole purpose of having them make modifications
+exclusively for you, or provide you with facilities for running those works,
+provided that you comply with the terms of this License in conveying all material
+for which you do not control copyright. Those thus making or running the covered
+works for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of your copyrighted
+material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the conditions
+stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
+
+   3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological measure
+under any applicable law fulfilling obligations under article 11 of the WIPO
+copyright treaty adopted on 20 December 1996, or similar laws prohibiting
+or restricting circumvention of such measures.
+
+When you convey a covered work, you waive any legal power to forbid circumvention
+of technological measures to the extent such circumvention is effected by
+exercising rights under this License with respect to the covered work, and
+you disclaim any intention to limit operation or modification of the work
+as a means of enforcing, against the work's users, your or third parties'
+legal rights to forbid circumvention of technological measures.
+
+   4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you receive
+it, in any medium, provided that you conspicuously and appropriately publish
+on each copy an appropriate copyright notice; keep intact all notices stating
+that this License and any non-permissive terms added in accord with section
+7 apply to the code; keep intact all notices of the absence of any warranty;
+and give all recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey, and you
+may offer support or warranty protection for a fee.
+
+   5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to produce
+it from the Program, in the form of source code under the terms of section
+4, provided that you also meet all of these conditions:
+
+a) The work must carry prominent notices stating that you modified it, and
+giving a relevant date.
+
+b) The work must carry prominent notices stating that it is released under
+this License and any conditions added under section 7. This requirement modifies
+the requirement in section 4 to "keep intact all notices".
+
+c) You must license the entire work, as a whole, under this License to anyone
+who comes into possession of a copy. This License will therefore apply, along
+with any applicable section 7 additional terms, to the whole of the work,
+and all its parts, regardless of how they are packaged. This License gives
+no permission to license the work in any other way, but it does not invalidate
+such permission if you have separately received it.
+
+d) If the work has interactive user interfaces, each must display Appropriate
+Legal Notices; however, if the Program has interactive interfaces that do
+not display Appropriate Legal Notices, your work need not make them do so.
+
+A compilation of a covered work with other separate and independent works,
+which are not by their nature extensions of the covered work, and which are
+not combined with it such as to form a larger program, in or on a volume of
+a storage or distribution medium, is called an "aggregate" if the compilation
+and its resulting copyright are not used to limit the access or legal rights
+of the compilation's users beyond what the individual works permit. Inclusion
+of a covered work in an aggregate does not cause this License to apply to
+the other parts of the aggregate.
+
+   6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of sections
+4 and 5, provided that you also convey the machine-readable Corresponding
+Source under the terms of this License, in one of these ways:
+
+a) Convey the object code in, or embodied in, a physical product (including
+a physical distribution medium), accompanied by the Corresponding Source fixed
+on a durable physical medium customarily used for software interchange.
+
+b) Convey the object code in, or embodied in, a physical product (including
+a physical distribution medium), accompanied by a written offer, valid for
+at least three years and valid for as long as you offer spare parts or customer
+support for that product model, to give anyone who possesses the object code
+either (1) a copy of the Corresponding Source for all the software in the
+product that is covered by this License, on a durable physical medium customarily
+used for software interchange, for a price no more than your reasonable cost
+of physically performing this conveying of source, or (2) access to copy the
+Corresponding Source from a network server at no charge.
+
+c) Convey individual copies of the object code with a copy of the written
+offer to provide the Corresponding Source. This alternative is allowed only
+occasionally and noncommercially, and only if you received the object code
+with such an offer, in accord with subsection 6b.
+
+d) Convey the object code by offering access from a designated place (gratis
+or for a charge), and offer equivalent access to the Corresponding Source
+in the same way through the same place at no further charge. You need not
+require recipients to copy the Corresponding Source along with the object
+code. If the place to copy the object code is a network server, the Corresponding
+Source may be on a different server (operated by you or a third party) that
+supports equivalent copying facilities, provided you maintain clear directions
+next to the object code saying where to find the Corresponding Source. Regardless
+of what server hosts the Corresponding Source, you remain obligated to ensure
+that it is available for as long as needed to satisfy these requirements.
+
+e) Convey the object code using peer-to-peer transmission, provided you inform
+other peers where the object code and Corresponding Source of the work are
+being offered to the general public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded from
+the Corresponding Source as a System Library, need not be included in conveying
+the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any tangible
+personal property which is normally used for personal, family, or household
+purposes, or (2) anything designed or sold for incorporation into a dwelling.
+In determining whether a product is a consumer product, doubtful cases shall
+be resolved in favor of coverage. For a particular product received by a particular
+user, "normally used" refers to a typical or common use of that class of product,
+regardless of the status of the particular user or of the way in which the
+particular user actually uses, or expects or is expected to use, the product.
+A product is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent the
+only significant mode of use of the product.
+
+"Installation Information" for a User Product means any methods, procedures,
+authorization keys, or other information required to install and execute modified
+versions of a covered work in that User Product from a modified version of
+its Corresponding Source. The information must suffice to ensure that the
+continued functioning of the modified object code is in no case prevented
+or interfered with solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or specifically
+for use in, a User Product, and the conveying occurs as part of a transaction
+in which the right of possession and use of the User Product is transferred
+to the recipient in perpetuity or for a fixed term (regardless of how the
+transaction is characterized), the Corresponding Source conveyed under this
+section must be accompanied by the Installation Information. But this requirement
+does not apply if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has been installed
+in ROM).
+
+The requirement to provide Installation Information does not include a requirement
+to continue to provide support service, warranty, or updates for a work that
+has been modified or installed by the recipient, or for the User Product in
+which it has been modified or installed. Access to a network may be denied
+when the modification itself materially and adversely affects the operation
+of the network or violates the rules and protocols for communication across
+the network.
+
+Corresponding Source conveyed, and Installation Information provided, in accord
+with this section must be in a format that is publicly documented (and with
+an implementation available to the public in source code form), and must require
+no special password or key for unpacking, reading or copying.
+
+   7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this License
+by making exceptions from one or more of its conditions. Additional permissions
+that are applicable to the entire Program shall be treated as though they
+were included in this License, to the extent that they are valid under applicable
+law. If additional permissions apply only to part of the Program, that part
+may be used separately under those permissions, but the entire Program remains
+governed by this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option remove any
+additional permissions from that copy, or from any part of it. (Additional
+permissions may be written to require their own removal in certain cases when
+you modify the work.) You may place additional permissions on material, added
+by you to a covered work, for which you have or can give appropriate copyright
+permission.
+
+Notwithstanding any other provision of this License, for material you add
+to a covered work, you may (if authorized by the copyright holders of that
+material) supplement the terms of this License with terms:
+
+a) Disclaiming warranty or limiting liability differently from the terms of
+sections 15 and 16 of this License; or
+
+b) Requiring preservation of specified reasonable legal notices or author
+attributions in that material or in the Appropriate Legal Notices displayed
+by works containing it; or
+
+c) Prohibiting misrepresentation of the origin of that material, or requiring
+that modified versions of such material be marked in reasonable ways as different
+from the original version; or
+
+d) Limiting the use for publicity purposes of names of licensors or authors
+of the material; or
+
+e) Declining to grant rights under trademark law for use of some trade names,
+trademarks, or service marks; or
+
+f) Requiring indemnification of licensors and authors of that material by
+anyone who conveys the material (or modified versions of it) with contractual
+assumptions of liability to the recipient, for any liability that these contractual
+assumptions directly impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further restrictions"
+within the meaning of section 10. If the Program as you received it, or any
+part of it, contains a notice stating that it is governed by this License
+along with a term that is a further restriction, you may remove that term.
+If a license document contains a further restriction but permits relicensing
+or conveying under this License, you may add to a covered work material governed
+by the terms of that license document, provided that the further restriction
+does not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you must place,
+in the relevant source files, a statement of the additional terms that apply
+to those files, or a notice indicating where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the form
+of a separately written license, or stated as exceptions; the above requirements
+apply either way.
+
+   8. Termination.
+
+You may not propagate or modify a covered work except as expressly provided
+under this License. Any attempt otherwise to propagate or modify it is void,
+and will automatically terminate your rights under this License (including
+any patent licenses granted under the third paragraph of section 11).
+
+However, if you cease all violation of this License, then your license from
+a particular copyright holder is reinstated (a) provisionally, unless and
+until the copyright holder explicitly and finally terminates your license,
+and (b) permanently, if the copyright holder fails to notify you of the violation
+by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently
+if the copyright holder notifies you of the violation by some reasonable means,
+this is the first time you have received notice of violation of this License
+(for any work) from that copyright holder, and you cure the violation prior
+to 30 days after your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses
+of parties who have received copies or rights from you under this License.
+If your rights have been terminated and not permanently reinstated, you do
+not qualify to receive new licenses for the same material under section 10.
+
+   9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run a copy
+of the Program. Ancillary propagation of a covered work occurring solely as
+a consequence of using peer-to-peer transmission to receive a copy likewise
+does not require acceptance. However, nothing other than this License grants
+you permission to propagate or modify any covered work. These actions infringe
+copyright if you do not accept this License. Therefore, by modifying or propagating
+a covered work, you indicate your acceptance of this License to do so.
+
+   10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically receives
+a license from the original licensors, to run, modify and propagate that work,
+subject to this License. You are not responsible for enforcing compliance
+by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an organization,
+or substantially all assets of one, or subdividing an organization, or merging
+organizations. If propagation of a covered work results from an entity transaction,
+each party to that transaction who receives a copy of the work also receives
+whatever licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the Corresponding
+Source of the work from the predecessor in interest, if the predecessor has
+it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the rights
+granted or affirmed under this License. For example, you may not impose a
+license fee, royalty, or other charge for exercise of rights granted under
+this License, and you may not initiate litigation (including a cross-claim
+or counterclaim in a lawsuit) alleging that any patent claim is infringed
+by making, using, selling, offering for sale, or importing the Program or
+any portion of it.
+
+   11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this License
+of the Program or a work on which the Program is based. The work thus licensed
+is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned or controlled
+by the contributor, whether already acquired or hereafter acquired, that would
+be infringed by some manner, permitted by this License, of making, using,
+or selling its contributor version, but do not include claims that would be
+infringed only as a consequence of further modification of the contributor
+version. For purposes of this definition, "control" includes the right to
+grant patent sublicenses in a manner consistent with the requirements of this
+License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free patent
+license under the contributor's essential patent claims, to make, use, sell,
+offer for sale, import and otherwise run, modify and propagate the contents
+of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express agreement
+or commitment, however denominated, not to enforce a patent (such as an express
+permission to practice a patent or covenant not to sue for patent infringement).
+To "grant" such a patent license to a party means to make such an agreement
+or commitment not to enforce a patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license, and the
+Corresponding Source of the work is not available for anyone to copy, free
+of charge and under the terms of this License, through a publicly available
+network server or other readily accessible means, then you must either (1)
+cause the Corresponding Source to be so available, or (2) arrange to deprive
+yourself of the benefit of the patent license for this particular work, or
+(3) arrange, in a manner consistent with the requirements of this License,
+to extend the patent license to downstream recipients. "Knowingly relying"
+means you have actual knowledge that, but for the patent license, your conveying
+the covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that country
+that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or arrangement,
+you convey, or propagate by procuring conveyance of, a covered work, and grant
+a patent license to some of the parties receiving the covered work authorizing
+them to use, propagate, modify or convey a specific copy of the covered work,
+then the patent license you grant is automatically extended to all recipients
+of the covered work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the scope
+of its coverage, prohibits the exercise of, or is conditioned on the non-exercise
+of one or more of the rights that are specifically granted under this License.
+You may not convey a covered work if you are a party to an arrangement with
+a third party that is in the business of distributing software, under which
+you make payment to the third party based on the extent of your activity of
+conveying the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory patent
+license (a) in connection with copies of the covered work conveyed by you
+(or copies made from those copies), or (b) primarily for and in connection
+with specific products or compilations that contain the covered work, unless
+you entered into that arrangement, or that patent license was granted, prior
+to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting any implied
+license or other defenses to infringement that may otherwise be available
+to you under applicable patent law.
+
+   12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or otherwise)
+that contradict the conditions of this License, they do not excuse you from
+the conditions of this License. If you cannot convey a covered work so as
+to satisfy simultaneously your obligations under this License and any other
+pertinent obligations, then as a consequence you may not convey it at all.
+For example, if you agree to terms that obligate you to collect a royalty
+for further conveying from those to whom you convey the Program, the only
+way you could satisfy both those terms and this License would be to refrain
+entirely from conveying the Program.
+
+   13. Use with the GNU Affero General Public License.
+
+Notwithstanding any other provision of this License, you have permission to
+link or combine any covered work with a work licensed under version 3 of the
+GNU Affero General Public License into a single combined work, and to convey
+the resulting work. The terms of this License will continue to apply to the
+part which is the covered work, but the special requirements of the GNU Affero
+General Public License, section 13, concerning interaction through a network
+will apply to the combination as such.
+
+   14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions of the
+GNU General Public License from time to time. Such new versions will be similar
+in spirit to the present version, but may differ in detail to address new
+problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies
+that a certain numbered version of the GNU General Public License "or any
+later version" applies to it, you have the option of following the terms and
+conditions either of that numbered version or of any later version published
+by the Free Software Foundation. If the Program does not specify a version
+number of the GNU General Public License, you may choose any version ever
+published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions of
+the GNU General Public License can be used, that proxy's public statement
+of acceptance of a version permanently authorizes you to choose that version
+for the Program.
+
+Later license versions may give you additional or different permissions. However,
+no additional obligations are imposed on any author or copyright holder as
+a result of your choosing to follow a later version.
+
+   15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE
+LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
+EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM
+PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
+CORRECTION.
+
+   16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL
+ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM
+AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
+INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO
+USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
+INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE
+PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
+PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+   17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided above cannot
+be given local legal effect according to their terms, reviewing courts shall
+apply local law that most closely approximates an absolute waiver of all civil
+liability in connection with the Program, unless a warranty or assumption
+of liability accompanies a copy of the Program in return for a fee. END OF
+TERMS AND CONDITIONS
+
+How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest possible
+use to the public, the best way to achieve this is to make it free software
+which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. It is safest to attach
+them to the start of each source file to most effectively state the exclusion
+of warranty; and each file should have at least the "copyright" line and a
+pointer to where the full notice is found.
+
+<one line to give the program's name and a brief idea of what it does.>
+
+Copyright (C) <year> <name of author>
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free Software
+Foundation, either version 3 of the License, or (at your option) any later
+version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with
+this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program does terminal interaction, make it output a short notice like
+this when it starts in an interactive mode:
+
+<program> Copyright (C) <year> <name of author>
+
+This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+
+This is free software, and you are welcome to redistribute it under certain
+conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands might
+be different; for a GUI interface, you would use an "about box".
+
+You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary. For
+more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>.
+
+The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General Public
+License instead of this License. But first, please read <https://www.gnu.org/
+licenses /why-not-lgpl.html>.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..cf20a0b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,91 @@
+export SHELL:=/bin/bash
+export SHELLOPTS:=$(if $(SHELLOPTS),$(SHELLOPTS):)pipefail:errexit
+
+.ONESHELL:
+.PHONY: coverage
+
+build:
+	go mod tidy
+	cd cmd/ffcss && go build
+
+tests:
+# setup mocks
+	$(MAKE) mocks-setup
+# defer tearing down mocks (so that it runs even if the tests fail)
+	trap "$(MAKE) mocks-teardown" EXIT
+# run tests, with $HOME overriden to a mocked directory, and gopath re-set, otherwise the compiler freaks out.
+	GOPATH=$$(go env GOPATH) HOME=testarea/home GIT_TERMINAL_PROMPT=0 go test -race -coverprofile=coverage.txt -covermode=atomic -parallel 10
+# compute code coverage
+	$(MAKE) coverage
+
+coverage:
+# get the binary to convert from go coverage file to a standard one accepted by codecov & co
+	go get -u github.com/jandelgado/gcov2lcov
+# run it
+	gcov2lcov -infile=coverage.txt -outfile=coverage/lcov.info
+# remove it from the go.mod file
+	go mod tidy
+
+install:
+# make necessary directories
+	mkdir -p ~/.config/ffcss/themes ~/.local/bin
+# copy builtin themes
+	cp themes/*.yaml ~/.config/ffcss/themes/
+# copy binary to some standard place that's in $PATH most of the time
+	cp cmd/ffcss/ffcss ~/.local/bin/ffcss
+
+format:
+	gofmt -s -w **.go
+	cd cmd/ffcss; gofmt -s -w *.go
+
+mocks-setup:
+# configure git identity (for whatever reason it needs it now)
+	git config --global --get user.name || git config --global user.name "Ewen Le Bihan"
+	git config --global --get user.email || git config --global user.email hey@ewen.works
+# init git repo for testdata/nomanifest
+	cd testdata/nomanifest; git init --quiet; git add .; git commit --quiet -m "a"; cd ../..
+# local webserver to mock http requests, save its PID to a file so that we can kill it during teardown
+	python -m http.server 8080 --bind localhost --directory testdata/ >/dev/null 2>/dev/null & echo $$! > .mockswebserverpid
+# create directories that are cleaned up after use
+	mkdir -p testarea/{zip-dropoff,cache,home/{.mozilla/firefox/667ekipp.default-release,.{config,cache}/ffcss,.config/ffcss/themes}}
+# copy themes into mock config directory
+	cp themes/*.yaml testarea/home/.config/ffcss/themes/
+# copy static mocks from testdata/ to testarea/
+	cp -R testdata/{home,catalogs,manifests}/ testarea/
+# create coverage directory
+	mkdir -p coverage
+
+mocks-teardown:
+	rm -rf testdata/nomanifest/.git
+# remove testing artifacts
+	rm -rf testarea
+# kill mocks webserver
+	kill -9 $$(cat .mockswebserverpid)
+# remove mocks webserver PID file
+	rm .mockswebserverpid
+
+
+release:
+# remove artifacts from previous release
+	rm -rf dist/
+# build & install binary
+	$(MAKE) build
+	$(MAKE) install
+# update changelog headings
+	chachacha release $$(read -p bump=; echo $$REPLY)
+# remove stupid bullet points in front of <hX> tags
+	sd '^- #' '#' CHANGELOG.md
+# extract release notes for the new version only,
+# and bump the version in go code
+	tools/make_release_notes.rb $$(read -p bump=; echo $$REPLY)
+# recompile so that the binary shows the new version when doing ffcss version
+	$(MAKE) build
+	$(MAKE) install
+# make tagged Release commit
+	git add CHANGELOG.md ffcss.go
+	git commit -m "🔖 Release $$(ffcss version)"
+	git tag -am v$$(ffcss version) v$$(ffcss version)
+# github push, tags push, github release w/ binaries in .tar.gz, milestone close, etc.
+	GITHUB_TOKEN=$$(cat .github_token) goreleaser release --release-notes release_notes.md
+# remove extracted release notes
+	rm release_notes.md
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9204b5d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,418 @@
+# ffcss
+
+![a demo of ffcss](./DEMO.gif)
+
+[![wakatime](https://wakatime.com/badge/github/ewen-lbh/ffcss.svg)](https://wakatime.com/badge/github/ewen-lbh/ffcss)
+![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/ewen-lbh/ffcss)
+[![Go reference](https://img.shields.io/badge/-reference-grey?logo=go)](https://pkg.go.dev/github.com/ewen-lbh/ffcss)
+![GitHub Workflow Status](https://img.shields.io/github/workflow/status/ewen-lbh/ffcss/test)
+![Codecov](https://img.shields.io/codecov/c/github/ewen-lbh/ffcss)
+
+A CLI interface to apply and configure [Firefox CSS themes](https://reddit.com/r/FirefoxCSS) (also known as userChrome.css themes).
+
+It uses a YAML file to declare files, variants, helper addons and others. This file is referred to as a "manifest".
+
+## Installation
+
+### AUR
+
+Arch Linux users can install the package through the AUR:
+
+```
+yay -S ffcss # latest release
+# COMING SOON yay -S ffcss-git # latest commit
+# COMING SOON yay -S ffcss-bin # pre-compiled binary of latest release
+```
+
+### Pre-compiled binary 
+
+#### Linux & MacOS
+
+_Make sure there isn't a folder called “installation” in your working directory_
+
+```sh
+# Don’t pollute your current directory
+mkdir installation; cd installation
+# Install the latest release by downloading the binary on Github
+wget https://github.com/ewen-lbh/ffcss/releases/latest/download/YOUR-OS-64bit.tar.gz -O ffcss.tar.gz # replace YOUR-OS with linux or macos. There's also a 32bit version (386 instead of amd64)
+# Extract it
+tar -xzvf ffcss.tar.gz
+# Install built-in themes
+mkdir -p ~/.config/ffcss/themes/
+cp -v themes/*.yaml ~/.config/ffcss/themes/
+# Make sure the file is marked as executable
+chmod +x ffcss
+# Move it to a folder that’s in your path (so you can type `ffcss` anywhere), eg.
+mv ffcss ~/.local/bin/ffcss
+# Remove the extracted contents (you don’t need them anymore)
+cd ..; rm -I -rf installation
+```
+
+#### Windows
+
+(Assuming your user folder is at `C:\Users\you`)
+
+1. Download the zip file: https://github.com/ewen-lbh/ffcss/releases/latest/download/windows-64bit.zip
+1. Extract it somewhere, let's say in `C:\Users\you\Desktop\ffcss`.
+1. Open that folder in the Explorer
+1. Right-click on empty space and select "Open Windows Terminal Here"
+1. Type `.\ffcss.exe`
+1. Copy `C:\Users\you\Desktop\ffcss\themes` to `C:\Users\you\.config\ffcss`
+
+To use ffcss, you can run that `ffcss.exe` in the Windows Terminal
+
+### Compile it yourself
+
+_instructions for linux & MacOS, I haven't figured out how to run `make` on Windows, winget installs it but the command is still 'not found'..._
+
+```sh
+git clone https://github.com/ewen-lbh/ffcss
+cd ffcss
+make 
+make tests # optional, to make sure everything works
+make install
+```
+
+## Usage
+
+```docopt
+ffcss - Apply and configure FirefoxCSS themes
+
+Usage:
+	ffcss [options] use THEME_NAME [VARIANT]
+	ffcss [options] get THEME_NAME
+	ffcss [options] cache clear
+	ffcss [options] init
+	ffcss [options] reapply
+	ffcss version [COMPONENT]
+
+Where:
+	THEME_NAME  a theme name or URL (see README.md)
+	COMPONENT   is either major, minor or patch (to get a single digit)
+
+Options:
+	-a --all-profiles           Apply the theme to all profiles
+	-p --profiles=PATHS      Select which profiles to apply the theme to.
+	                         Can be absolute or relative to --profiles-dir.
+							 Comma-separated.
+	--profiles-dir=PATH      Directory that contains profile directories.
+	                         Default value is platform-specific:
+	                         - $HOME/.mozilla/firefox                                on Linux
+	                         - $HOME/Library/Application Support/Firefox/Profiles    on MacOS
+	                         - %appdata%/Roaming/Mozilla/Firefox/Profiles            on Windows
+	--skip-manifest-source   Don't ask to show the manifest source
+```
+
+#### The `use` command
+
+Synopsis: `ffcss use THEME_NAME [VARIANT_NAME]`
+
+ffcss will first search for a folder at `~/.cache/ffcss/THEME_NAME/VARIANT_NAME`. If not found, it will try to download the theme:
+
+
+If `THEME_NAME` is of the form `OWNER/REPO`:
+
+- It'll use the repository `github.com/OWNER/REPO`
+  
+If `THEME_NAME` is of the form `NAME`:
+
+- It'll search for `NAME` in the manifests included with ffcss (see [Built-in themes](#built-in-themes))
+
+The `NAME` here is insensitive to case, whitespace, punctuation (`.`, ` `, `_` and `-`) and unicode ([NFD normalization](http://www.macchiato.com/unicode/nfc-faq) is applied before searching). 
+
+That means that typing `ffcss use FrozenFox` will work even if the theme's manifest is stored in `frozenfox.yaml`, and not `FrozenFox.yaml`.
+
+And if `THEME_NAME` is an URL:
+
+- It'll download the zip file / clone the git repository at `THEME_NAME` (the `https://` part can be omitted)
+
+_Technical note: when no variant is used, `VARIANT_NAME` is "\_"_
+
+<!-- ### The `config` command
+
+Synopsis: `ffcss config KEY [VALUE]`
+
+Much simpler than the `use` command, this one just adds convenience to set `about:config` keys. If `VALUE` is not provided, ffcss will output the specified `KEY`'s current value. -->
+
+### The `reapply` command
+
+Synopsis: `ffcss reapply`
+
+This is the same as doing `ffcss use` with the current theme, useful when firefox updates.
+
+The current theme for each profile is stored in ffcss' configuration folder, in `currently.yaml`
+
+### The `get` command
+
+This is the same as running `use`, but does not actually apply the theme, it just downloads it to the cache.
+
+### The `cache clear` command
+
+Clears the ffcss cache, including all downloaded themes.
+
+### The `init` command
+
+Synopsis: `ffcss init`
+
+Creates a [`ffcss` manifest file](#creating-a-firefoxcss-theme) in the current directory
+
+## What if the theme I want to download has no manifest?
+
+You can create one for yourself, by placing it in `<home folder>/.config/ffcss/<your theme's name>.yaml`, along with the other included themes, then running `ffcss use` with that name.
+
+Consider opening a pull request if you can, so that others can benefit from your manifest without having to write it again.
+
+The next section covers how to create that manifest file. It is written for theme makers, but the only difference is that the manifest you wrote isn't named `ffcss.yaml`.
+
+## Creating a FirefoxCSS theme
+
+So that your users can benefit from the simple installation process provided by ffcss, you can add a `ffcss.yaml` file in the root of your project and declare additional configuration you might need. 
+
+_Note: example use cases from real FirefoxCSS themes are given for illustrative purposes, and I don't claim to speak in place of the theme's author(s), even if it may appear as so_.
+
+You can start by using `ffcss init` in your theme's folder, some values will be filled automatically.
+
+
+### Download
+
+You can specify in your manifest file from where to download the theme.
+This can be useful if
+
+- you want to use that value in `variants`, to download a different zip file
+- you want to [add your manifest to the registry](#add-to-the-registry), in which case the repository's URL is needed for ffcss to figure out where to download your theme from
+- you want users to download a zip file instead of cloning your repo
+
+### Branch, Tag & Commit
+
+If you use a git repository URL (ffcss will know if any given URL is `git-clone`-able), you can also use these three to:
+
+- change the branch used (`branch: <the branch's name>`)
+- change the commit used (`commit: <the commit's SHA>`)
+- change the tag used (`tag: <the git tag's name>`)
+
+Here again, this is especially useful for variants (for example, Lepton's photon-style variant is on a separate branch `photon-style`)
+
+### Config
+
+An object mapping `about:config` configuration keys to their values:
+
+```yaml
+config:
+    svg.context-properties.content.enabled: true
+    security.insecure_connection_text.enabled: true
+```
+
+Note that `toolkit.legacyUserProfileCustomizations.stylesheets` is set to `true` automatically, no need to declare it.
+
+This will write a generated `user.js` file in the selected profile directory (or directories).
+If you provide your own `user.js` file, the generated content will be appended to yours and written on the same file.
+
+### Files
+
+You can use `userChrome`, `userContent` and `user.js` keys to specify where those files are in your repo. You can use [glob patterns][globster].
+
+They will get copied to `<selected profile directory>/chrome/userChrome.css` (or `userContent.css`) and `<selected profile directory>/user.js>`, respectively.
+
+Note that even if _your_ file is not called like this, if will get copied with that name, no manual renaming required.
+
+For example, SimplerentFox uses three different `userContent.css` files for its variants, and the manifest has `{{ os }}/userContent__{{ variant }}.css`.
+
+<!--If not specified, their default values are either `userChrome.css` (or `userContent.css`, or `user.js`) or `null` when the default file is not found.-->
+
+You can also declare other files in `assets`, an array of [glob patterns][globster]. They take precedence over the others keys, since they get copied last.
+
+You can use `{{ os }}`, which will get replaced with one of `windows`, `linux` or `macos`, and `{{ variant }}`, which will get replaced by the variant the user has chosen.
+
+
+All files will get copied to `<user's profile folder>/chrome/`. You can change the from where they get copied with `copy from`.
+This is pretty useful if you store your assets inside a folder (such as `chrome`), to avoid them being nested too much
+
+```yaml
+assets: 
+    - my-assets/**
+copy from: my-assets/ 
+```
+
+without the `copy from`, files would get copied to `<user's profile folder>/chrome/my-assets/...` instead of `<user's profile folder>/chrome/...`: `my-assets/` will be a part of the file names described by `my-assets/**`.
+
+For example, MaterialFox stores everything under `chrome/`, and its manifest uses `copy from: chrome/` to tell ffcss to copy assets _from that directory_, not from the repository's root.
+
+[globster]: https://globster.xyz
+
+### Variants
+
+Some themes allow users to choose between different variations. To declare them, add an object with key `variants`, that maps variant names to a configuration object, overriding `config`, `userContent`, etc. for that variation. <!--An additional `description` key is available and will be shown to users when selecting a variant.-->
+
+Note that overriding `config` only overrides values set, it does not remove configuration keys that have been set globally: with the following manifest:
+
+```yaml
+ffcss: 1 # signals that the manifest works with ffcss versions 1.X.X
+
+config:
+    one.property: yes
+    another.property: buckaroo
+
+variants:
+    blue:
+        config:
+            one.property: false
+```
+
+choosing the variant "blue" will apply the following config:
+
+```yaml
+one.property: false
+another.property: buckaroo
+```
+
+Properties that can be overriden are:
+
+- repository 
+- branch
+- tag
+- commit
+- config
+- userChrome
+- userContent
+- user.js
+- assets
+- addons
+- run
+- description
+
+
+For example, SimplerentFox proposes an addon to enhance the experience, and declares it as such:
+
+```yaml
+...
+
+addons:
+    - https://addons.mozilla.org/en-US/firefox/addon/simplerentfox/
+
+...
+```
+
+### Running custom commands
+
+You can run any shell command after and/or before the installation, with the manifest entries `run`.`before` and `run`.`after`:
+
+```yaml
+run:
+  before: echo hello from {{ profile_path }}!
+  after: wget https://example.com/my-custom-file?version={{ firefox_version }}
+```
+
+In both values, `{{ profile_path }}` and `{{ firefox_version }}` will respectively get replaced with the path of the profile to which the theme is being installed, and that profile's firefox version.
+
+### Messages
+
+You can specify a message to be printed at the end of the installation. Markdown syntax is supported.
+
+Useful to provide additional instructions that can't be automated, to suggest optional tweaks, to do shameless promotion or even just to say thanks! 
+
+For example, VerticalTabs uses messages to tell users about additional installation steps that can't be automated:
+
+```yaml
+message: |
+  Go to about:addons, select Tab Center Reborn, go to Preferences and set:
+
+  - *Animations*: on.
+  - *Use current browser theme*: on, if you want to use dark mode.
+  - *Compact Mode*: either “Dynamic” or “Enabled”. It works with “Disabled” too but looks nicer with only favicons.
+  - *Favicon-only pinned tabs*: off.
+  - *Activate Custom Stylesheet* and paste the contents of tabCenterReborn.css into the text area, and click “Save CSS”.
+```
+
+### Description & By
+
+Those two fields can be set to be displayed before the installation begins. Something like
+
+
+```
+Installing <name> by <by>
+
+| <description>
+```
+
+Will then be shown to the user when starting the installation
+
+### Declaring ffcss' version
+
+Putting a `ffcss: 0` in your manifest tells the user that this theme was made with ffcss version 0.X.X. 
+If their version differs, they'll get a warning: major version changes (i.e. when the leftmost number changes) may include _breaking changes_ regarding the manifest file, which means that your theme might not work.
+
+I'll try to not do that though, it sucks to break things. (I may support _different versions of manifest files_, at least)
+
+### Declaring supported Firefox versions
+
+The manifest entry `firefox` can be used to specify which versions of Firefox are compatible with your theme.
+
+Users that have a non-compatible Firefox version will be shown a warning.
+
+The following patterns can be used:
+
+- `<version>` - compatible with that version only.
+
+    For example:
+
+    ```yaml
+    firefox: 90
+    ```
+- `<A>-<B>` - compatible with every version from _A_ to _B_ (including both _A_ and _B_)
+
+    For example:
+
+    ```yaml
+    firefox: 86-90
+    ```
+
+- `<A>+` - compatible with version _A_ or more recent 
+
+    For example:
+
+    ```yaml
+    firefox: 90+
+    ```
+
+- `up to <A>` - compatible with every version, up to _A_ (including _A_)
+
+    For example:
+
+    ```yaml
+    firefox: up to 88
+    ```
+
+Note that you can also be more precise and specify the minor part (the second digit after the dot), for example `firefox: 90.5+`.
+
+### Examples
+
+You can look at some themes' manifests in `themes/*.yaml` to see how they are written.
+
+## Built-in themes
+
+The following themes are provided with ffcss, you can install them with `ffcss use <theme's name>`.
+
+- [alpenblue](https://github.com/Godiesc/AlpenBlue) by [Godiesc](https://github.com/Godiesc)
+- [australis-tabs](https://github.com/sagars007/Australis-like-tabs-FF-ProtonUI-changes) by [sagars007](https://github.com/sagars007)
+- [blurredfox](https://github.com/manilarome/blurredfox) by [manilarome](https://github.com/manilarome)
+- [chameleons-beauty](https://github.com/Godiesc/Chameleons-Beauty) by [Godiesc](https://github.com/Godiesc)
+- [compactmode](https://github.com/Godiesc/compactmodefirefoxcss) by [Godiesc](https://github.com/Godiesc)
+- [diamondfucsia](https://github.com/Godiesc/DiamondFucsia) by [Godiesc](https://github.com/Godiesc)
+- [frozenfox](https://github.com/tortious/FrozenFox) by [tortious](https://github.com/tortious)
+- [fxcompact](https://github.com/dannycolin/fx-compact-mode) by [dannycolin](https://github.com/dannycolin)
+- [halo](https://github.com/seirin-blu/Firefox-Halo) by [seirin-blu](https://github.com/seirin-blu)
+- [lepton](https://github.com/black7375/Firefox-UI-Fix) by [black7375](https://github.com/black7375)
+- [martinfox](https://github.com/arp242/MartinFox) by [arp242](https://github.com/arp242)
+- [materialfox](https://github.com/muckSponge/MaterialFox) by [muckSponge](https://github.com/muckSponge)
+- [modoki](https://github.com/soup-bowl/Modoki-FirefoxCSS) by [soup-bowl](https://github.com/soup-bowl)
+- [montereyfox](https://github.com/FirefoxCSSThemers/Monterey-Fox) by [FirefoxCSSThemers](https://github.com/FirefoxCSSThemers)
+- [pro-fox](https://github.com/xmansyx/Pro-Fox) by [xmansyx](https://github.com/xmansyx)
+- [proton-connected-rounded-tabs](https://github.com/sagars007/Proton-UI-connected-rounded-tabs) by [sagars007](https://github.com/sagars007)
+- [simplerentfox](https://github.com/MiguelRAvila/SimplerentFox) by [MiguelRAvila](https://github.com/MiguelRAvila)
+- [technetium](https://github.com/edo0/Technetium) by [edo0](https://github.com/edo0)
+- [verticaltabs](https://github.com/ranmaru22/firefox-vertical-tabs) by [ranmaru22](https://github.com/ranmaru22)
+- [wavefox](https://github.com/QNetITQ/WaveFox) by [QNetITQ](https://github.com/QNetITQ)
+
+### Add to registry
+
+You can add your theme to this list, just fork the repository, copy your `ffcss.yaml` theme as `themes/<your theme's name>.yaml`, and submit a PR!
diff --git a/addons.go b/addons.go
new file mode 100644
index 0000000..eb8c6d7
--- /dev/null
+++ b/addons.go
@@ -0,0 +1,30 @@
+package ffcss
+
+import (
+	"fmt"
+	"os/exec"
+)
+
+// InstallAddon install the addon at addonURL on this profile.
+// Currently, it justs opens firefox with that URL so that the user can manually install it (hence the operatingSystem argument).
+func (ffp FirefoxProfile) InstallAddon(operatingSystem string, addonURL string) error {
+	LogStep(1, "Opening [blue][bold]%s", addonURL)
+	LogStep(1, "[yellow]Waiting for you to close Firefox")
+	var command *exec.Cmd
+	switch operatingSystem {
+	case "linux":
+		command = exec.Command("firefox", "--new-tab", addonURL, "--profile", ffp.Path)
+	case "macos":
+		command = exec.Command("open", "-a", "firefox", addonURL, "--args", "--profile", ffp.Path)
+	case "windows":
+		command = exec.Command("start", "firefox", "-profile", ffp.Path, addonURL)
+	default:
+		LogWarning("unrecognized OS %s, cannot open firefox automatically. Open %s in firefox using profile %s", operatingSystem, addonURL, ffp)
+		return nil
+	}
+	err := command.Run()
+	if err != nil {
+		return fmt.Errorf("couldn't open %q: while running %s: %w", addonURL, command.String(), err)
+	}
+	return nil
+}
diff --git a/addons_test.go b/addons_test.go
new file mode 100644
index 0000000..b66fdc4
--- /dev/null
+++ b/addons_test.go
@@ -0,0 +1,30 @@
+package ffcss
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestInstallAddon(t *testing.T) {
+	// Empty the buffer
+	mockedStdout = bytes.Buffer{}
+	BaseIndentLevel = 0
+	mockedProfile.InstallAddon("linux", "https://example.com")
+	// assert.NoError(t, err) FIXME firefox won't open with the given profile even if the mocked .mozilla/firefox dir is copied exactly from my ~
+	assert.Equal(t,
+		"  \x1b[36m•\x1b[0m Opening \x1b[34m\x1b[1mhttps://example.com\x1b[0m\n  \x1b[36m•\x1b[0m \x1b[33mWaiting for you to close Firefox\x1b[0m\n",
+		mockedStdout.String(),
+	)
+
+	// Empty the buffer
+	mockedStdout = bytes.Buffer{}
+	BaseIndentLevel = 0
+	err := mockedProfile.InstallAddon("goretijgoierjogirej", "https://example.com")
+	assert.NoError(t, err)
+	assert.Equal(t,
+		"  \x1b[36m•\x1b[0m Opening \x1b[34m\x1b[1mhttps://example.com\x1b[0m\n  \x1b[36m•\x1b[0m \x1b[33mWaiting for you to close Firefox\x1b[0m\n\x1b[33m\x1b[1munrecognized OS goretijgoierjogirej, cannot open firefox automatically. Open https://example.com in firefox using profile default-release (667ekipp)\n\x1b[0m",
+		mockedStdout.String(),
+	)
+}
diff --git a/assets.go b/assets.go
new file mode 100644
index 0000000..0b29826
--- /dev/null
+++ b/assets.go
@@ -0,0 +1,72 @@
+package ffcss
+
+import (
+	"fmt"
+	"path/filepath"
+	"strings"
+
+	"github.com/bmatcuk/doublestar"
+	"github.com/hoisie/mustache"
+)
+
+func renderFileTemplate(f FileTemplate, operatingSystem string, variant Variant, osRenameMap map[string]string) string {
+	if strings.Contains(strings.Trim(f, " "), "{{variant}}") && variant.Name == "" {
+		LogWarning("%q uses {{variant}} which is empty\n", f)
+	}
+	var osName string
+	if osRenameMap[operatingSystem] == "" {
+		osName = operatingSystem
+	} else {
+		osName = osRenameMap[operatingSystem]
+	}
+	return mustache.Render(f, map[string]string{
+		"os":      osName,
+		"variant": variant.Name,
+	})
+}
+
+// DestinationPathOfAsset computes the destination path of some asset from its path and the destination profile directory
+// It is assumed that assetPath is absolute.
+func (t Theme) DestinationPathOfAsset(assetPath string, profileDir string, operatingSystem string, variant Variant) (string, error) {
+	if !strings.HasPrefix(assetPath, t.DownloadedTo) {
+		return "", fmt.Errorf("asset %q is outside of the theme's root %q", assetPath, t.DownloadedTo)
+	}
+
+	relativeTo := filepath.Clean(filepath.Join(t.DownloadedTo, filepath.Clean(renderFileTemplate(t.CopyFrom, operatingSystem, variant, t.OSNames))))
+	if !strings.HasPrefix(relativeTo, t.DownloadedTo) {
+		return "", fmt.Errorf("copy from %q is outside of the theme's root %q", relativeTo, t.DownloadedTo)
+	}
+
+	relativised, err := filepath.Rel(relativeTo, assetPath)
+	if err != nil {
+		return "", fmt.Errorf("couldn't make %s relative to %s: %w", assetPath, filepath.Join(t.DownloadedTo, filepath.Clean(t.CopyFrom)), err)
+	}
+
+	return filepath.Join(profileDir, "chrome", relativised), nil
+}
+
+// AssetsPaths returns the individual file paths of all assets.
+func (t Theme) AssetsPaths(os string, variant Variant) ([]string, error) {
+	resolvedFiles := make([]string, 0)
+	for _, template := range t.Assets {
+		glob := renderFileTemplate(template, os, variant, t.OSNames)
+		LogDebug("looking for assets: globbing %q", filepath.Join(t.DownloadedTo, glob))
+		glob = filepath.Clean(filepath.Join(t.DownloadedTo, glob))
+		files, err := doublestar.Glob(glob)
+		if err != nil {
+			return resolvedFiles, fmt.Errorf("while getting all matches of glob %s: %w", glob, err)
+		}
+		// If no matches
+		if len(files) < 1 {
+			// If it's _really_ a glob pattern
+			if strings.Contains(glob, "*") {
+				return resolvedFiles, fmt.Errorf("glob pattern %s matches no files", glob)
+			}
+			// If it's just a regular file (that was treated as a glob pattern)
+			return resolvedFiles, fmt.Errorf("file %s not found", glob)
+		}
+		// For each match of the glob pattern
+		resolvedFiles = append(resolvedFiles, files...)
+	}
+	return resolvedFiles, nil
+}
diff --git a/assets_test.go b/assets_test.go
new file mode 100644
index 0000000..5ee8733
--- /dev/null
+++ b/assets_test.go
@@ -0,0 +1,109 @@
+package ffcss
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRenderFileTemplate(t *testing.T) {
+	assert.Equal(t, "userChrome.css", renderFileTemplate(
+		"userChrome.css",
+		"linux",
+		Variant{},
+		map[string]string{},
+	))
+
+	assert.Equal(t, "linux.css", renderFileTemplate(
+		"linux.css",
+		"linux",
+		Variant{},
+		map[string]string{"linux": "Linux"},
+	))
+
+	assert.Equal(t, "GNU/Linux.css", renderFileTemplate(
+		"{{os}}.css",
+		"linux",
+		Variant{},
+		map[string]string{"linux": "GNU/Linux"},
+	))
+
+	assert.Equal(t, "linux.css", renderFileTemplate(
+		"linux.css",
+		"windows",
+		Variant{},
+		map[string]string{},
+	))
+
+	assert.Equal(t, "./macos/rainbow.css", renderFileTemplate(
+		"./{{ os }}/{{variant}}.css",
+		"macos",
+		Variant{Name: "rainbow"},
+		map[string]string{},
+	))
+
+}
+
+func TestDestinationPathOfAsset(t *testing.T) {
+	manifest := Theme{
+		DownloadedTo: CacheDir("simplerentfox/_"),
+		ExplicitName: "materialfox",
+		Variants:     map[string]Variant{},
+		Config:       Config{},
+	}
+
+	file, err := manifest.DestinationPathOfAsset("/home/ewen/lol.pdf", testarea, "linux", Variant{})
+	if assert.Error(t, err) {
+		assert.Regexp(t, `asset ".+" is outside of the theme's root ".+"`, err.Error())
+		assert.Equal(t, "", file)
+	}
+
+	file, err = manifest.DestinationPathOfAsset("testarea/home/.cache/ffcss/simplerentfox/../../../lol.pdf", testarea, "linux", Variant{})
+	if assert.Error(t, err) {
+		assert.Regexp(t, `asset ".+" is outside of the theme's root ".+"`, err.Error())
+		assert.Equal(t, "", file)
+	}
+
+	manifest = Theme{
+		DownloadedTo: CacheDir("simplerentfox/_"),
+		ExplicitName: "simplerentfox",
+		Variants: map[string]Variant{
+			"WithoutURLBar": {},
+			"OneLine":       {},
+		},
+		CopyFrom: "{{ os }}/",
+		OSNames: map[string]string{
+			"linux": "Linux",
+		},
+	}
+
+	file, err = manifest.DestinationPathOfAsset(CacheDir("simplerentfox/_/Linux/userChrome__OneLine.css"), testarea, "linux", Variant{Name: "OneLine"})
+	assert.NoError(t, err)
+	assert.Equal(t, filepath.Join(testarea, "chrome", "userChrome__OneLine.css"), file)
+}
+
+func TestAssetsPaths(t *testing.T) {
+	simplerentfox := Theme{
+		ExplicitName: "simplerentfox",
+		DownloadedTo: CacheDir("simplerentfox/_"),
+		Variants: map[string]Variant{
+			"WithoutURLBar": {},
+			"OneLine":       {},
+		},
+		Assets: []string{"./{{ os }}/userChrome__{{ variant }}.css"}, // for the purposes of testing
+		OSNames: map[string]string{
+			"linux":   "Linux",
+			"macos":   "Linux",
+			"windows": "Windows",
+		},
+	}
+
+	files, err := simplerentfox.AssetsPaths("linux", Variant{Name: "blue"})
+	assert.Regexp(t, "file .+ not found", err.Error())
+	assert.Equal(t, []string{}, files)
+
+	files, err = simplerentfox.AssetsPaths("linux", Variant{Name: "OneLine"})
+	assert.NoError(t, err)
+	assert.Equal(t, []string{CacheDir("simplerentfox/_/Linux/userChrome__OneLine.css")}, files)
+}
diff --git a/cache.go b/cache.go
new file mode 100644
index 0000000..ca0202a
--- /dev/null
+++ b/cache.go
@@ -0,0 +1,26 @@
+package ffcss
+
+import (
+	"fmt"
+	"os"
+)
+
+// CleanDownloadArea removes the temporary download area used to download themes before knowing their name from their manifest
+func CleanDownloadArea() error {
+	return os.RemoveAll(CacheDir(TempDownloadsDirName))
+}
+
+// ClearWholeCache destroys the cache directory
+func ClearWholeCache() error {
+	err := os.RemoveAll(CacheDir())
+	if err != nil {
+		return fmt.Errorf("while deleting directory tree %q: %w", CacheDir(), err)
+	}
+
+	err = os.Mkdir(CacheDir(), 0777)
+	if err != nil {
+		return fmt.Errorf("while re-creating empty cache directory at %q: %w", CacheDir(), err)
+	}
+
+	return nil
+}
diff --git a/cache_test.go b/cache_test.go
new file mode 100644
index 0000000..1dd7e26
--- /dev/null
+++ b/cache_test.go
@@ -0,0 +1,40 @@
+package ffcss
+
+import (
+	"io/fs"
+	"os"
+	"os/exec"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func fillCache() {
+	exec.Command("cp", "-r", "testdata/home/.cache", "testarea/home")
+	LogDebug("filled mock cache")
+}
+
+func TestCleanDownloadArea(t *testing.T) {
+	fillCache()
+	initialCacheDir, _ := os.ReadDir(CacheDir())
+	expectedCacheDir := make([]fs.DirEntry, 0)
+	for _, dirEntry := range initialCacheDir {
+		if dirEntry.Name() != ".download" {
+			expectedCacheDir = append(expectedCacheDir, dirEntry)
+		}
+	}
+	err := CleanDownloadArea()
+	assert.NoError(t, err)
+	actualCacheDir, err := os.ReadDir(CacheDir())
+	assert.NoError(t, err)
+	assert.Equal(t, expectedCacheDir, actualCacheDir)
+}
+
+func TestClearWholeCache(t *testing.T) {
+	fillCache()
+	err := ClearWholeCache()
+	assert.NoError(t, err)
+	actual, err := os.ReadDir(CacheDir())
+	assert.NoError(t, err)
+	assert.Equal(t, []fs.DirEntry{}, actual)
+}
diff --git a/catalog.go b/catalog.go
new file mode 100644
index 0000000..7b4df30
--- /dev/null
+++ b/catalog.go
@@ -0,0 +1,77 @@
+package ffcss
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+
+	"github.com/hbollon/go-edlib"
+	"golang.org/x/text/unicode/norm"
+)
+
+// Catalog represents a collection of themes
+type Catalog map[string]Theme
+
+// Lookup looks up a theme by its name in the theme store.
+// It also returns an error starting with "did you mean:" when
+// a theme name is not found but themes with similar names exist.
+func (store Catalog) Lookup(query string) (Theme, error) {
+	originalQuery := query
+	query = lookupPreprocess(query)
+	LogDebug("using query %q", query)
+	processedThemeNames := make([]string, 0, len(store))
+	for _, theme := range store {
+		LogDebug("\tlooking up against %q (%q)", lookupPreprocess(theme.Name()), theme.Name())
+		if lookupPreprocess(theme.Name()) == query {
+			return theme, nil
+		}
+		processedThemeNames = append(processedThemeNames, lookupPreprocess(theme.Name()))
+	}
+	// Use fuzzy search for did-you-mean errors
+	suggestion, _ := edlib.FuzzySearchThreshold(query, processedThemeNames, 0.75, edlib.Levenshtein)
+
+	if suggestion != "" {
+		return Theme{}, fmt.Errorf("theme %q not found. did you mean [blue][bold]%s[reset]?", originalQuery, suggestion)
+	}
+	return Theme{}, fmt.Errorf("theme %q not found", originalQuery)
+}
+
+// lookupPreprocess applies transformations to s so that it can be compared
+// to search for something.
+// For example, it is used by (ThemeStore).Lookup
+func lookupPreprocess(s string) string {
+	return strings.ToLower(regexp.MustCompile(`[-_ .]`).ReplaceAllString(norm.NFKD.String(s), ""))
+}
+
+// LoadCatalog loads a directory of theme manifests.
+// Keys are theme names (files' basenames with the .yaml removed).
+func LoadCatalog(storeDirectory string) (themes Catalog, err error) {
+	themeNamePattern := regexp.MustCompile(`^(.+)\.ya?ml$`)
+	themes = make(Catalog)
+	manifests, err := os.ReadDir(storeDirectory)
+	if err != nil {
+		return
+	}
+	LogDebug("loading potential themes %v into catalog", func() []string {
+		dirNames := make([]string, 0, len(manifests))
+		for _, dir := range manifests {
+			dirNames = append(dirNames, dir.Name())
+		}
+		return dirNames
+	}())
+	for _, manifest := range manifests {
+		if !themeNamePattern.MatchString(manifest.Name()) {
+			continue
+		}
+		themeName := themeNamePattern.FindStringSubmatch(manifest.Name())[1]
+		theme, err := LoadManifest(filepath.Join(storeDirectory, manifest.Name()))
+		if err != nil {
+			return nil, fmt.Errorf("while loading theme %q: %w", themeName, err)
+		}
+		LogDebug("\tadding theme from manifest %q", manifest.Name())
+		themes[themeName] = theme
+	}
+	return
+}
diff --git a/catalog_test.go b/catalog_test.go
new file mode 100644
index 0000000..33384d4
--- /dev/null
+++ b/catalog_test.go
@@ -0,0 +1,71 @@
+package ffcss
+
+import (
+	"path/filepath"
+	"sort"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func makeTheme(name string) Theme {
+	t := NewTheme()
+	t.ExplicitName = name
+	return t
+}
+
+func TestCatalogLookup(t *testing.T) {
+	catalog := Catalog{
+		"lorem": makeTheme("lorem"),
+		"ipsum": makeTheme("ipsum"),
+		"dolor": makeTheme("a_completely_different_name"),
+		"bacon": makeTheme("bacon"),
+	}
+
+	actual, err := catalog.Lookup("lorem")
+	assert.NoError(t, err)
+	assert.Equal(t, makeTheme("lorem"), actual)
+
+	actual, err = catalog.Lookup("a.completely different-name")
+	assert.NoError(t, err)
+	assert.Equal(t, makeTheme("a_completely_different_name"), actual)
+
+	actual, err = catalog.Lookup("dolor")
+	assert.Error(t, err, `theme "dolor" not found`)
+	assert.Equal(t, Theme{}, actual)
+
+	actual, err = catalog.Lookup("baaaa con")
+	assert.Error(t, err, `theme "baaaa con" not found. did you mean [blue][bold]bacon[reset]?`)
+	assert.Equal(t, Theme{}, actual)
+}
+
+func TestLookupPreprocess(t *testing.T) {
+	cases := []struct{ in, out string }{
+		{"same", "same"},
+		{"B  O   N    K", "bonk"},
+		{"you. shall. not. pass.", "youshallnotpass"},
+		{"wHaTevER", "whatever"},
+		{"all-thE.puncT_uAtio    n", "allthepunctuation"},
+		{"with great power\u037e comes great responsibility.", "withgreatpower;comesgreatresponsibility"},
+		{"𝔽𝔸ℕℂ𝕐", "fancy"},
+	}
+
+	for _, caze := range cases {
+		assert.Equal(t, caze.out, lookupPreprocess(caze.in))
+	}
+}
+
+func TestLoadCatalog(t *testing.T) {
+	actual, err := LoadCatalog(filepath.Join(testarea, "catalogs/empty"))
+	assert.NoError(t, err)
+	assert.Equal(t, make(Catalog), actual)
+
+	actual, err = LoadCatalog(filepath.Join(testarea, "catalogs/various"))
+	assert.NoError(t, err)
+	actualNames := make([]string, 0, len(actual))
+	for name := range actual {
+		actualNames = append(actualNames, name)
+	}
+	sort.Strings(actualNames)
+	assert.Equal(t, []string{"alpenblue", "flyingfox", "montereyfox", "sometheme"}, actualNames)
+}
diff --git a/cmd/ffcss/USAGE b/cmd/ffcss/USAGE
new file mode 100644
index 0000000..727ce37
--- /dev/null
+++ b/cmd/ffcss/USAGE
@@ -0,0 +1,27 @@
+ffcss - Apply and configure FirefoxCSS themes
+
+Usage:
+	ffcss [options] use THEME_NAME [VARIANT]
+	ffcss [options] get THEME_NAME
+	ffcss [options] cache clear
+	ffcss [options] init
+	ffcss [options] reapply
+	ffcss [options] reset
+	ffcss [options] version [COMPONENT]
+
+Where:
+	THEME_NAME  a theme name or URL (see README.md)
+	COMPONENT   is either major, minor or patch (to get a single digit)
+
+Options:
+	-a --all-profiles        Apply the theme to all profiles
+	-p --profiles=PATHS      Select which profiles to apply the theme to.
+	                         Can be absolute or relative to --profiles-dir.
+							 Comma-separated.
+	--profiles-dir=PATH      Directory that contains profile directories.
+	                         Default value is platform-specific:
+	                         - $HOME/.mozilla/firefox                                on Linux
+	                         - $HOME/Library/Application Support/Firefox/Profiles    on MacOS
+	                         - %appdata%/Roaming/Mozilla/Firefox/Profiles            on Windows
+	-d --default-profile     Apply the themes to the default profile (ending with default-release)
+	--skip-manifest-source   Don't ask to show the manifest source
diff --git a/cmd/ffcss/commands.go b/cmd/ffcss/commands.go
new file mode 100644
index 0000000..90cf165
--- /dev/null
+++ b/cmd/ffcss/commands.go
@@ -0,0 +1,290 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+
+	"github.com/docopt/docopt-go"
+	"github.com/ewen-lbh/ffcss"
+	"gopkg.in/yaml.v2"
+)
+
+func runCommandUse(args flagsAndArgs) error {
+	err := ffcss.CreateDataDirectories()
+	if err != nil {
+		return err
+	}
+
+	ffcss.LogStep(0, "Resolving the theme's name")
+	uri, typ, err := ffcss.ResolveURL(args.string("THEME_NAME"))
+	if err != nil {
+		return fmt.Errorf("while resolving name %s: %w", args.string("THEME_NAME"), err)
+	}
+
+	ffcss.LogStep(0, "Downloading the theme")
+	manifest, err := ffcss.Download(uri, typ)
+	if err != nil {
+		return err
+	}
+
+	ffcss.DescribeTheme(manifest, ffcss.BaseIndentLevel)
+
+	manifest.AskToSeeManifestSource(args.bool("--skip-manifest-source"))
+
+	// Detect OS
+	operatingSystem := ffcss.GOOStoOS(runtime.GOOS)
+
+	selectedProfiles, err := ffcss.SelectProfiles(
+		args.strings("--profiles"),
+		args.string("--profiles-dir"),
+		args.bool("--default-profile"),
+		args.bool("--all-profiles"),
+	)
+	if err != nil {
+		return err
+	}
+
+	if len(selectedProfiles) == 0 {
+		return nil
+	}
+	singleProfile := len(selectedProfiles) == 1
+
+	incompatibleProfiles, err := manifest.IncompatibleProfiles(selectedProfiles)
+	if err != nil {
+		return fmt.Errorf("while checking for incompatible profiles: %w", err)
+	}
+
+	if len(incompatibleProfiles) != 0 {
+		ffcss.LogStep(1, "[yellow]This theme ensures compatibility with firefox [bold]%s[reset][yellow]. The following themes could be incompatible:", manifest.FirefoxVersionConstraint.Sentence)
+		for _, profile := range incompatibleProfiles {
+			ffcss.LogStep(2, "%s [dim]([reset]version [blue][bold]%s[reset][dim])", profile.Profile, profile.Version)
+		}
+	}
+
+	// Choose variant
+	variant := ffcss.Variant{}
+	if len(manifest.AvailableVariants()) > 0 {
+		variantName, _ := args.String("VARIANT")
+		if variantName == "" {
+			var cancel bool
+			variant, cancel = manifest.ChooseVariant()
+			if cancel {
+				return nil
+			}
+		} else {
+			var found bool
+			variant, found = manifest.Variants[variantName]
+			if !found {
+				return fmt.Errorf("variant %q does not exist on this theme. Available variants are %s", variantName, strings.Join(manifest.AvailableVariants(), ", "))
+			}
+		}
+		manifest, actionsNeeded := manifest.WithVariant(variant)
+		manifest.ReDownloadIfNeeded(actionsNeeded)
+	}
+
+	// Check for OS compatibility
+	manifest.WarnIfIncompatibleWithOS(operatingSystem)
+
+	// For each profile directory...
+	if singleProfile {
+		ffcss.BaseIndentLevel--
+	}
+	for _, profile := range selectedProfiles {
+		if !singleProfile {
+			ffcss.LogStep(0, "With profile "+filepath.Base(profile.Path))
+		}
+
+		ffcss.LogStep(1, "Backing up the current theme")
+		err = profile.BackupChrome()
+		if err != nil {
+			return fmt.Errorf("while backing up chrome directory: %w", err)
+		}
+		err = profile.BackupUserJS()
+		if err != nil {
+			return fmt.Errorf("while backing up user.js: %w", err)
+		}
+
+		// Run pre-install script
+		if manifest.Run.Before != "" {
+			ffcss.LogStep(1, "Running pre-install script")
+			// TODO for this to be useful, print commandline _with mustaches replaced_:  Step(baseIndent+2, "[dim]$ bash -c [reset][bold]%s", manifest.Run.Before)
+			output, err := manifest.RunPreInstallHook(profile)
+			if err != nil {
+				return fmt.Errorf("while running pre-install script: %w", err)
+			}
+			ffcss.ShowHookOutput(output)
+		}
+
+		err := os.Mkdir(filepath.Join(profile.Path, "chrome"), 0700)
+		if err != nil {
+			return err
+		}
+
+		// Install stuff
+		ffcss.LogStep(1, "Installing the theme")
+		err = manifest.InstallUserChrome(operatingSystem, variant, profile.Path)
+		if err != nil {
+			return fmt.Errorf("couldn't install userChrome.css: %w", err)
+		}
+
+		err = manifest.InstallUserContent(operatingSystem, variant, profile.Path)
+		if err != nil {
+			return fmt.Errorf("couldn't install userContent.css: %w", err)
+		}
+
+		err = manifest.InstallUserJS(operatingSystem, variant, profile.Path)
+		if err != nil {
+			return fmt.Errorf("couldn't install user.js: %w", err)
+		}
+
+		err = manifest.InstallAssets(operatingSystem, variant, profile.Path)
+		if err != nil {
+			return fmt.Errorf("couldn't install assets: %w", err)
+		}
+
+		// Run post-install script
+		if manifest.Run.After != "" {
+			ffcss.LogStep(1, "Running post-install script")
+			// TODO for this to be useful, print commandline _with mustaches replaced_:  Step(baseIndent+2, "[dim]$ bash -c [reset][bold]%s", manifest.Run.After)
+			output, err := manifest.RunPostInstallHook(profile)
+			if err != nil {
+				return fmt.Errorf("while running post-install script: %w", err)
+			}
+			ffcss.ShowHookOutput(output)
+		}
+
+		err = profile.RegisterCurrentTheme(args.string("THEME_NAME"))
+		if err != nil {
+			return fmt.Errorf("while registering current theme for profile %q: %w", profile.FullName(), err)
+		}
+
+	}
+	if singleProfile {
+		ffcss.BaseIndentLevel++
+	}
+
+	// Ask to open extensions' pages
+	if len(manifest.Addons) > 0 {
+		if ffcss.ConfirmInstallAddons(manifest.Addons) {
+			for _, profile := range selectedProfiles {
+				ffcss.LogStep(0, "With profile "+filepath.Base(profile.Path))
+				for _, addonURL := range manifest.Addons {
+					profile.InstallAddon(operatingSystem, addonURL)
+				}
+			}
+		}
+	}
+
+	// Show message
+	err = manifest.ShowMessage()
+	if err != nil {
+		return fmt.Errorf("couldn't display the message: %w", err)
+	}
+	return nil
+}
+
+func runCommandGet(args flagsAndArgs) error {
+	themeName, _ := args.String("THEME_NAME")
+	// variant, _ := args.String("VARIANT")
+
+	err := ffcss.CreateDataDirectories()
+	if err != nil {
+		return err
+	}
+
+	ffcss.LogStep(0, "Resolving the theme's name")
+	uri, typ, err := ffcss.ResolveURL(themeName)
+	if err != nil {
+		return fmt.Errorf("while resolving name %s: %w", themeName, err)
+	}
+
+	ffcss.LogStep(0, "Downloading the theme")
+	manifest, err := ffcss.Download(uri, typ)
+	if err != nil {
+		return err
+	}
+
+	ffcss.LogStepC("✓", 0, "Downloaded [blue][bold]%s[reset] [dim](to %s)", manifest.Name(), manifest.DownloadedTo)
+	return nil
+}
+
+func runCommandReapply(args flagsAndArgs) error {
+	operatingSystem := ffcss.GOOStoOS(runtime.GOOS)
+	profilesDir, _ := args.String("--profiles-dir")
+	var profilesPaths []string
+	var err error
+	if profilesDir != "" {
+		profilesPaths, err = ffcss.ProfilePaths(operatingSystem, profilesDir)
+	} else {
+		profilesPaths, err = ffcss.ProfilePaths(operatingSystem)
+	}
+	if err != nil {
+		return fmt.Errorf("while getting profiles: %w", err)
+	}
+
+	currentThemesFileContents, err := os.ReadFile(ffcss.ConfigDir("currently.yaml"))
+	if err != nil {
+		return fmt.Errorf("while reading current themes list: %w", err)
+	}
+
+	currentThemes := make(map[string]string)
+	yaml.Unmarshal(currentThemesFileContents, &currentThemes)
+
+	for _, profilePath := range profilesPaths {
+		themeName, exists := currentThemes[filepath.Base(profilePath)]
+		if !exists {
+			ffcss.LogStep(0, "[yellow]Profile %s[reset][yellow] has no ffcss theme applied, skipping.", ffcss.NewFirefoxProfileFromPath(profilePath).Display())
+			continue
+		}
+		ffcss.LogStep(0, "Apply theme [blue][bold]%s[reset] to profile %s", themeName, ffcss.NewFirefoxProfileFromPath(profilePath).Display())
+
+		useArgs, _ := docopt.ParseArgs(usage, []string{"use", string(themeName), "--profiles", profilePath, "--skip-manifest-source"}, ffcss.VersionString)
+		ffcss.BaseIndentLevel++
+		err = runCommandUse(flagsAndArgs{useArgs})
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func runCommandReset(args flagsAndArgs) error {
+	profiles, err := ffcss.SelectProfiles(args.strings("--profiles"), args.string("--profiles-dir"), args.bool("--default-profile"), args.bool("--all-profiles"))
+	if err != nil {
+		return err
+	}
+	for _, profile := range profiles {
+		ffcss.LogStep(0, "With profile %s", profile.Display())
+		ffcss.LogStep(1, "Removing the current theme")
+		ffcss.LogStep(2, "Moving chrome/ to chrome.bak/")
+		err = profile.BackupChrome()
+		if err != nil {
+			return fmt.Errorf("couldn't back up the chrome folder: %w", err)
+		}
+		ffcss.LogStep(2, "Moving user.js to user.js.bak")
+		err = profile.BackupUserJS()
+		if err != nil {
+			return fmt.Errorf("couldn't back up user.js: %w", err)
+		}
+	}
+	return nil
+}
+
+func runCommandInit(args flagsAndArgs) error {
+	// TODO: set user{Chrome,Content,.js} by finding their path
+	// TODO: only set assets if chrome/ actually exists
+	workingDir, err := os.Getwd()
+	if err != nil {
+		return fmt.Errorf("could not get working directory: %w", err)
+	}
+
+	theme, err := ffcss.InitializeTheme(workingDir)
+	if err != nil {
+		return fmt.Errorf("while initializing theme: %w", err)
+	}
+
+	return theme.WriteManifest(workingDir)
+}
diff --git a/cmd/ffcss/main.go b/cmd/ffcss/main.go
new file mode 100644
index 0000000..ddaf081
--- /dev/null
+++ b/cmd/ffcss/main.go
@@ -0,0 +1,83 @@
+// Apply and configure FirefoxCSS themes
+package main
+
+import (
+	"fmt"
+	"io"
+	"os"
+	_ "embed"
+
+	"github.com/docopt/docopt-go"
+	"github.com/ewen-lbh/ffcss"
+)
+
+//go:embed USAGE
+var usage string
+
+var out io.Writer = os.Stdout
+
+func main() {
+	args, _ := docopt.ParseDoc(usage)
+
+	err := os.MkdirAll(ffcss.CacheDir(), 0700)
+	if err != nil {
+		panic(err)
+	}
+
+	err = os.MkdirAll(ffcss.ConfigDir(), 0700)
+	if err != nil {
+		panic(err)
+	}
+
+	if err := dispatchCommand(flagsAndArgs{args}); err != nil {
+		fmt.Fprintln(out)
+		ffcss.LogError("Woops! An error occurred:")
+		fmt.Fprintln(out)
+		ffcss.DisplayErrorMessage(err)
+	}
+}
+
+func dispatchCommand(args flagsAndArgs) error {
+	ffcss.LogDebug("dispatching %#v", args)
+	if val, _ := args.Bool("configure"); val {
+		return fmt.Errorf("not implemented")
+	}
+	if val, _ := args.Bool("use"); val {
+		err := runCommandUse(args)
+		return err
+	}
+	if val, _ := args.Bool("get"); val {
+		err := runCommandGet(args)
+		return err
+	}
+	if val, _ := args.Bool("reapply"); val {
+		err := runCommandReapply(args)
+		return err
+	}
+	if val, _ := args.Bool("init"); val {
+		err := runCommandInit(args)
+		return err
+	}
+	if val, _ := args.Bool("cache"); val {
+		if val, _ := args.Bool("clear"); val {
+			return ffcss.ClearWholeCache()
+		}
+	}
+	if val, _ := args.Bool("reset"); val {
+		return runCommandReset(args)
+	}
+	if val, _ := args.Bool("version"); val {
+		component, _ := args.String("COMPONENT")
+		switch component {
+		case "major":
+			fmt.Fprintln(out, ffcss.VersionMajor)
+		case "minor":
+			fmt.Fprintln(out, ffcss.VersionMinor)
+		case "patch":
+			fmt.Fprintln(out, ffcss.VersionPatch)
+		default:
+			fmt.Fprintln(out, ffcss.VersionString)
+		}
+	}
+	return nil
+}
diff --git a/cmd/ffcss/utils.go b/cmd/ffcss/utils.go
new file mode 100644
index 0000000..f90a79a
--- /dev/null
+++ b/cmd/ffcss/utils.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+	"strings"
+
+	"github.com/docopt/docopt-go"
+	"github.com/ewen-lbh/ffcss"
+)
+
+type flagsAndArgs struct {
+	docopt.Opts
+}
+
+func (o flagsAndArgs) string(name string) string {
+	val, err := o.String(name)
+	if err != nil {
+		ffcss.LogDebug("while getting value of %s: %s: using %#v", name, err.Error(), val)
+	}
+	return val
+}
+
+func (o flagsAndArgs) bool(name string) bool {
+	val, err := o.Bool(name)
+	if err != nil {
+		ffcss.LogDebug("while getting value of %s: %s: using %#v", name, err.Error(), val)
+	}
+	return val
+}
+
+func (o flagsAndArgs) strings(name string) []string {
+	val, err := o.String(name)
+	if err != nil {
+		ffcss.LogDebug("while getting value of %s: %s: using an empty arary", name, err.Error())
+		return []string{}
+	}
+	return strings.Split(val, ",")
+}
diff --git a/currenttheme.go b/currenttheme.go
new file mode 100644
index 0000000..62fc6a9
--- /dev/null
+++ b/currenttheme.go
@@ -0,0 +1,46 @@
+package ffcss
+
+import (
+	"fmt"
+	"os"
+
+	"gopkg.in/yaml.v2"
+)
+
+// CurrentThemeByProfile returns a map mapping a profile path to its current theme's name.
+func CurrentThemeByProfile() (map[string]string, error) {
+	currentThemesRaw, err := os.ReadFile(ConfigDir("currently.yaml"))
+	if os.IsNotExist(err) {
+		err = os.WriteFile(ConfigDir("currently.yaml"), []byte(""), 0777)
+		if err != nil {
+			return nil, fmt.Errorf("while creating current themes list file: %w", err)
+		}
+	} else if err != nil {
+		return nil, fmt.Errorf("while reading current themes list: %w", err)
+	}
+
+	currentThemes := make(map[string]string)
+	yaml.Unmarshal(currentThemesRaw, &currentThemes)
+	return currentThemes, nil
+}
+
+// RegisterCurrentTheme writes the currently.yaml file in ffcss' configuration to update
+// what ffcss considers to be the current theme for that profile.
+func (ffp FirefoxProfile) RegisterCurrentTheme(themeName string) error {
+	currentThemes, err := CurrentThemeByProfile()
+	if err != nil {
+		return err
+	}
+	currentThemes[ffp.FullName()] = themeName
+	currentThemesNewContents, err := yaml.Marshal(currentThemes)
+	if err != nil {
+		return fmt.Errorf("while marshaling into YAML: %w", err)
+	}
+
+	err = os.WriteFile(ConfigDir("currently.yaml"), currentThemesNewContents, 0777)
+	if err != nil {
+		return fmt.Errorf("while writing new contents: %w", err)
+	}
+
+	return nil
+}
diff --git a/currenttheme_test.go b/currenttheme_test.go
new file mode 100644
index 0000000..8231de5
--- /dev/null
+++ b/currenttheme_test.go
@@ -0,0 +1,17 @@
+package ffcss
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCurrentThemeByProfile(t *testing.T) {
+	actual, err := CurrentThemeByProfile()
+
+	assert.NoError(t, err)
+	assert.Equal(t, map[string]string{
+		"thingie": "hmmmm",
+		"stuff":   "yesees",
+	}, actual)
+}
diff --git a/datadirs.go b/datadirs.go
new file mode 100644
index 0000000..88ada62
--- /dev/null
+++ b/datadirs.go
@@ -0,0 +1,16 @@
+package ffcss
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+)
+
+// CreateDataDirectories makes the required directories for ffcss to function properly.
+func CreateDataDirectories() error {
+	err := os.MkdirAll(filepath.Join(getConfigDir(), "themes"), 0777)
+	if err != nil {
+		return fmt.Errorf("couldn't create data directories: %w", err)
+	}
+	return nil
+}
diff --git a/datadirs_test.go b/datadirs_test.go
new file mode 100644
index 0000000..6e8b43f
--- /dev/null
+++ b/datadirs_test.go
@@ -0,0 +1,20 @@
+package ffcss
+
+import (
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCreateDataDirectories(t *testing.T) {
+	err := CreateDataDirectories()
+	assert.NoError(t, err)
+	actual, err := os.ReadDir(getConfigDir())
+	assert.NoError(t, err)
+	dirnames := make([]string, 0, len(actual))
+	for _, n := range actual {
+		dirnames = append(dirnames, n.Name())
+	}
+	assert.Contains(t, dirnames, "themes")
+}
diff --git a/download.go b/download.go
new file mode 100644
index 0000000..fa2a045
--- /dev/null
+++ b/download.go
@@ -0,0 +1,308 @@
+package ffcss
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+
+	"github.com/evilsocket/islazy/zip"
+)
+
+// RootVariantName is the name of the default variant, when none were applied.
+// This is used when computing the location of the cached theme: a variant-less theme is downloaded to <theme name>/_.
+const RootVariantName = "_"
+
+// TempDownloadsDirName is the name of the directory used to download themes when the theme's name is not known.
+// See the source code of Download for more insights.
+// A theme cannot have that name.
+const TempDownloadsDirName = ".download"
+
+// ResolveURL resolves the THEME_NAME given to ffcss use to either:
+// - a URL to download
+// - a git repo URL to clone
+func ResolveURL(themeName string) (URL string, typ string, err error) {
+	protocolLessURL := regexp.MustCompile(`^[\w_-]+\.[\w_-]+/.*$`)
+	userSlashRepo := regexp.MustCompile(`^[\w_-]+/[\w_-]+$`)
+	var completeURL string
+
+	// Try OWNER/REPO
+	if userSlashRepo.MatchString(themeName) {
+		completeURL = "https://github.com/" + themeName
+		if !isURLClonable(completeURL) {
+			return "", "", fmt.Errorf("%s is not clonable. Make sure it exists", completeURL)
+		}
+		// Try DOMAIN.TLD/PATH
+	} else if protocolLessURL.MatchString(themeName) {
+		completeURL = "https://" + themeName
+		if !isValidURL(completeURL) {
+			return "", "", fmt.Errorf("%q is not a valid URL", completeURL)
+		}
+		// Try URL
+	} else if isValidURL(themeName) {
+		completeURL = themeName
+	} else {
+		return themeName, "bare", nil
+	}
+
+	if isURLClonable(completeURL) {
+		return completeURL, "git", nil
+	}
+	return completeURL, "website", nil
+}
+
+// Download downloads the theme at URL.
+// If typ is website, then it downloads the zip file and extracts it.
+// If typ is git, then it clones the repository
+// If typ is bare, then it tries to find the URL in ~/.config/ffcss/themes/{{URL}}.yaml
+// In all cases, the theme is downloaded to ~/.cache/ffcss/{{themeName}}.
+// If themeName is not provided, the theme will first be downloaded to a temporary location to get the name from the manifest.
+func Download(URL string, typ string, themeManifest ...Theme) (manifest Theme, err error) {
+	LogDebug("typ is %s", typ)
+	if len(themeManifest) >= 1 {
+		manifest = themeManifest[0]
+		LogDebug("manifest is provided")
+		// Don't re-download if it already exists
+		LogDebug("checking if theme is in cache @ %s", manifest.DownloadedTo)
+		stat, err := os.Stat(manifest.DownloadedTo)
+		if err == nil && stat.IsDir() {
+			LogDebug("skipped downloading of %s [%s#%s]", URL, manifest.Name(), manifest.currentVariantName)
+			return manifest, nil
+		}
+	}
+	err = os.MkdirAll(CacheDir(TempDownloadsDirName), 0777)
+	if err != nil {
+		return manifest, fmt.Errorf("couldn't create %s: %w", CacheDir(TempDownloadsDirName), err)
+	}
+	tempDir, err := os.MkdirTemp(CacheDir(TempDownloadsDirName), "*")
+	if err != nil {
+		return manifest, fmt.Errorf("couldn't create a temporary directory at %s: %w", CacheDir(TempDownloadsDirName), err)
+	}
+	switch typ {
+	case "website":
+		manifest, err = DownloadFromZip(URL, tempDir, CacheDir(), themeManifest...)
+		if err != nil {
+			return manifest, fmt.Errorf("couldn't use the zip file at %s: %w", URL, err)
+		}
+	case "git":
+		manifest, err = DownloadRepository(URL, tempDir, CacheDir(), themeManifest...)
+		if err != nil {
+			return manifest, fmt.Errorf("couldn't use the repository %s: %w", URL, err)
+		}
+	case "bare":
+		themes, err := LoadCatalog(ConfigDir("themes"))
+		if err != nil {
+			return manifest, fmt.Errorf("while loading catalog of themes: %w", err)
+		}
+		theme, err := themes.Lookup(URL)
+		if err != nil {
+			return theme, err
+		}
+
+		manifest, err = Download(theme.DownloadAt, "git", theme)
+		if err != nil {
+			return manifest, fmt.Errorf("from catalog: %w", err)
+		}
+	default:
+		panic("unexpected URL type")
+	}
+	manifest.DownloadedTo = CacheDir(manifest.Name(), manifest.currentVariantName)
+	return
+}
+
+// DownloadRepository downloads the repository at URL to {{cloneTo}}/{{ffcss.yaml:name}}/{{current variant's name}}
+// It first clones the repo to tempCloneTo, then loads the manifest to determine where to move it.
+// the manifest can be provided in case the repository does not contain it.
+func DownloadRepository(URL string, tempCloneTo string, cloneTo string, themeManifest ...Theme) (manifest Theme, err error) {
+	hasManifest := len(themeManifest) >= 1
+	if hasManifest {
+		manifest = themeManifest[0]
+	}
+
+	err = os.MkdirAll(cloneTo, 0777)
+	if err != nil {
+		return manifest, fmt.Errorf("could not create directory to download to: %w", err)
+	}
+
+	err = os.MkdirAll(tempCloneTo, 0777)
+	if err != nil {
+		return manifest, fmt.Errorf("could not create directory to clone to: %w", err)
+	}
+
+	cloneArgs := []string{"clone", URL, tempCloneTo}
+	if hasManifest && manifest.Branch != "" {
+		cloneArgs = append(cloneArgs, "--branch", manifest.Branch)
+	}
+	LogDebug("Cloning repo...")
+	process := exec.Command("git", cloneArgs...)
+	//TODO print this in verbose mode: fmt.Fprintf(out, "DEBUG $ %s\n", process.String())
+	output, err := process.CombinedOutput()
+	if err != nil {
+		return manifest, fmt.Errorf("%w: %s", err, output)
+	}
+
+	if !hasManifest {
+		manifest, err = LoadManifest(filepath.Join(tempCloneTo, "ffcss.yaml"))
+		if _, err := os.Stat(filepath.Join(tempCloneTo, "ffcss.yaml")); os.IsNotExist(err) {
+			return manifest, fmt.Errorf("no manifest found: %w", err)
+		}
+		if err != nil {
+			return manifest, fmt.Errorf("could not load manifest: %w", err)
+		}
+		if manifest.Branch != "" {
+			LogDebug("switching to branch %q", manifest.Branch)
+			err = switchGitBranch(manifest.Branch, tempCloneTo)
+			if err != nil {
+				return manifest, fmt.Errorf("while switching to branch %q: %w", manifest.Branch, err)
+			}
+		}
+	}
+
+	if manifest.Commit != "" {
+		LogDebug("switching to commit %q", manifest.Commit)
+		err = switchGitCommit(manifest.Commit, tempCloneTo)
+		if err != nil {
+			return manifest, fmt.Errorf("while checking out commit %q: %w", manifest.Commit, err)
+		}
+	}
+	if manifest.Tag != "" {
+		LogDebug("switching to tag %q", manifest.Tag)
+		err = switchGitTag(manifest.Tag, tempCloneTo)
+		if err != nil {
+			return manifest, fmt.Errorf("while checking out tag %q: %w", manifest.Tag, err)
+		}
+	}
+
+	if manifest.Name() == "" {
+		return manifest, errors.New("manifest has no name")
+	}
+
+	err = os.MkdirAll(filepath.Dir(manifest.DownloadedTo), 0700)
+	if err != nil {
+		return manifest, fmt.Errorf("while creating final cache location: %w", err)
+	}
+
+	err = os.Rename(tempCloneTo, manifest.DownloadedTo)
+	if err != nil {
+		return manifest, fmt.Errorf("while moving from temporary downloads %q to final cache location %q: %w", tempCloneTo, manifest.DownloadedTo, err)
+	}
+
+	return
+}
+
+// DownloadFromZip downloads a ffcss manifest files along with its resources from the given URL.
+// The URL must point to a zip file that contains a ffcss.yaml in its root.
+// The zip file will be downloaded and extracted to {{tempDownloadTo}}, then, after loading the manifest,
+// the folder will then be moved to {{finalDownloadTo}}/{{ffcss.yaml:name}}.
+// the manifest can be provided in case the zip does not contain it.
+func DownloadFromZip(URL string, tempDownloadTo string, finalDownloadTo string, themeManifest ...Theme) (manifest Theme, err error) {
+	tempDownloadTo = tempDownloadTo + "/theme.zip"
+	hasManifest := len(themeManifest) >= 1
+	if hasManifest {
+		manifest = themeManifest[0]
+	}
+
+	// Check if file exists, has the right Content-Type, etc.
+	head, err := http.Head(URL)
+	if err != nil {
+		return manifest, fmt.Errorf("couldn't check remote file: %w", err)
+	}
+	if head.StatusCode >= 400 {
+		return manifest, fmt.Errorf("couldn't check remote file: server returned %s", head.Status)
+	}
+	if head.Header.Get("Content-Type") != "application/zip" {
+		return manifest, fmt.Errorf("expected a zip file (application/zip), got a %s", head.Header.Get("Content-Type"))
+	}
+
+	// Download it
+	process := exec.Command("wget", URL, "-O", tempDownloadTo)
+	LogDebug("Running %s", process.String())
+	output, err := process.CombinedOutput()
+	if err != nil {
+		return manifest, fmt.Errorf("couldn't download zip file: %w: %s", err, output)
+	}
+
+	// Unzip it, check contents
+	LogDebug("Unzipping %s to %s", tempDownloadTo, filepath.Dir(tempDownloadTo))
+	unzipped, err := zip.Unzip(tempDownloadTo, filepath.Dir(tempDownloadTo))
+	if err != nil {
+		return manifest, fmt.Errorf("while unzipping %s: %w", tempDownloadTo, err)
+	}
+
+	if !hasManifest {
+		for _, file := range unzipped {
+			if filepath.Base(file) == "ffcss.yaml" {
+				hasManifest = true
+				manifest, err = LoadManifest(file)
+				if err != nil {
+					return manifest, fmt.Errorf("couldn't load the manifest file: %w", err)
+				}
+				break
+			}
+		}
+		if !hasManifest {
+			os.RemoveAll(filepath.Dir(tempDownloadTo))
+			return manifest, errors.New("downloaded zip file has no manifest file (ffcss.yaml)")
+		}
+	}
+	if manifest.Name() == "" {
+		return manifest, errors.New("manifest has no name")
+	}
+	err = os.Rename(filepath.Dir(tempDownloadTo), filepath.Join(finalDownloadTo, manifest.Name()))
+	if err != nil {
+		return manifest, fmt.Errorf("could not move %s to %s: %w", filepath.Dir(tempDownloadTo), filepath.Join(finalDownloadTo, manifest.Name()), err)
+	}
+	return
+}
+
+// isURLClonable determines if the given URL points to a git repository
+func isURLClonable(URL string) bool {
+	output, err := exec.Command("git", "ls-remote", URL).CombinedOutput()
+	if err == nil {
+		return true
+	}
+	switch err.(type) {
+	case *exec.ExitError:
+		if err.(*exec.ExitError).ExitCode() == 128 {
+			return false
+		}
+	}
+	LogWarning("could not determine clonability of %s: while running git-ls-remote: %w: %s\n", URL, err, output)
+	return false
+}
+
+// ReDownloadIfNeeded re-downloads the repo, actionNeeded.reDownload
+// is true
+func (t Theme) ReDownloadIfNeeded(actionsNeeded struct {
+	switchBranch bool
+	reDownload   bool
+}) error {
+	// FIXME for now switching branches just re-downloads the entire thing to a new dir with the new branch
+	// ideal thing would be to copy from the root variant to the new variant, cd into it then `git switch` there.
+	if actionsNeeded.reDownload || actionsNeeded.switchBranch {
+		LogStep(0, "Downloading the variant")
+		LogDebug("re-downloading: new repo is %s", t.DownloadAt)
+		uri, typ, err := ResolveURL(t.DownloadAt)
+		if err != nil {
+			return fmt.Errorf("while resolving URL %s: %w", t.DownloadAt, err)
+		}
+
+		_, err = Download(uri, typ, t)
+		if err != nil {
+			return fmt.Errorf("couldn't download the variant at %s: %w", uri, err)
+		}
+	}
+	return nil
+}
+
+// WarnIfIncompatibleWithOS shows a warning to the user if operatingSystem is marked as incompatible by the theme ("os" entry in the manifest).
+func (t Theme) WarnIfIncompatibleWithOS(operatingSystem string) {
+	for k, v := range t.OSNames {
+		if k == operatingSystem && v == "" {
+			LogWarning("This theme is marked as incompatible with %s. Things might not work.", operatingSystem)
+		}
+	}
+}
diff --git a/download_test.go b/download_test.go
new file mode 100644
index 0000000..f2517ca
--- /dev/null
+++ b/download_test.go
@@ -0,0 +1,71 @@
+package ffcss
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestResolveURL(t *testing.T) {
+	name, typ, err := ResolveURL("ewen-lbh/ffcss")
+	assert.NoError(t, err)
+	assert.Equal(t, []string{"https://github.com/ewen-lbh/ffcss", "git"}, []string{name, typ})
+
+	name, typ, err = ResolveURL("bitbucket.io/guaca/mole")
+	assert.NoError(t, err)
+	assert.Equal(t, []string{"https://bitbucket.io/guaca/mole", "website"}, []string{name, typ})
+
+	name, typ, err = ResolveURL("http://localhost:8080/")
+	assert.NoError(t, err)
+	assert.Equal(t, []string{"http://localhost:8080/", "website"}, []string{name, typ})
+
+	name, typ, err = ResolveURL("materialfox")
+	assert.NoError(t, err)
+	assert.Equal(t, []string{"materialfox", "bare"}, []string{name, typ})
+
+	name, typ, err = ResolveURL("unknownone")
+	assert.NoError(t, err)
+	assert.Equal(t, []string{"unknownone", "bare"}, []string{name, typ})
+
+	name, typ, err = ResolveURL("unvalid~github~username/somerepo")
+	assert.NoError(t, err)
+	assert.Equal(t, []string{"unvalid~github~username/somerepo", "bare"}, []string{name, typ})
+}
+
+func TestDownload(t *testing.T) {
+	_, err := Download("testdata/nomanifest", "git")
+	assert.Contains(t, err.Error(), "no manifest found")
+
+	_, err = Download("http://localhost:8080/notfound", "website")
+	assert.Contains(t, err.Error(), "server returned 404 File not found")
+
+	_, err = Download("http://localhost:8080/htmlfile.html", "website")
+	assert.Contains(t, err.Error(), "expected a zip file (application/zip), got")
+
+	_, err = Download("materialfox", "bare")
+	assert.NoError(t, err)
+
+	_, err = Download("unknownone", "bare")
+	assert.Contains(t, err.Error(), `theme "unknownone" not found`)
+}
+
+func TestDownloadFromZip(t *testing.T) {
+	i := 0
+	dl := func(s string) error {
+		os.MkdirAll(filepath.Join(cwd(), fmt.Sprintf("testarea/zip-dropoff/%d", i)), 0777)
+		_, err := DownloadFromZip(s, filepath.Join(cwd(), fmt.Sprintf("testarea/zip-dropoff/%d", i)), filepath.Join(cwd(), "testarea/cache"))
+		i++
+		return err
+	}
+
+	assert.Contains(t, dl("http://localhost:8080/notfound").Error(), "couldn't check remote file: server returned 404 File not found")
+	assert.Contains(t, dl("http://localhost:8080/htmlfile.html").Error(), "expected a zip file (application/zip), got a text/html")
+	// TODO: check for absence of unzipped folder
+	assert.Contains(t, dl("http://localhost:8080/../themeWithNoManifest.zip").Error(), "downloaded zip file has no manifest file (ffcss.yaml)")
+	// TODO: check for presence of unzipped folder
+	assert.Nil(t, dl("http://localhost:8080/../materialfox.zip"))
+	// TODO: check for absence of zip file, in both cases
+}
diff --git a/ffcss.go b/ffcss.go
new file mode 100644
index 0000000..fb4a06f
--- /dev/null
+++ b/ffcss.go
@@ -0,0 +1,30 @@
+package ffcss
+
+import (
+	"fmt"
+	"io"
+	"os"
+)
+
+const (
+	// VersionMajor is the major semver component of the current version, X._._.
+	// It is incremented on breaking changes. Incrementing sets others to zero.
+	VersionMajor = 0
+	// VersionMinor is the semver minor component of the current version, _.Y._.
+	// It is incremented on feature changes. Incrementing sets VersionPatch to zero.
+	VersionMinor = 2
+	// VersionPatch is the semver patch component of the current version, _._.Z.
+	// It is incremented on bug fixes
+	VersionPatch = 0
+)
+
+var (
+	// VersionString is the full version string, constructed from the component constants Version{Major,Minor,Patch}.
+	VersionString = fmt.Sprintf("%d.%d.%d", VersionMajor, VersionMinor, VersionPatch)
+	// out is an IO writer used to capture stdout instead of printing in unit tests
+	out io.Writer = os.Stdout
+	// BaseIndentLevel is the base level of indentation for LogStep and others.
+	// Can be incremented when a whole section of code is run in the context of another
+	// (For an example usage, see reapply in the cmd/ffcss package, that effectively runs the use command on each profile).
+	BaseIndentLevel uint
+)
diff --git a/ffcss_test.go b/ffcss_test.go
new file mode 100644
index 0000000..611b927
--- /dev/null
+++ b/ffcss_test.go
@@ -0,0 +1,30 @@
+package ffcss
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"os/user"
+	"path/filepath"
+)
+
+var currentUser user.User
+var realHomedir string
+var mockedHomedir string
+var mockedProfile FirefoxProfile
+var mockedStdout bytes.Buffer
+var testarea = filepath.Join(cwd(), "testarea")
+
+func init() {
+	usr, _ := user.Current()
+	currentUser = *usr
+	realHomedir, _ = os.UserHomeDir()
+	mockedHomedir = os.Getenv("HOME")
+	mockedProfile = NewFirefoxProfileFromPath(filepath.Join(mockedHomedir, ".mozilla", "firefox", "667ekipp.default-release"))
+	out = &mockedStdout
+}
+
+// withUser Replaces %s with currentUser.Username in s
+func withuser(s string) string {
+	return fmt.Sprintf(s, currentUser.Username)
+}
diff --git a/firefoxversion.go b/firefoxversion.go
new file mode 100644
index 0000000..38f020b
--- /dev/null
+++ b/firefoxversion.go
@@ -0,0 +1,183 @@
+package ffcss
+
+import (
+	"fmt"
+	"math"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+)
+
+// FirefoxVersion represents a firefox version of the form "major.minor".
+type FirefoxVersion struct {
+	Major int
+	Minor int // -1 means "unspecified". Can be obtained by using "x" for the minor part. Useful for constraints.
+}
+
+// FirefoxVersionConstraint represents a constraint to test on a firefox version.
+type FirefoxVersionConstraint struct {
+	Min FirefoxVersion
+	Max FirefoxVersion
+	// To be included in a Sentence like "this theme ensures compatibility with firefox <Sentence>"
+	Sentence string
+}
+
+// NewFirefoxVersionConstraint creates a firefox version constraint from its string representation.
+//
+// Leaving out the minor version (e.g. 90 instead of 90.something) implies a trailing ".x", which means "any minor version".
+//
+// The following formats are supported, where X, Z are integers and Y, W are integers or the character 'x'
+//
+//    Format     Meaning                                    Interval
+//    X.Y+       X.Y or higher                              [X.Y,  +∞]   (where +∞ = math.MaxInt32)
+//    up to X.Y  X.Y but not higher                         [0.0, X.Y]
+//    X.Y-Z.W    between X.Y and Z.W (including both ends)  [X.Y, Z.W]
+//    X.Y        exactly X.Y                                [X.Y, X.Y]
+func NewFirefoxVersionConstraint(constraint string) (FirefoxVersionConstraint, error) {
+	min := FirefoxVersion{0, 0}
+	max := FirefoxVersion{math.MaxInt32, math.MaxInt32}
+	var sentence string
+	var err error
+	if strings.HasSuffix(constraint, "+") {
+		LogDebug("constraint type is minimum")
+		min, err = NewFirefoxVersion(strings.TrimSuffix(constraint, "+"))
+		if err != nil {
+			return FirefoxVersionConstraint{}, fmt.Errorf("while parsing minimum constraint %q: %w", constraint, err)
+		}
+		sentence = fmt.Sprintf("version %s or higher", min)
+	} else if strings.Count(constraint, "-") == 1 {
+		LogDebug("constraint type is range")
+		minmaxStrings := strings.SplitN(constraint, "-", 2)
+		min, err = NewFirefoxVersion(minmaxStrings[0])
+		if err != nil {
+			return FirefoxVersionConstraint{}, fmt.Errorf("while parsing lower bound of range constraint %q: %w", constraint, err)
+		}
+		max, err = NewFirefoxVersion(minmaxStrings[1])
+		if err != nil {
+			return FirefoxVersionConstraint{}, fmt.Errorf("while parsing upper bound of range constraint %q: %w", constraint, err)
+		}
+		if min.GreaterOrEqual(max) && !min.Equal(max) {
+			return FirefoxVersionConstraint{}, fmt.Errorf("lower bound (%s) is higher than upper bound (%s)", min, max)
+		}
+		sentence = min.String() + "–" + max.String()
+	} else if strings.HasPrefix(constraint, "up to ") {
+		LogDebug("constraint type is upto")
+		max, err = NewFirefoxVersion(strings.TrimPrefix(constraint, "up to "))
+		if err != nil {
+			return FirefoxVersionConstraint{}, fmt.Errorf("while parsing maximum constraint %q: %w", constraint, err)
+		}
+		sentence = fmt.Sprintf("version %s or lower", max)
+	} else {
+		LogDebug("constraint type is exact match")
+		exact, err := NewFirefoxVersion(constraint)
+		if err != nil {
+			return FirefoxVersionConstraint{}, fmt.Errorf("while parsing exact match constraint %q: %w", constraint, err)
+		}
+		min = exact
+		max = exact
+		sentence = fmt.Sprintf("%s only", exact)
+	}
+	return FirefoxVersionConstraint{min, max, sentence}, nil
+}
+
+// GreaterOrEqual checks if the version is greater or equal to other.
+// If one of the two (or both) has the minor part unspecified (".x", stored as -1),
+// it only compares major parts. Otherwise, it uses a standard lexical sort.
+func (ffv FirefoxVersion) GreaterOrEqual(other FirefoxVersion) bool {
+	if other.Minor == -1 || ffv.Minor == -1 {
+		return ffv.Major >= other.Major
+	}
+	return ffv.Major > other.Major || (ffv.Major == other.Major && ffv.Minor >= other.Minor)
+}
+
+// LessOrEqual checks if the version is less than or equal to other.
+// If one of the two (or both) has the minor part unspecified (".x", stored as -1),
+// it only compares major parts. Otherwise, it uses a standard lexical sort.
+func (ffv FirefoxVersion) LessOrEqual(other FirefoxVersion) bool {
+	if other.Minor == -1 || ffv.Minor == -1 {
+		return ffv.Major <= other.Major
+	}
+	LogDebug("check that %d <= %d or %d = %d and %d <= %d", ffv.Major, other.Major, ffv.Major, other.Major, ffv.Minor, other.Minor)
+	return ffv.Major < other.Major || (ffv.Major == other.Major && ffv.Minor <= other.Minor)
+}
+
+// Equal checks if the two versions are equal.
+func (ffv FirefoxVersion) Equal(other FirefoxVersion) bool {
+	return ffv.Major == other.Major && ffv.Minor == other.Minor
+}
+
+// String returns a string representation of the version
+// If the minor part is -1, it is rendered as a 'x' character.
+func (ffv FirefoxVersion) String() string {
+	if ffv.Minor == -1 {
+		return fmt.Sprintf("%d.x", ffv.Major)
+	}
+	return fmt.Sprintf("%d.%d", ffv.Major, ffv.Minor)
+}
+
+// FulfilledBy checks if version ∈ [constraint.min, constraint.max]
+func (constraint FirefoxVersionConstraint) FulfilledBy(version FirefoxVersion) bool {
+	LogDebug("checking if %s ∈ [%s, %s]", version, constraint.Min, constraint.Max)
+	return version.GreaterOrEqual(constraint.Min) && version.LessOrEqual(constraint.Max)
+}
+
+// NewFirefoxVersion turns a version string (90 or 90.0 for example) into a FirefoxVersion.
+// defaultMinor is used when parsing a dot-less version string. It defaults to "x" (meaning unspecified).
+func NewFirefoxVersion(stringRepr string, defaultMinor ...string) (FirefoxVersion, error) {
+	fragments := strings.Split(stringRepr, ".")
+	if len(defaultMinor) == 0 {
+		defaultMinor = []string{"x"}
+	}
+	if len(fragments) == 1 {
+		fragments = append(fragments, defaultMinor...)
+	}
+	LogDebug("parsing version %s: fragments is %#v", stringRepr, fragments)
+	major, err := strconv.ParseInt(fragments[0], 10, 64)
+	if major < 0 {
+		return FirefoxVersion{}, fmt.Errorf("version number cannot be negative")
+	}
+	if err != nil {
+		return FirefoxVersion{}, fmt.Errorf("while converting major segment: %w", err)
+	}
+	var minor int64
+	if fragments[1] == "x" {
+		minor = -1
+	} else {
+		var err error
+		minor, err = strconv.ParseInt(fragments[1], 10, 64)
+		if err != nil {
+			return FirefoxVersion{}, fmt.Errorf("while converting minor segment: %w", err)
+		}
+		if minor < 0 {
+			return FirefoxVersion{}, fmt.Errorf("version number cannot be negative")
+		}
+	}
+	LogDebug("parsed as major=%d minor=%d", major, minor)
+	return FirefoxVersion{
+		Major: int(major),
+		Minor: int(minor),
+	}, nil
+}
+
+// FirefoxVersion returns the firefox version of the profile by reading the value of
+// the browser.startup.homepage_override.mstone configuration entry in the profile's prefs.js.
+// This method does not work for profiles that have never been opened by the user.
+func (profile FirefoxProfile) FirefoxVersion() (FirefoxVersion, error) {
+	prefs, err := os.ReadFile(filepath.Join(profile.Path, "prefs.js"))
+	if err != nil {
+		return FirefoxVersion{}, fmt.Errorf("while reading file: %w", err)
+	}
+
+	versionString, err := ValueOfUserPrefCall(prefs, "browser.startup.homepage_override.mstone")
+	if err != nil {
+		return FirefoxVersion{}, fmt.Errorf("while getting value in prefs.js: %w", err)
+	}
+
+	version, err := NewFirefoxVersion(versionString)
+	if err != nil {
+		return FirefoxVersion{}, fmt.Errorf("while parsing version string %q: %w", versionString, err)
+	}
+
+	return version, nil
+}
diff --git a/firefoxversion_test.go b/firefoxversion_test.go
new file mode 100644
index 0000000..dd13f7e
--- /dev/null
+++ b/firefoxversion_test.go
@@ -0,0 +1,122 @@
+package ffcss
+
+import (
+	"fmt"
+	"math"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestFirefoxVersionOfProfile(t *testing.T) {
+	version, err := NewFirefoxProfileFromPath(filepath.Join(mockedHomedir, ".mozilla", "firefox", "667ekipp.default-release")).FirefoxVersion()
+	assert.NoError(t, err)
+	assert.Equal(t, FirefoxVersion{90, 0}, version)
+}
+
+func TestFirefoxVersionConstraint(t *testing.T) {
+	parsingFailsWith := func(constraint string, errorPart string) {
+		_, err := NewFirefoxVersionConstraint(constraint)
+		assert.Contains(t, err.Error(), errorPart)
+	}
+	fulfillementIs := func(fulfilled bool, version FirefoxVersion, constraint string) {
+		parsedConstraint, err := NewFirefoxVersionConstraint(constraint)
+		assert.NoError(t, err)
+		assert.Equal(t, fulfilled, parsedConstraint.FulfilledBy(version), fmt.Sprintf("testing if %s statisfies %v", version.String(), parsedConstraint))
+
+	}
+
+	fulfillementIs(true, FirefoxVersion{90, 0}, "90+")
+	fulfillementIs(true, FirefoxVersion{90, 0}, "88-90")
+	fulfillementIs(true, FirefoxVersion{90, 0}, "90")
+	fulfillementIs(true, FirefoxVersion{90, 0}, "up to 90")
+	fulfillementIs(false, FirefoxVersion{90, 0}, "88-89")
+	fulfillementIs(true, FirefoxVersion{90, 0}, "70+")
+	fulfillementIs(false, FirefoxVersion{90, 0}, "100")
+	fulfillementIs(true, FirefoxVersion{90, 1}, "up to 90")
+	fulfillementIs(false, FirefoxVersion{90, 1}, "up to 90.0")
+
+	parsingFailsWith("10o", "while parsing exact match constraint")
+	parsingFailsWith("-10", "while parsing lower bound of range constraint")
+	parsingFailsWith("88-", "while parsing upper bound of range constraint")
+	parsingFailsWith("up to me", "while parsing maximum constraint")
+}
+
+func TestNewFirefoxVersionConstraint(t *testing.T) {
+	type v = FirefoxVersion
+	cases := []struct {
+		in       string
+		min, max FirefoxVersion
+	}{
+		{"90+", v{90, -1}, v{math.MaxInt32, math.MaxInt32}},
+		{"up to 88", v{0, 0}, v{88, -1}},
+		{"88-90", v{88, -1}, v{90, -1}},
+		{"up to 76.43", v{0, 0}, v{76, 43}},
+		{"45", v{45, -1}, v{45, -1}},
+	}
+
+	for _, caze := range cases {
+		actual, err := NewFirefoxVersionConstraint(caze.in)
+		assert.NoError(t, err)
+		assert.Equal(t, caze.min, actual.Min)
+		assert.Equal(t, caze.max, actual.Max)
+	}
+
+	errorCases := []struct{ in, inErr string }{
+		{"gouirhigerjig", "while converting major segment"},
+		{"98-30", "lower bound (98.x) is higher than upper bound (30.x)"},
+		{"10o", "while parsing exact match constraint"},
+		{"-10", "while parsing lower bound of range constraint"},
+		{"88-", "while parsing upper bound of range constraint"},
+		{"up to me", "while parsing maximum constraint"},
+	}
+
+	for _, caze := range errorCases {
+		_, err := NewFirefoxVersionConstraint(caze.in)
+		assert.Error(t, err)
+		if err != nil {
+			assert.Contains(t, err.Error(), caze.inErr)
+		}
+	}
+}
+
+func TestNewFirefoxVersion(t *testing.T) {
+	type v = FirefoxVersion
+	cases := []struct {
+		in, defaultMinor string
+		out              FirefoxVersion
+	}{
+		{"1.12", "", v{1, 12}},
+		{"465", "5", v{465, 5}},
+		{"465.7", "5", v{465, 7}},
+		{"4", "", v{4, -1}},
+	}
+
+	for _, caze := range cases {
+		var actual FirefoxVersion
+		var err error
+		if caze.defaultMinor == "" {
+			actual, err = NewFirefoxVersion(caze.in)
+		} else {
+			actual, err = NewFirefoxVersion(caze.in, caze.defaultMinor)
+		}
+		assert.NoError(t, err)
+		assert.Equal(t, caze.out, actual)
+	}
+
+	errorCases := []struct{ in, inErr string }{
+		{"gouirhigerjig", "while converting major segment"},
+		{"98-30", "while converting major segment"},
+		{"10o", ""},
+		{"-10", "cannot be negative"},
+	}
+
+	for _, caze := range errorCases {
+		_, err := NewFirefoxVersion(caze.in)
+		assert.Error(t, err)
+		if err != nil {
+			assert.Contains(t, err.Error(), caze.inErr)
+		}
+	}
+}
diff --git a/git.go b/git.go
new file mode 100644
index 0000000..69adffa
--- /dev/null
+++ b/git.go
@@ -0,0 +1,59 @@
+package ffcss
+
+import (
+	"bytes"
+	"fmt"
+	"os/exec"
+)
+
+// currentRepoRemote returns the git repo's origin remote URL
+// if any error occurred while getting the URL, the empty string is returned.
+func currentRepoRemote() string {
+	var out bytes.Buffer
+	command := exec.Command("git", "config", "--get", "remote.origin.url")
+	command.Stdout = &out
+
+	err := command.Run()
+	if err != nil {
+		LogWarning("Could not get the current git remote origin's URL. Leaving repository entry blank.\n")
+		return ""
+	}
+	return out.String()
+}
+
+func switchGitBranch(newBranch, clonedTo string) error {
+	process := exec.Command("git", "switch", newBranch)
+	process.Dir = clonedTo
+	output, err := process.CombinedOutput()
+	if err != nil {
+		return fmt.Errorf("%w: %s", err, output)
+	}
+	return nil
+}
+
+func switchGitCommit(commitSHA, clonedTo string) error {
+	process := exec.Command("git", "checkout", commitSHA)
+	process.Dir = clonedTo
+	output, err := process.CombinedOutput()
+	if err != nil {
+		return fmt.Errorf("%w: %s", err, output)
+	}
+	return nil
+}
+
+func switchGitTag(tagName, clonedTo string) error {
+	process := exec.Command("git", "fetch", "--all", "--tags")
+	process.Dir = clonedTo
+	output, err := process.CombinedOutput()
+	if err != nil {
+		return fmt.Errorf("%w: %s", err, output)
+	}
+
+	process = exec.Command("git", "checkout", "tags/"+tagName)
+	process.Dir = clonedTo
+	output, err = process.CombinedOutput()
+	if err != nil {
+		return fmt.Errorf("%w: %s", err, output)
+	}
+	return nil
+}
diff --git a/git_test.go b/git_test.go
new file mode 100644
index 0000000..485f9bd
--- /dev/null
+++ b/git_test.go
@@ -0,0 +1 @@
+package ffcss
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..166f29c
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,25 @@
+module github.com/ewen-lbh/ffcss
+
+go 1.16
+
+require (
+	github.com/AlecAivazis/survey/v2 v2.3.2
+	github.com/alecthomas/chroma v0.10.0
+	github.com/bmatcuk/doublestar v1.3.4
+	github.com/charmbracelet/glamour v0.5.0
+	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
+	github.com/evilsocket/islazy v1.10.6
+	github.com/hbollon/go-edlib v1.6.0
+	github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69
+	github.com/mattn/go-colorable v0.1.12 // indirect
+	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
+	github.com/microcosm-cc/bluemonday v1.0.18 // indirect
+	github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db
+	github.com/muesli/termenv v0.11.0 // indirect
+	github.com/stretchr/testify v1.7.0
+	github.com/yuin/goldmark v1.4.10 // indirect
+	golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
+	golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 // indirect
+	golang.org/x/text v0.3.7
+	gopkg.in/yaml.v2 v2.4.0
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..af048ae
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,110 @@
+github.com/AlecAivazis/survey/v2 v2.3.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO4gCnU8=
+github.com/AlecAivazis/survey/v2 v2.3.2/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
+github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
+github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
+github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
+github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
+github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
+github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
+github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
+github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g=
+github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
+github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/evilsocket/islazy v1.10.6 h1:MFq000a1ByoumoJWlytqg0qon0KlBeUfPsDjY0hK0bo=
+github.com/evilsocket/islazy v1.10.6/go.mod h1:OrwQGYg3DuZvXUfmH+KIZDjwTCbrjy48T24TUpGqVVw=
+github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
+github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
+github.com/hbollon/go-edlib v1.6.0 h1:ga7AwwVIvP8mHm9GsPueC0d71cfRU/52hmPJ7Tprv4E=
+github.com/hbollon/go-edlib v1.6.0/go.mod h1:wnt6o6EIVEzUfgbUZY7BerzQ2uvzp354qmS2xaLkrhM=
+github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
+github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
+github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69 h1:umaj0TCQ9lWUUKy2DxAhEzPbwd0jnxiw1EI2z3FiILM=
+github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69/go.mod h1:zdLK9ilQRSMjSeLKoZ4BqUfBT7jswTGF8zRlKEsiRXA=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
+github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
+github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
+github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
+github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
+github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
+github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
+github.com/microcosm-cc/bluemonday v1.0.18 h1:6HcxvXDAi3ARt3slx6nTesbvorIc3QeTzBNRvWktHBo=
+github.com/microcosm-cc/bluemonday v1.0.18/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
+github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
+github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
+github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
+github.com/muesli/termenv v0.11.0 h1:fwNUbu2mfWlgicwG7qYzs06aOI8Z/zKPAv8J4uKbT+o=
+github.com/muesli/termenv v0.11.0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
+github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
+github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
+github.com/yuin/goldmark v1.4.10 h1:+WgKGo8CQrlMTRJpGCFCyNddOhW801TKC2QijVV9QVg=
+github.com/yuin/goldmark v1.4.10/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
+github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
+github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc=
+golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/hooks.go b/hooks.go
new file mode 100644
index 0000000..bb70dae
--- /dev/null
+++ b/hooks.go
@@ -0,0 +1,44 @@
+package ffcss
+
+import (
+	"fmt"
+	"os/exec"
+
+	"github.com/hoisie/mustache"
+)
+
+// runHook runs a provided command for a specific profile. See any of the (Manifest).Run*Hook methods
+// for a list of available {{mustache}} placeholders.
+func (t Theme) runHook(commandline string, profile FirefoxProfile) (output string, err error) {
+	ffversion, err := profile.FirefoxVersion()
+	if err != nil {
+		return "", fmt.Errorf("while getting firefox version for current profile: %w", err)
+	}
+
+	command := exec.Command("bash", "-c", mustache.Render(commandline, map[string]interface{}{
+		"profile_path":    profile.Path,
+		"firefox_version": ffversion.String(),
+	}))
+
+	outputBytes, err := command.CombinedOutput()
+	output = string(outputBytes)
+	if err != nil {
+		return "", fmt.Errorf("while running %q: %s: %w", command.String(), output, err)
+	}
+
+	return
+}
+
+// RunPreInstallHook passes the pre-install hook specified in the manifest's run.before entry to bash.
+// Several {{mustache}} placeholders are available:
+//
+//	profile_path        The current profile's path
+//	firefox_version     The current profile's Firefox version
+func (t Theme) RunPreInstallHook(profile FirefoxProfile) (output string, err error) {
+	return t.runHook(t.Run.Before, profile)
+}
+
+// RunPostInstallHook does the same as RunPreInstallHook but for the manifest's run.after entry.
+func (t Theme) RunPostInstallHook(profile FirefoxProfile) (output string, err error) {
+	return t.runHook(t.Run.After, profile)
+}
diff --git a/hooks_test.go b/hooks_test.go
new file mode 100644
index 0000000..485f9bd
--- /dev/null
+++ b/hooks_test.go
@@ -0,0 +1 @@
+package ffcss
diff --git a/installation.go b/installation.go
new file mode 100644
index 0000000..d753824
--- /dev/null
+++ b/installation.go
@@ -0,0 +1,134 @@
+package ffcss
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+)
+
+// InstallAssets installs the assets in the specified profile directory
+func (t Theme) InstallAssets(operatingSystem string, variant Variant, profileDir string) (err error) {
+	files, err := t.AssetsPaths(operatingSystem, variant)
+	if err != nil {
+		return fmt.Errorf("while gathering assets: %w", err)
+	}
+	LogDebug("gathered %d asset(s)", len(files))
+
+	for _, file := range files {
+		stat, err := os.Stat(file)
+		if err != nil {
+			return fmt.Errorf("couldn't check file %s: %w", file, err)
+		}
+
+		if stat.IsDir() {
+			continue
+		}
+
+		content, err := ioutil.ReadFile(file)
+		if err != nil {
+			return fmt.Errorf("while reading %s: %w", file, err)
+		}
+
+		destPath, err := t.DestinationPathOfAsset(file, profileDir, operatingSystem, variant)
+		if err != nil {
+			println(err.Error())
+			continue
+		}
+
+		err = os.MkdirAll(filepath.Dir(destPath), 0700)
+		if err != nil {
+			return fmt.Errorf("couldn't create parent directories for %s: %w", destPath, err)
+		}
+
+		err = ioutil.WriteFile(destPath, content, 0700)
+		if err != nil {
+			return fmt.Errorf("while writing to %s: %w", destPath, err)
+		}
+		LogDebug("wrote %s", destPath)
+
+	}
+	return nil
+}
+
+// InstallUserJS installs the content of user.js and the config entries to {{profileDir}}/user.js
+func (t Theme) InstallUserJS(operatingSystem string, variant Variant, profileDir string) error {
+	var content []byte
+	var err error
+
+	if t.UserJS != "" {
+		file := filepath.Join(t.DownloadedTo, renderFileTemplate(t.UserJS, operatingSystem, variant, t.OSNames))
+		content, err = ioutil.ReadFile(file)
+		if err != nil {
+			return fmt.Errorf("while reading %s: %w", file, err)
+		}
+
+	} else {
+		content = []byte{}
+	}
+
+	additionalContent, err := t.UserJSFileContent()
+	if err != nil {
+		return fmt.Errorf("while translating config entries to javascript: %w", err)
+	}
+
+	if additionalContent != "" {
+		content = []byte(string(content) + "\n" + additionalContent)
+		LogDebug("generated additional user.js content from config entries: %q", additionalContent)
+	}
+
+	if string(content) == "" {
+		return nil
+	}
+
+	err = ioutil.WriteFile(filepath.Join(profileDir, "user.js"), content, 0700)
+	if err != nil {
+		return fmt.Errorf("while writing: %w", err)
+	}
+
+	LogDebug("installed user.js @ %s", filepath.Join(profileDir, "user.js"))
+
+	return nil
+}
+
+// InstallUserChrome writes the content of userChrome to {{profileDir}}/chrome/userChrome.css
+func (t Theme) InstallUserChrome(os string, variant Variant, profileDir string) error {
+	if t.UserChrome == "" {
+		return nil
+	}
+	file := filepath.Join(t.DownloadedTo, renderFileTemplate(t.UserChrome, os, variant, t.OSNames))
+	content, err := ioutil.ReadFile(file)
+	if err != nil {
+		return fmt.Errorf("while reading %s: %w", file, err)
+	}
+
+	err = ioutil.WriteFile(filepath.Join(profileDir, "chrome", "userChrome.css"), content, 0700)
+	if err != nil {
+		return fmt.Errorf("while writing: %w", err)
+	}
+
+	LogDebug("installed userChrome.css @ %s", filepath.Join(profileDir, "chrome", "userChrome.css"))
+
+	return nil
+}
+
+// InstallUserContent writes the content of userContent to {{profileDir}}/chrome/userContent.css
+func (t Theme) InstallUserContent(os string, variant Variant, profileDir string) error {
+	if t.UserContent == "" {
+		return nil
+	}
+	file := filepath.Join(t.DownloadedTo, renderFileTemplate(t.UserContent, os, variant, t.OSNames))
+	content, err := ioutil.ReadFile(file)
+	if err != nil {
+		return fmt.Errorf("while reading %s: %w", file, err)
+	}
+
+	err = ioutil.WriteFile(filepath.Join(profileDir, "chrome", "userContent.css"), content, 0700)
+	if err != nil {
+		return fmt.Errorf("while writing: %w", err)
+	}
+
+	LogDebug("installed userContent.css @ %s", filepath.Join(profileDir, "chrome", "userContent.css"))
+
+	return nil
+}
diff --git a/installation_test.go b/installation_test.go
new file mode 100644
index 0000000..485f9bd
--- /dev/null
+++ b/installation_test.go
@@ -0,0 +1 @@
+package ffcss
diff --git a/manifest.go b/manifest.go
new file mode 100644
index 0000000..edb7424
--- /dev/null
+++ b/manifest.go
@@ -0,0 +1,344 @@
+package ffcss
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/charmbracelet/glamour"
+	"gopkg.in/yaml.v2"
+)
+
+// ThemeCompatWarningShown controls whether the ffcss incompatibility between the installed version and the theme's declared version (ffcss entry in the manifest)
+// is warned to the user.
+// Set to VersionMajor > 0 to comply with semver: versions with the major component (X._._) set to 0 can have breaking changes at any version change.
+var ThemeCompatWarningShown = VersionMajor > 0
+
+// Config represents a configuration map in a manifest, representing a set of values for the about:config page in Firefox
+type Config map[string]interface{}
+
+// Equal returns true if the config c has all of its values equal to the other Config.
+func (c Config) Equal(other Config) bool {
+	// XXX: this is not efficient AT ALL. It sucks. Change this.
+	for k, v := range c {
+		if v != other[k] {
+			return false
+		}
+	}
+	for k, v := range other {
+		if other[k] != v {
+			return false
+		}
+	}
+	return true
+}
+
+// FileTemplate represents a string with placeholders (e.g. "{{os}}") to be replaced by {{mustache}}.
+type FileTemplate = string
+
+// Variant represents a theme's variant. Most of the properties are identical to Theme's, because they overwrite the default values.
+type Variant struct {
+	// Properties exclusive to variants
+	Name string
+
+	// Properties that modify the "default variant"
+	DownloadAt  string `yaml:"download"`
+	Branch      string
+	Commit      string
+	Tag         string
+	Config      Config
+	UserChrome  FileTemplate `yaml:"userChrome"`
+	UserContent FileTemplate `yaml:"userContent"`
+	UserJS      FileTemplate `yaml:"user.js"`
+	Assets      []FileTemplate
+	Description string
+	Addons      []string
+	Run         struct {
+		Before string
+		After  string
+	}
+	Message string
+}
+
+// Theme represents a FirefoxCSS theme, read from a manifest YAML file. (See LoadManifest).
+type Theme struct {
+	// Internal, cannot be set in the YAML file
+	currentVariantName string `yaml:"-"` // Used to construct the directory where the theme will be cached
+	raw                string `yaml:"-"` // Contains the raw yaml file contents
+	DownloadedTo       string `yaml:"-"` // Stores the path to the directory where the theme is cached. Set by .Download().
+
+	// Top-level (non-variant-modifiable)
+	FfcssVersion             int                      `yaml:"ffcss"`
+	FirefoxVersion           string                   `yaml:"firefox,omitempty"`
+	FirefoxVersionConstraint FirefoxVersionConstraint `yaml:"-"`
+	ExplicitName             string                   `yaml:"name"`
+	Author                   string                   `yaml:"by"`
+	Description              string
+	Variants                 map[string]Variant
+	OSNames                  map[string]string `yaml:"os,omitempty"`
+
+	// Override-able by variants
+	DownloadAt  string `yaml:"download"`
+	Branch      string
+	Commit      string `yaml:",omitempty"`
+	Tag         string `yaml:",omitempty"`
+	Config      Config
+	UserChrome  FileTemplate `yaml:"userChrome"`
+	UserContent FileTemplate `yaml:"userContent"`
+	UserJS      FileTemplate `yaml:"user.js"`
+	Assets      []FileTemplate
+	CopyFrom    string `yaml:"copy from,omitempty"`
+	Addons      []string
+	Run         struct {
+		Before string
+		After  string
+	}
+	Message string
+}
+
+// ManifestKeyGroupsStarts specifies at which keys a group of related keys starts.
+// This assumes that the YAML keys are displayed/written in the order they are defined in (see Theme).
+//
+// It is used to add blank lines in generated manifests: for example, above the 'variant' key, a blank line should be added to add grouping.
+var ManifestKeyGroupsStarts = [...]string{"name", "variants", "os", "download", "config", "run", "message"}
+
+// NewTheme creates a new Theme with vital defaults (namely the config entry to enable CSS customization of Firefox).
+func NewTheme() Theme {
+	return Theme{
+		Config: Config{
+			"toolkit.legacyUserProfileCustomizations.stylesheets": true,
+		},
+		Variants: map[string]Variant{},
+		Assets:   []FileTemplate{},
+	}
+}
+
+// LoadManifest loads a ffcss.yaml file into a Theme object.
+func LoadManifest(manifestPath string) (manifest Theme, err error) {
+	raw, err := os.ReadFile(manifestPath)
+	if err != nil {
+		err = fmt.Errorf("while reading manifest %s: %w", manifestPath, err)
+		return
+	}
+	manifest = NewTheme()
+	manifest.raw = string(raw)
+	err = yaml.Unmarshal(raw, &manifest)
+
+	if manifest.FfcssVersion < 0 {
+		err = fmt.Errorf("ffcss version cannot be negative but is set to %d", manifest.FfcssVersion)
+		return
+	}
+
+	if manifest.FfcssVersion != VersionMajor && !ThemeCompatWarningShown && manifest.FfcssVersion != 0 {
+		LogWarning("ffcss %s is installed, but you are using a theme made for ffcss %d.X.X. Some things may not work.\n", VersionString, manifest.FfcssVersion)
+		ThemeCompatWarningShown = true
+	}
+
+	if manifest.Name() == TempDownloadsDirName {
+		err = fmt.Errorf("invalid theme name %q", TempDownloadsDirName)
+		return
+	}
+
+	if manifest.Name() == "" {
+		err = fmt.Errorf("theme has no name")
+		return
+	}
+
+	for key := range manifest.OSNames {
+		if key != "linux" && key != "macos" && key != "windows" {
+			return Theme{}, fmt.Errorf("%s is not a valid os replacement target. Targets are macos, windows and linux", key)
+		}
+	}
+
+	for name, variant := range manifest.Variants {
+		if name == RootVariantName {
+			err = fmt.Errorf("invalid variant name %q", name)
+			return
+		}
+		variantWithName := variant
+		variantWithName.Name = name
+		manifest.Variants[name] = variantWithName
+	}
+	manifest.currentVariantName = RootVariantName // ensure the current variant's name wasn't manipulated by the YAML unmarshaling
+	if err != nil {
+		err = fmt.Errorf("while parsing manifest %s: %w", manifestPath, err)
+		return
+	}
+	manifest.DownloadedTo = CacheDir(manifest.Name(), manifest.currentVariantName)
+	if manifest.FirefoxVersion != "" {
+		manifest.FirefoxVersionConstraint, err = NewFirefoxVersionConstraint(manifest.FirefoxVersion)
+		if err != nil {
+			err = fmt.Errorf("invalid Firefox version constraint %q: %w", manifest.FirefoxVersion, err)
+			return
+		}
+
+	}
+	return
+}
+
+// WithVariant returns a Manifest representing the theme if the selected variant
+// was used as the "root values".
+// i.e. the values of UserJS, UserContent, UserChrome, Assets are replaced with their variant's, if set,
+// and the value of Config is combined with the variant's.
+// Some variants change the git branch, the entire repository or other settings that require external actions.
+// Those are returned in actionsNeeded as a struct of booleans with descriptive field names.
+func (t Theme) WithVariant(variant Variant) (newTheme Theme, actionsNeeded struct{ switchBranch, reDownload bool }) {
+	// TODO might clean this up with reflection, selecting fields that are both in Manifest & Variant
+	newTheme = t
+	newTheme.currentVariantName = variant.Name
+	if variant.UserChrome != "" {
+		newTheme.UserChrome = variant.UserChrome
+	}
+	if variant.UserContent != "" {
+		newTheme.UserContent = variant.UserContent
+	}
+	if variant.UserJS != "" {
+		newTheme.UserJS = variant.UserJS
+	}
+	if variant.Message != "" {
+		newTheme.Message = variant.Message
+	}
+	if len(variant.Assets) > 0 {
+		newTheme.Assets = variant.Assets
+	}
+	if variant.DownloadAt != "" {
+		actionsNeeded.reDownload = true
+		newTheme.DownloadAt = variant.DownloadAt
+	}
+	if variant.Branch != "" {
+		actionsNeeded.switchBranch = true
+		newTheme.Branch = variant.Branch
+	}
+	if variant.Commit != "" {
+		newTheme.Commit = variant.Commit
+	}
+	if variant.Tag != "" {
+		newTheme.Tag = variant.Tag
+	}
+	if variant.Run.Before != "" {
+		newTheme.Run.Before = variant.Run.Before
+	}
+	if variant.Run.After != "" {
+		newTheme.Run.After = variant.Run.After
+	}
+	for key, val := range variant.Config {
+		newTheme.Config[key] = val
+	}
+	if actionsNeeded.reDownload || actionsNeeded.switchBranch {
+		newTheme.DownloadedTo = CacheDir(newTheme.Name(), newTheme.currentVariantName)
+	}
+	return newTheme, actionsNeeded
+}
+
+// Name returns a theme's name. If the name was explicitly set in the manifest (i.e. if t.ExplicitName is not empty), it is returned.
+// Otherwise, the name is guessed.
+// Currently, it is only guessed if a github repository is set for t.DownloadAt.
+// If guessing is not possible, it returns the empty string.
+func (t Theme) Name() string {
+	if t.ExplicitName != "" {
+		return strings.ToLower(t.ExplicitName)
+	}
+	if strings.HasPrefix(t.DownloadAt, "https://github.com") {
+		fragments := strings.Split(t.DownloadAt, "/")
+		return strings.ToLower(fragments[len(fragments)-1])
+	}
+	return ""
+}
+
+// AvailableVariants lists the possible variant names to choose from
+func (t Theme) AvailableVariants() []string {
+	names := make([]string, 0, len(t.Variants))
+	for name := range t.Variants {
+		names = append(names, name)
+	}
+	return names
+}
+
+// ShowMessage renders the message and prints it to the user
+func (t Theme) ShowMessage() error {
+	scheme := os.Getenv("COLORSCHEME")
+	if scheme != "light" && scheme != "dark" {
+		// TODO: detect with the terminal's current background color as a fallback
+		scheme = "dark"
+	}
+	rendered, err := glamour.Render(t.Message, scheme)
+	if err != nil {
+		return fmt.Errorf("while rendering message: %w", err)
+	}
+
+	if strings.TrimSpace(rendered) != "" {
+		fmt.Fprintln(out, rendered)
+	}
+	return nil
+}
+
+// ManifestPath returns the path of a theme's manifest file
+func ManifestPath(themeRoot string) string {
+	return filepath.Join(themeRoot, "ffcss.yaml")
+}
+
+// GenerateManifest returns the YAML contents of the manifest corresponding to the given theme.
+// If t.Raw is set, it'll return it.
+// Otherwise, it serializes the values into YAML, following the Theme struct.
+func (t Theme) GenerateManifest() (string, error) {
+	if t.raw != "" {
+		return t.raw, nil
+	}
+	t.ExplicitName = t.Name()
+	// Remove redundant keys
+	if t.Config.Equal(NewTheme().Config) {
+		t.Config = Config{}
+	}
+	contentBytes, err := yaml.Marshal(t)
+	if err != nil {
+		return "", err
+	}
+
+	content := string(contentBytes)
+
+	for _, key := range ManifestKeyGroupsStarts {
+		content = strings.Replace(content, key+": ", "\n"+key+": ", 1)
+		content = strings.Replace(content, key+":\n", "\n"+key+":\n", 1)
+	}
+
+	return content, nil
+}
+
+// WriteManifest writes the contents of t as a YAML file named ffcss.yaml
+// inside inDirectory.
+// It adds a comment mentioning the documentation at the top of the file.
+// See GenerateManifest to see how the contents of the file are generated.
+func (t Theme) WriteManifest(inDirectory string) error {
+	content, err := t.GenerateManifest()
+	if err != nil {
+		return fmt.Errorf("while generating manifest contents for %s: %w", t.Name(), err)
+	}
+
+	content = "# This is a manifest for a FirefoxCSS theme. \n# See https://github.com/ewen-lbh/ffcss for more information.\n" + content
+
+	err = ioutil.WriteFile(filepath.Join(inDirectory, "ffcss.yaml"), []byte(content), 0700)
+	if err != nil {
+		return fmt.Errorf("while writing the manifest: %w", err)
+	}
+
+	return nil
+}
+
+// InitializeTheme returns a new, blank theme, but with some values guessed from the current context.
+// Meant to be used by "ffcss init".
+func InitializeTheme(workingDir string) (Theme, error) {
+	theme := NewTheme()
+
+	theme.DownloadAt = strings.TrimSuffix(currentRepoRemote(), ".git")
+	if theme.DownloadAt == "" {
+		theme.DownloadAt = "TODO"
+	}
+
+	if !strings.Contains(theme.DownloadAt, "https://github.com") {
+		theme.ExplicitName = filepath.Base(workingDir)
+	}
+
+	return theme, nil
+}
diff --git a/manifest_test.go b/manifest_test.go
new file mode 100644
index 0000000..baba3bc
--- /dev/null
+++ b/manifest_test.go
@@ -0,0 +1,100 @@
+package ffcss
+
+import (
+	"math"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewTheme(t *testing.T) {
+	assert.Equal(t, Theme{
+		Config: Config{
+			"toolkit.legacyUserProfileCustomizations.stylesheets": true,
+		},
+		Variants: map[string]Variant{},
+		Assets:   []string{},
+	}, NewTheme())
+}
+
+func TestLoadManifest(t *testing.T) {
+	errorCases := []struct{ manifestName, errorPart string }{
+		{"ffcss_version_negative", "ffcss version cannot be negative"},
+		{"invalid_firefox_constraint", "invalid Firefox version constraint"},
+		{"no_name", "no name"},
+		{"temp_download_dir_as_name", "invalid theme name \"" + TempDownloadsDirName + "\""},
+		{"temp_download_dir_as_name_via_github_remote", "invalid theme name \"" + TempDownloadsDirName + "\""},
+		{"root_variant_name", "invalid variant name \"" + RootVariantName + "\""},
+		{"unknown_os_key", "hannah montana is not a valid os replacement target. Targets are macos, windows and linux"},
+		{"wrong_casing_os_key", "MacOS is not a valid os replacement target. Targets are macos, windows and linux"},
+	}
+
+	// TODO when out of 0.x.x, test for warning appearing when ffcss version incompatible (and appearing only _once_)
+	// {"ffcss_version_99999"
+
+	for _, caze := range errorCases {
+		_, err := LoadManifest(filepath.Join(testarea, "manifests", caze.manifestName+".yaml"))
+		assert.Error(t, err, "case %s needs to contain %q in error message", caze.manifestName, caze.errorPart)
+		if err != nil {
+			assert.Contains(t, err.Error(), caze.errorPart)
+		}
+	}
+
+	actual, err := LoadManifest(filepath.Join(testarea, "manifests", "fine.yaml"))
+	fileContents, _ := os.ReadFile(filepath.Join(testarea, "manifests", "fine.yaml"))
+	assert.NoError(t, err)
+	assert.Equal(t, Theme{
+		currentVariantName:       RootVariantName,
+		raw:                      string(fileContents),
+		DownloadedTo:             filepath.Join(mockedHomedir, ".cache", "ffcss", "a fine theme", RootVariantName),
+		FfcssVersion:             0,
+		FirefoxVersion:           "89+",
+		FirefoxVersionConstraint: FirefoxVersionConstraint{Min: FirefoxVersion{89, -1}, Max: FirefoxVersion{math.MaxInt32, math.MaxInt32}, Sentence: "version 89.x or higher"},
+		ExplicitName:             "a fine theme",
+		Author:                   "some nice person",
+		Description:              "Lorem ipsum _dolor_ sit am**et**\n",
+		Variants: map[string]Variant{
+			"default": {
+				Name: "default",
+			},
+			"new moon": {
+				Name:       "new moon",
+				DownloadAt: "https://example.com/new-moon",
+			},
+		},
+		OSNames: map[string]string{
+			"linux": "GNU+Linux",
+		},
+		DownloadAt: "https://example.com/.git",
+		Branch:     "sun",
+		Commit:     "85dfe1ac",
+		Tag:        "v0.184.668",
+		Config: Config{
+			"legacy.some-config-entry":                            "yeees",
+			"toolkit.legacyUserProfileCustomizations.stylesheets": true,
+			"zincoxide": true,
+		},
+		UserChrome:  "userChrome.sass",
+		UserContent: "./{{os}}/userContent--{{variant}}.css",
+		UserJS:      "user.ls",
+		Assets: []string{
+			"chrome/**",
+			"logos/*.svg",
+		},
+		CopyFrom: "chromeee/",
+		Addons: []string{
+			"https://example.com/extensions/a",
+			"https://example.com/extensions/b",
+		},
+		Run: struct {
+			Before string
+			After  string
+		}{
+			Before: "cd /; tree; echo you have been hacked",
+			After:  "echo hacking complete 😎",
+		},
+		Message: "Here's a choccy milk :) <https://i.redd.it/sh9re7861t851.png>\n",
+	}, actual)
+}
diff --git a/profiles.go b/profiles.go
new file mode 100644
index 0000000..d0f01a7
--- /dev/null
+++ b/profiles.go
@@ -0,0 +1,176 @@
+package ffcss
+
+import (
+	"fmt"
+	"os"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"runtime"
+	"strings"
+)
+
+// FirefoxProfile represents a Firefox Profile stored at Path. Each Firefox Profile is a separate instance of Firefox settings and other user data.
+// See https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data for more information.
+type FirefoxProfile struct {
+	ID   string
+	Name string
+	Path string
+}
+
+type firefoxProfileWithVersion = struct {
+	Profile FirefoxProfile
+	Version FirefoxVersion
+}
+
+// FullName returns the basename of ffp.Path
+func (ffp FirefoxProfile) FullName() string {
+	return filepath.Base(ffp.Path)
+}
+
+// String returns a string representation of the profile.
+func (ffp FirefoxProfile) String() string {
+	return fmt.Sprintf("%s (%s)", ffp.Name, ffp.ID)
+}
+
+// NewFirefoxProfileFromPath returns a FirefoxProfile by parsing the path into and ID and a Name.
+func NewFirefoxProfileFromPath(path string) FirefoxProfile {
+	base := filepath.Base(path)
+	parts := strings.Split(base, ".")
+	return FirefoxProfile{
+		Path: path,
+		ID:   parts[0],
+		Name: parts[1],
+	}
+}
+
+// NewFirefoxProfileFromDisplay returns a FirefoxProfile by matching the given displayString against profiles'.
+// See (FirefoxProfile).Display for the definition of a display string.
+func NewFirefoxProfileFromDisplay(displayString string, profiles []FirefoxProfile) FirefoxProfile {
+	for _, profile := range profiles {
+		ffp := NewFirefoxProfileFromPath(profile.Path)
+		if ffp.Display() == displayString {
+			return ffp
+		}
+	}
+	LogDebug("while searching for %s in %v", displayString, profiles)
+	panic("internal error: can't get profile from display string")
+}
+
+// ProfilePaths returns an array of profile directories from the profile folder.
+// 1 arguments: the profiles folder is assumed to be the current OS's default.
+// 2 argument: use the given profiles folder
+// more arguments: panic.
+func ProfilePaths(operatingSystem string, optionalProfilesDir ...string) ([]string, error) {
+	var profilesFolder string
+	if len(optionalProfilesDir) == 0 {
+		// XXX: Weird golang thing, if I assign to profilesFolder directly, it tells me the variable is unused
+		_profilesFolder, err := DefaultProfilesDir(operatingSystem)
+		profilesFolder = _profilesFolder
+		if err != nil {
+			return []string{}, fmt.Errorf("couldn't get the profiles folder: %w. Try to use --profiles-dir", err)
+		}
+	} else if len(optionalProfilesDir) == 1 {
+		profilesFolder = optionalProfilesDir[0]
+	} else {
+		panic(fmt.Sprintf("received %d arguments, expected 1 or 2", len(optionalProfilesDir)+1))
+	}
+	directories, err := os.ReadDir(profilesFolder)
+	releasesPaths := make([]string, 0)
+	patternReleaseID := regexp.MustCompile(`[a-z0-9]{8}\.\w+`)
+	if err != nil {
+		return []string{}, fmt.Errorf("couldn't read %s: %w", profilesFolder, err)
+	}
+	for _, releasePath := range directories {
+		if patternReleaseID.MatchString(releasePath.Name()) {
+			stat, err := os.Stat(filepath.Join(profilesFolder, releasePath.Name()))
+			if err != nil {
+				continue
+			}
+			if stat.IsDir() {
+				releasesPaths = append(releasesPaths, filepath.Join(profilesFolder, releasePath.Name()))
+			}
+		}
+	}
+	return releasesPaths, nil
+}
+
+// Profiles returns a list of FirefoxProfiles. If optionalProfilesDir is "",
+// DefaultProfilesDir() is used to get the profiles' directory.
+func Profiles(optionalProfilesDir string) ([]FirefoxProfile, error) {
+	var profiles []FirefoxProfile
+	var err error
+	var profilePaths []string
+
+	if optionalProfilesDir != "" {
+		profilePaths, err = ProfilePaths(GOOStoOS(runtime.GOOS), optionalProfilesDir)
+	} else {
+		profilePaths, err = ProfilePaths(GOOStoOS(runtime.GOOS))
+	}
+
+	if err != nil {
+		return profiles, fmt.Errorf("while getting profile paths: %w", err)
+	}
+
+	for _, profilePath := range profilePaths {
+		profiles = append(profiles, NewFirefoxProfileFromPath(profilePath))
+	}
+
+	return profiles, nil
+}
+
+// DefaultProfilesDir returns the operating-system-dependent default location for the Firefox profiles' directories.
+func DefaultProfilesDir(operatingSystem string) (string, error) {
+	switch operatingSystem {
+	case "linux":
+		homedir, err := os.UserHomeDir()
+		if err != nil {
+			return "", err
+		}
+		return filepath.Join(homedir, ".mozilla", "firefox"), nil
+	case "macos":
+		user, err := user.Current()
+		if err != nil {
+			return "", fmt.Errorf("couldn't get the current user: %w", err)
+		}
+
+		return filepath.Join("/Users", user.Username, "Library", "Application Support", "Firefox", "Profiles"), nil
+	case "windows":
+		homedir, err := os.UserHomeDir()
+		if err != nil {
+			return "", err
+		}
+
+		return filepath.Join(homedir, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles"), nil
+	}
+	return "", fmt.Errorf("unknown operating system %s", operatingSystem)
+}
+
+// IncompatibleProfiles returns the list of profiles that don't meet the Firefox version constraint specified by the theme, along with the detect Firefox version of each profile.
+func (t Theme) IncompatibleProfiles(profiles []FirefoxProfile) ([]firefoxProfileWithVersion, error) {
+	if t.FirefoxVersion != "" {
+		incompatibleProfileDirs := make([]firefoxProfileWithVersion, 0)
+		for _, profile := range profiles {
+			profileVersion, err := profile.FirefoxVersion()
+			if err != nil {
+				LogWarning("Couldn't get firefox version for profile %s", profile)
+			}
+			fulfillsConstraint := t.FirefoxVersionConstraint.FulfilledBy(profileVersion)
+			if !fulfillsConstraint {
+				incompatibleProfileDirs = append(incompatibleProfileDirs, firefoxProfileWithVersion{profile, profileVersion})
+			}
+		}
+		return incompatibleProfileDirs, nil
+	}
+	return []firefoxProfileWithVersion{}, nil
+}
+
+// BackupChrome moves the chrome/ folder to chrome.bak/
+func (ffp FirefoxProfile) BackupChrome() error {
+	return renameIfExists(filepath.Join(ffp.Path, "chrome"), filepath.Join(ffp.Path, "chrome.bak"))
+}
+
+// BackupUserJS moves user.js to user.js.bak if user.js exists
+func (ffp FirefoxProfile) BackupUserJS() error {
+	return renameIfExists(filepath.Join(ffp.Path, "user.js"), filepath.Join(ffp.Path, "user.js.bak"))
+}
diff --git a/profiles_test.go b/profiles_test.go
new file mode 100644
index 0000000..485f9bd
--- /dev/null
+++ b/profiles_test.go
@@ -0,0 +1 @@
+package ffcss
diff --git a/testdata/catalogs/empty/.gitkeep b/testdata/catalogs/empty/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/testdata/catalogs/various/.yaml b/testdata/catalogs/various/.yaml
new file mode 100644
index 0000000..12f49c9
--- /dev/null
+++ b/testdata/catalogs/various/.yaml
@@ -0,0 +1,16 @@
+ffcss: 0
+
+name: compactmode
+download: https://github.com/Godiesc/compactmodefirefoxcss
+
+user.js: user.js
+userChrome: chrome/userChrome.css
+
+addons:
+  - https://addons.mozilla.org/en-US/firefox/addon/beautifultotaldark/
+
+variants:
+  default: {}
+  photon-tabs:
+    user.js: OtherCss/user.js
+    userChrome: OtherCss/chrome/userChrome.css
diff --git a/testdata/catalogs/various/.yml b/testdata/catalogs/various/.yml
new file mode 100644
index 0000000..f3c5b8d
--- /dev/null
+++ b/testdata/catalogs/various/.yml
@@ -0,0 +1,16 @@
+ffcss: 0
+
+name: lepton
+download: https://github.com/black7375/Firefox-UI-Fix
+
+user.js: user.js
+userChrome: userChrome.css
+userContent: userContent.css
+
+asssets:
+  - icons/**
+
+variants:
+  default: {}
+  photon-style:
+    branch: photon-style
diff --git a/testdata/catalogs/various/alpenblue.yaml b/testdata/catalogs/various/alpenblue.yaml
new file mode 120000
index 0000000..40df2c3
--- /dev/null
+++ b/testdata/catalogs/various/alpenblue.yaml
@@ -0,0 +1 @@
+../../../themes/alpenblue.yaml
\ No newline at end of file
diff --git a/testdata/catalogs/various/flyingfox.yaml b/testdata/catalogs/various/flyingfox.yaml
new file mode 120000
index 0000000..acdc3cd
--- /dev/null
+++ b/testdata/catalogs/various/flyingfox.yaml
@@ -0,0 +1 @@
+../../../themes/flyingfox.yaml
\ No newline at end of file
diff --git a/testdata/catalogs/various/montereyfox.yaml b/testdata/catalogs/various/montereyfox.yaml
new file mode 120000
index 0000000..8ed5fe3
--- /dev/null
+++ b/testdata/catalogs/various/montereyfox.yaml
@@ -0,0 +1 @@
+../../../themes/montereyfox.yaml
\ No newline at end of file
diff --git a/testdata/catalogs/various/noext b/testdata/catalogs/various/noext
new file mode 100644
index 0000000..488a34e
--- /dev/null
+++ b/testdata/catalogs/various/noext
@@ -0,0 +1,2 @@
+ffcss: 0
+name: no extension
diff --git a/testdata/catalogs/various/sometheme.yml b/testdata/catalogs/various/sometheme.yml
new file mode 100644
index 0000000..9564cda
--- /dev/null
+++ b/testdata/catalogs/various/sometheme.yml
@@ -0,0 +1,2 @@
+ffcss: 0
+name: ugly 8.3 filename, like we're still on windows 2.1 or whatever
diff --git a/testdata/home/.cache/ffcss/materialfox/_/.github/ISSUE_TEMPLATE/bug_report.md b/testdata/home/.cache/ffcss/materialfox/_/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..19d520a
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,33 @@
+---
+name: Bug report
+about: Create a report to help us improve the theme
+title: "[Feature/element] broken when [doing something]"
+labels: bug
+assignees: ''
+
+---
+
+**Sanity checks (must complete)**
+- [ ] I have read and followed the installation instructions in the README
+- [ ] I have not modified the userChrome.css file
+- [ ] I have tested the latest release for my Firefox version, or commit on master/beta branch (beta is for Firefox Beta only)
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Enable '...'
+2. Go to '....'
+3. Click on '....'
+4. See issue
+
+**Screenshots**
+Add screenshots to help explain your problem.
+
+**System info**
+ - OS: [e.g. Windows 10]
+ - Firefox version: [e.g. 75]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/testdata/home/.cache/ffcss/materialfox/_/.gitignore b/testdata/home/.cache/ffcss/materialfox/_/.gitignore
new file mode 100644
index 0000000..8a90ad3
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/.gitignore
@@ -0,0 +1,140 @@
+# macOS
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# Windows
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Linux
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+# JetBrains
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn.  Uncomment if using
+# auto-import.
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/LICENSE b/testdata/home/.cache/ffcss/materialfox/_/LICENSE
new file mode 100644
index 0000000..28d3937
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 muckSponge
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/testdata/home/.cache/ffcss/materialfox/_/README.md b/testdata/home/.cache/ffcss/materialfox/_/README.md
new file mode 100644
index 0000000..aefbf28
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/README.md
@@ -0,0 +1,37 @@
+# MaterialFox
+*A Material Design-inspired userChrome.css theme for Firefox*
+
+![Preview](https://user-images.githubusercontent.com/5405629/45172944-21d91900-b24a-11e8-8bc5-03814121b0de.png)
+This theme is powered by blood, sweat, and coffee. If you like it, please consider helping out to support its continued development.
+
+[![Buy me a coffee](https://svgshare.com/i/8Yd.svg)](https://www.buymeacoffee.com/n4ho5QX2l)
+
+## What this does
+Inspired by Google's Material Design and their latest Google Chrome UI, this theme turns your Firefox into a Material-styled web browser. The aim was to style the browser as closely as possible to Google Chrome, where practical.
+
+This is a userChrome.css theme, which means you must manually add it to your Firefox profile. The theme overrides certain browser styles. Currently, only the main UI is affected (settings pages, etc. are not). More elements of the UI may be styled in the future but a broader scope becomes harder to maintain as Mozilla updates their browser code so some UI styles may be culled or redone if they become unmaintainable.
+
+## What version do I use?
+Check the [releases](https://github.com/muckSponge/MaterialFox/releases) section. Each release version will match the compatible Firefox version. For example, if you're using Firefox 88, try a v88.x release. If there's no matching release and you're not using an old version of Firefox, go for the latest one. If you're using a beta version of Firefox, you might want to try the master branch, which will have the latest bug fixes.
+
+## Installation
+1. Copy the chrome folder and user.js file into your Firefox profile directory. To find your profile directory, go to about:support or about:profiles.
+2. See [Recommended instructions](#recommended-instructions) if you'd prefer a more Chrome-like experience.
+3. Restart Firefox.
+
+### Recommended instructions
+Add space above tab bar:
+* Right click on toolbar -> Customize.
+* Check Drag Space checkbox.
+
+Replicate Chrome behaviour for clipped tabs:
+* [about:config] Set ```browser.tabs.tabClipWidth``` to ```83``` (default is ```140```).
+
+Replicate Chrome's "Not Secure" text on HTTP:
+* [about:config] Set ```security.insecure_connection_text.enabled``` to ```true```.
+
+## Please note
+* Linux is no longer officially supported but you can give it a try – if you'd like to work on it feel free to make a PR.
+* Some customisation settings may no longer work (such as compact/touch density).
+* Some custom themes may clash with the address bar.
+* Some themes using transparency might not work.
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/findbar/findbar.css b/testdata/home/.cache/ffcss/materialfox/_/chrome/findbar/findbar.css
new file mode 100755
index 0000000..cfc694c
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/findbar/findbar.css
@@ -0,0 +1,73 @@
+.findbar-textbox
+{
+	background-color: transparent !important;
+	background-image: url(../icons/search.svg) !important;
+	background-repeat: no-repeat !important;
+	background-position: 5px center !important;
+	fill-opacity: 1 !important;
+	border: 0 !important;
+	box-shadow: none !important;
+	margin-inline-start: 2px !important;
+	padding-inline-start: 28px !important;
+}
+
+hbox[anonid="findbar-textbox-wrapper"]
+{
+	background-color: var(--toolbar-field-background-color) !important;
+	border-radius: 16px !important;
+	position: relative !important;
+	display: flex !important;
+	padding: 2px !important;
+	height: 28px !important;
+	transition: background-color .1s var(--ease-basic) !important;
+}
+
+hbox[anonid="findbar-textbox-wrapper"]:hover
+{
+	background-color: var(--toolbar-field-hover-background-color) !important;
+}
+
+hbox[anonid="findbar-textbox-wrapper"]:focus-within
+{
+	background-color: var(--toolbar-field-focus-background-color) !important;
+	/* won't work in a var... */
+	border: 2px solid -moz-accent-color !important;
+	margin: -1px !important;
+	padding: 1px !important;
+	height: 30px !important;
+}
+
+hbox[anonid="findbar-textbox-wrapper"] toolbarbutton
+{
+	border-radius: 99px !important;
+	border: 0 !important;
+	width: 24px !important;
+	height: 24px !important;
+	padding: 0 !important;
+	display: flex !important;
+	background: 0 !important;
+	transition: background-color .2s var(--ease-basic) !important;
+	margin: 0 !important;
+	box-shadow: none !important;
+}
+
+hbox[anonid="findbar-textbox-wrapper"] toolbarbutton[disabled]
+{
+	display: none !important;
+}
+
+hbox[anonid="findbar-textbox-wrapper"] toolbarbutton .toolbarbutton-text
+{
+	visibility: collapse !important;
+}
+
+hbox[anonid="findbar-textbox-wrapper"] toolbarbutton:not([disabled]):hover
+{
+	background-color: var(--toolbarbutton-hover-background) !important;
+}
+
+hbox[anonid="findbar-textbox-wrapper"] toolbarbutton:not([disabled]):hover:active
+{
+	background-color: var(--toolbarbutton-active-background) !important;
+	transition-duration: 0 !important;
+}
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/global/circle.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/global/circle.svg
new file mode 100644
index 0000000..b4b4617
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/global/circle.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<circle fill="context-fill" fill-opacity="context-fill-opacity" cx="8" cy="8" r="8"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/global/global.css b/testdata/home/.cache/ffcss/materialfox/_/chrome/global/global.css
new file mode 100755
index 0000000..21e4a61
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/global/global.css
@@ -0,0 +1,294 @@
+@media (-moz-windows-compositor)
+{
+	@media not (-moz-os-version: windows-win7)
+	{
+		@media not (-moz-os-version: windows-win8)
+		{
+			@media (-moz-windows-default-theme)
+			{
+				:root[tabsintitlebar] .titlebar-button
+				{
+					transform: translateY(-8px) !important;
+				}
+				
+				@media (-moz-windows-accent-color-in-titlebar: 0)
+				{					
+					:root[tabsintitlebar]:not(:-moz-lwtheme):not([privatebrowsingmode=temporary])
+					{
+						background-color: #dee1e6 !important;
+						color: #3c4043 !important;
+        		  	}
+					
+					:root[tabsintitlebar]:not(:-moz-lwtheme):not([privatebrowsingmode=temporary]):-moz-window-inactive
+					{
+						background-color: #e7eaed !important;
+						color: #666a6d !important;
+        		  	}
+        		}
+      		}
+		}
+	}
+}
+
+#navigator-toolbox :-moz-any(toolbar, #nav-bar-customization-target):not(#toolbar-menubar)
+{
+	-moz-box-align: center !important;
+}
+
+#navigator-toolbox :-moz-any(.toolbarbutton-1, toolbarbutton.bookmark-item:not(.subviewbutton)),
+/* no ::part workaround - may have side effects */
+#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton),
+#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton)
+{
+	border-radius: 99px !important;
+	transition-property:
+		background-color,
+		background-size,
+		fill-opacity !important;
+	transition-duration: .3s !important;
+	transition-timing-function:
+		var(--ease-basic),
+		var(--ease-out),
+		var(--ease-basic) !important;
+
+	fill: currentColor !important;
+	fill-opacity: 0 !important;
+
+	background-image:
+		url(circle.svg),
+		url(circle.svg) !important;
+	background-size: 25% !important;
+	background-repeat: no-repeat !important;
+	background-position: center !important;
+	background-color: transparent !important;
+}
+
+#navigator-toolbox :-moz-any(.toolbarbutton-1, toolbarbutton.bookmark-item:not(.subviewbutton)),
+/* no ::part workaround - may have side effects */
+#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton),
+#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton)
+{
+	font-size: 12px !important;
+	padding: 0 !important;
+	margin: 0 !important;
+	border: 2px solid transparent !important;
+	background-clip: padding-box !important;
+}
+
+#navigator-toolbox :-moz-any(.toolbarbutton-1, .scrollbutton-up, .scrollbutton-down),
+/* no ::part workaround - may have side effects */
+.#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton),
+.#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton)
+{
+	max-height: 32px !important;
+	height: 32px !important;
+	min-height: 32px !important;
+}
+
+#navigator-toolbox :-moz-any(.toolbarbutton-1, toolbarbutton.bookmark-item:not(.subviewbutton))[disabled],
+/* no ::part workaround - may have side effects */
+#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton)[disabled],
+#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton)[disabled]
+{
+	opacity: .42 !important;
+}
+
+#navigator-toolbox :-moz-any(.toolbarbutton-1, toolbarbutton.bookmark-item:not(.subviewbutton)):hover:not([disabled]),
+#navigator-toolbox :-moz-any(.toolbarbutton-1, toolbarbutton.bookmark-item:not(.subviewbutton)):-moz-any(:hover:active, [checked], [open]):not([disabled]),
+/* no ::part workaround - may have side effects */
+#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton):hover:not([disabled]),
+#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton):hover:not([disabled])
+{
+	background-color: var(--toolbarbutton-hover-background) !important;
+}
+
+#navigator-toolbox :-moz-any(.toolbarbutton-1, toolbarbutton.bookmark-item:not(.subviewbutton)):-moz-any(:hover:active, [checked], [open]):not([disabled]),
+/* no ::part workaround - may have side effects */
+#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton):hover:active:not([disabled]),
+#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton):hover:active:not([disabled])
+{
+	fill-opacity: .04 !important;
+	background-size: 100% !important;
+}
+
+#navigator-toolbox :-moz-any(.toolbarbutton-icon, .toolbarbutton-badge-stack),
+/* no ::part workaround - may have side effects */
+#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton) > .toolbarbutton-icon,
+#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton) > .toolbarbutton-icon
+{
+	fill-opacity: 1 !important;
+	border: 0 !important;
+	box-shadow: none !important;
+}
+
+#navigator-toolbox .toolbarbutton-1 > :-moz-any(.toolbarbutton-icon, .toolbarbutton-badge-stack)
+{
+	width: calc(2 * var(--toolbarbutton-inner-padding) + 16px) !important;
+	height: calc(2 * var(--toolbarbutton-inner-padding) + 16px) !important;
+}
+
+#navigator-toolbox > #PersonalToolbar .toolbarbutton-1 > .toolbarbutton-icon,
+#navigator-toolbox .toolbarbutton-1 > :-moz-any(.toolbarbutton-icon, .toolbarbutton-badge-stack),
+/* no ::part workaround - may have side effects */
+#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton) > .toolbarbutton-icon,
+#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton) > .toolbarbutton-icon
+{
+	padding: 6px !important;
+	background: 0 !important;
+	width: auto !important;
+	height: auto !important;
+}
+
+/*
+	hack: fix customization screen popping bug when changing ui density
+	icon size is enforced and it doesn't like it when the normal density
+	nav-bar height is less than 38px (it's 36px due to smaller back icon)
+*/
+:root:not([uidensity=compact]) #customization-content-container
+{
+	padding-top: 1px !important;
+}
+
+:root:not([uidensity=compact]) :-moz-any(#customization-palette-container, #customization-panel-container)
+{
+	margin-top: -1px !important;
+}
+
+.menu-iconic,
+.menuitem-iconic
+{
+	fill: currentColor !important;
+}
+
+/* legacy */
+#navigator-toolbox
+{
+	border-bottom: 1px solid var(--chrome-content-separator-color, var(--toolbox-border-bottom-color)) !important;
+}
+
+/* legacy */
+#navigator-toolbox::after
+{
+	display: none !important;
+}
+
+.pointerlockfswarning
+{
+	display: flex !important;
+	border-radius: 4px !important;
+	padding: 6px !important;
+	background: #333 !important;
+	border: 0 !important;
+	font-size: 14px !important;
+	box-shadow:
+		0 3px 5px -1px rgba(0,0,0,.2),
+		0 6px 10px 0 rgba(0,0,0,.14),
+		0 1px 18px 0 rgba(0,0,0,.12) !important;
+	color: hsla(0,0%,100%,.87) !important;
+	transition-timing-function: var(--ease-out), linear !important;
+	transition-property: transform, top !important;
+}
+
+#fullscreen-warning
+{
+	flex-direction: column !important;
+	min-width: 344px !important;
+}
+
+.pointerlockfswarning[hidden]
+{
+	visibility: hidden !important;
+}
+
+/* hack to keep it in same place as [ontop] */
+/* probably doesn't work for single-line */
+.pointerlockfswarning:not([hidden]):not([ontop])
+{
+	top: -32px !important;
+}
+
+.pointerlockfswarning[ontop]
+{
+	top: 28px !important;
+}
+
+.pointerlockfswarning::before
+{
+	display: none !important;
+}
+
+.pointerlockfswarning-domain-text
+{
+	margin-block: 8px 18px !important;
+	margin-inline: 10px !important;
+	font-size: unset !important;
+	font-weight: unset !important;
+	align-self: start !important;
+}
+
+.pointerlockfswarning-domain
+{
+	font-weight: unset !important;
+}
+
+#fullscreen-exit-button
+{
+	-moz-appearance: none !important;
+	border: 0 !important;
+	height: 32px !important;
+	display: flex !important;
+	align-items: center !important;
+	margin-inline: 8px 0 !important;
+	text-transform: uppercase !important;
+	font-weight: 500 !important;
+	letter-spacing: .0892857143em !important;
+	background: 0 !important;
+	position: relative !important;
+	color: var(--toolbarbutton-icon-fill-attention) !important;
+	align-self: end !important;
+	font-family: Roboto, inherit !important;
+}
+
+#fullscreen-exit-button::before
+{
+	content: "" !important;
+	display: block !important;
+	z-index: -1 !important;
+	position: absolute !important;
+	left: 0 !important;
+	right: 0 !important;
+	top: 0 !important;
+	bottom: 0 !important;
+	border-radius: 4px !important;
+	background: currentColor !important;
+	opacity: 0 !important;
+	transition: opacity .3s var(--ease-basic) !important;
+}
+
+#fullscreen-exit-button:hover::before
+{
+	opacity: .12 !important;
+}
+
+#fullscreen-exit-button:hover:active::before
+{
+	opacity: .24 !important;
+	transition-duration: .1s !important;
+}
+
+/* note: use Firefox account button instead */
+#TabsToolbar .private-browsing-indicator
+{
+	display: none !important;
+}
+
+/* high DPI adjustments */
+@media (min--moz-device-pixel-ratio: 2)
+{
+	#navigator-toolbox
+	{
+		border-bottom-width: .5px !important;
+		padding-bottom: .5px !important;
+		box-shadow: inset 0 -.5px var(--toolbar-bgcolor) !important;
+	}
+}
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/global/variables.css b/testdata/home/.cache/ffcss/materialfox/_/chrome/global/variables.css
new file mode 100755
index 0000000..b1e2445
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/global/variables.css
@@ -0,0 +1,215 @@
+/* light mode */
+:root:not([style]),
+:root[style*="--lwt-accent-color:rgb(227, 228, 230);"],
+:root[style*="--lwt-accent-color:white;"]
+{
+	/* accentcolor */
+	--lwt-accent-color: #dee1e6 !important;
+	
+	/* textcolor */
+	--lwt-text-color: #606368 !important;
+	
+	/* toolbar */
+	--toolbar-bgcolor: #fff !important;
+	
+	/* toolbar_text */
+	--toolbar-color: #606368 !important;
+	
+	/* toolbar_bottom_separator */
+	--toolbox-border-bottom-color: #b3b1b3 !important; /* legacy */
+	--chrome-content-separator-color: #b3b1b3 !important;
+	
+	/* icons */
+	--lwt-toolbarbutton-icon-fill: #606368 !important;
+	
+	/* icons_attention */
+	--lwt-toolbarbutton-icon-fill-attention: #5086ec !important;
+	
+	/* button_background_hover */
+	--lwt-toolbarbutton-hover-background: rgba(0, 0, 0, .07) !important;
+	
+	/* button_background_active */
+	--lwt-toolbarbutton-active-background: rgba(0, 0, 0, .11) !important;
+	
+	--lwt-toolbar-field-color: #202124 !important;
+	
+	/* popup_highlight */
+	--autocomplete-popup-highlight-background: #f2f2f2 !important;
+	
+	/* popup_highlight_text */
+	--autocomplete-popup-highlight-color: #202124 !important;
+}
+
+/* don't bother with sidebar in light mode */
+
+/* dark mode */
+:root[style*="--lwt-accent-color:rgb(12, 12, 13);"],
+:root[privatebrowsingmode=temporary]
+{
+	/* accentcolor */
+	--lwt-accent-color: #202124 !important;
+	
+	/* textcolor */
+	--lwt-text-color: #9ba0a5 !important;
+	
+	/* toolbar */
+	--toolbar-bgcolor: #333639 !important;
+	
+	/* toolbar_text */
+	--toolbar-color: #fff !important;
+	
+	/* toolbar_bottom_separator */
+	--toolbox-border-bottom-color: #282828 !important; /* legacy */
+	--chrome-content-separator-color: #282828 !important;
+	
+	/* icons */
+	--lwt-toolbarbutton-icon-fill: #fff !important;
+	
+	/* icons_attention */
+	--lwt-toolbarbutton-icon-fill-attention: #5086ec !important;
+	
+	/* button_background_hover */
+	--lwt-toolbarbutton-hover-background: #434649 !important;
+	
+	/* button_background_active */
+	--lwt-toolbarbutton-active-background: #4e5153 !important;
+	
+	--lwt-toolbar-field-color: #fff !important;
+	
+	/* popup */
+	--arrowpanel-background: #373737 !important;
+	
+	/* popup_text */
+	--arrowpanel-color: #fff !important;
+	--autocomplete-popup-color: #fff !important;
+	--panel-disabled-color: hsla(0, 0%, 100%, .5) !important;
+	
+	/* popup_border */
+	--arrowpanel-border-color: #555 !important;
+	--autocomplete-popup-border-color: #555 !important;
+	
+	/* popup_highlight */
+	--autocomplete-popup-highlight-background: #4c4f52 !important;
+	
+	/* popup_highlight_text */
+	--autocomplete-popup-highlight-color: #fff !important;
+}
+
+:-moz-any(#sidebar-box, #sidebar)[style*="--sidebar-background-color:rgb(56, 56, 61);"]
+{
+	/* sidebar */
+	--sidebar-background-color: #333639 !important;
+	
+	/* sidebar_text */
+	--sidebar-text-color: #f1f3ee !important;
+}
+
+:-moz-any(.sidebar-panel, body)[style*="--lwt-sidebar-background-color:rgb(56, 56, 61);"]
+{
+	/* sidebar */
+	--lwt-sidebar-background-color: #333639 !important;
+	
+	/* sidebar_text */
+	--lwt-sidebar-text-color: #f1f3ee !important;
+}
+
+:root
+{
+    --ease-in: cubic-bezier(.4, 0, 1, 1);
+	--ease-out: cubic-bezier(0, 0, .2, 1);
+	--ease-basic: linear;
+
+	--button-size: 32px; /* is this used? */
+	--icon-size: 24px; /* is this used? */
+	
+	--downloads-item-height: 48px !important;
+	
+	--toolbar-non-lwt-bgcolor: var(--toolbar-bgcolor) !important;
+	--toolbar-non-lwt-textcolor: var(--toolbar-color) !important;
+	
+	--toolbar-field-fontsize: 14px !important;
+}
+
+#titlebar,
+#tabbrowser-tabs
+{
+ 	--tab-min-height: 34px !important;
+}
+
+@supports -moz-bool-pref("materialFox.reduceTabOverflow")
+{
+	#tabbrowser-tabs
+	{
+		--tab-min-width: 32px !important;
+	}
+}
+
+:root
+{	
+	--tab-separator-opacity: .35 !important;
+	
+	--toolbar-field-background-color: hsl(200, 12%, 95%) !important;
+	--toolbar-field-hover-background-color: hsl(216, 12%, 92%) !important;
+	--toolbar-field-focus-background-color: hsl(0, 0%, 100%) !important;
+}
+
+:root:-moz-any(:-moz-lwtheme-brighttext, [privatebrowsingmode=temporary])
+{
+	--toolbar-field-background-color: #202124 !important;
+	--toolbar-field-hover-background-color: #292a2d !important;
+	--toolbar-field-focus-background-color: #202124 !important;
+}
+
+:root:not(:-moz-lwtheme):not([privatebrowsingmode=temporary])
+{
+	--toolbar-bgcolor: hsl(0, 0%, 100%) !important;
+	--toolbar-color: hsl(213, 5%, 39%) !important;
+	--toolbox-border-bottom-color: hsl(0, 0%, 70%) !important;
+}
+
+@media (-moz-mac-yosemite-theme)
+{
+	:root[extradragspace]
+	{
+		--space-above-tabbar: 8px !important;
+	}
+}
+
+/* Ubuntu */
+@media (-moz-gtk-csd-available)
+{
+	:root:not(:-moz-lwtheme):not([privatebrowsingmode=temporary])
+	{
+		--toolbar-bgcolor: -moz-dialog !important;
+		--toolbar-color: -moz-dialogtext !important;
+	}
+}
+
+:root:-moz-lwtheme-darktext,
+:root toolbar:not([brighttext]),
+.tabbrowser-tab[visuallyselected]
+{
+	--toolbarbutton-hover-background: var(--lwt-toolbarbutton-hover-background, hsla(0, 0%, 0%, .07)) !important;
+    --toolbarbutton-active-background: var(--lwt-toolbarbutton-active-background, hsla(0, 0%, 0%, .11)) !important;
+}
+
+:root:-moz-lwtheme-brighttext,
+:root toolbar[brighttext],
+.tabbrowser-tab[visuallyselected]:-moz-lwtheme-brighttext
+{
+    --toolbarbutton-hover-background: var(--lwt-toolbarbutton-hover-background, hsla(0, 0%, 100%, .1)) !important;
+    --toolbarbutton-active-background: var(--lwt-toolbarbutton-active-background, hsla(0, 0%, 100%, .17)) !important;
+}
+
+/* special case for urlbar buttons */
+:-moz-any(#urlbar, #searchbar)
+{
+	--toolbarbutton-hover-background: hsl(210, 5%, 85%) !important;
+	--toolbarbutton-active-background: hsl(210, 4%, 80%) !important;
+}
+
+:root:-moz-any([privatebrowsingmode=temporary], :-moz-lwtheme-brighttext) :-moz-any(#urlbar, #searchbar)
+{
+	--toolbarbutton-hover-background: hsl(206, 5%, 26%) !important;
+	--toolbarbutton-active-background: hsl(210, 4%, 31%) !important;
+}
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/addons.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/addons.svg
new file mode 100644
index 0000000..6369afb
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/addons.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 22 22">
+    <path fill="context-fill" fill-opacity="context-fill-opacity" transform="translate(-1 -1)" 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>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/arrow-back-1x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/arrow-back-1x.svg
new file mode 100644
index 0000000..026afad
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/arrow-back-1x.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" stroke-opacity="context-fill-opacity" d="M8,1A1,1,0,1,1,7,2,1,1,0,0,1,8,1ZM2,7A1,1,0,1,1,1,8,1,1,0,0,1,2,7ZM14,7a1,1,0,1,1-1,1A1,1,0,0,1,14,7ZM8,13a1,1,0,1,1-1,1A1,1,0,0,1,8,13Zm0.723,0.309L7.309,14.723l-6-6L2.723,7.309Zm-6-4.586L1.309,7.309l6-6L8.723,2.723Zm-0.707.293v-2h12v2h-12Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/arrow-back-2x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/arrow-back-2x.svg
new file mode 100644
index 0000000..8869d65
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/arrow-back-2x.svg
@@ -0,0 +1,8 @@
+<svg
+	version="1.1" xmlns="http://www.w3.org/2000/svg"
+	width="16" height="16" viewBox="0 0 16 16" 
+	fill="none" stroke-opacity="context-fill-opacity" stroke="context-fill" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
+>
+	<path d="M2.25,7.75h11.5"/>
+	<path d="M7.75,13.25l-5.5-5.5,5.5-5.5"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/audio-muted.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/audio-muted.svg
new file mode 100644
index 0000000..c076c20
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/audio-muted.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M11,9.143V5.5h1v4.639ZM9,7.149V6.5h1V8.146ZM6.672,4.828L8,3.5V6.153Zm-3.96-2.11,9.531,9.5-0.509.494L8,8.985V12.5l-3-3H3v-3H5l0.255-.255L2.218,3.212l0.5-.5h0Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/audio.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/audio.svg
new file mode 100644
index 0000000..5eeba1e
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/audio.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M11,10.5v-5h1v5H11Zm-2-4h1v3H9v-3Zm-6,3v-3H5l3-3v9l-3-3H3Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/bookmark.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/bookmark.svg
new file mode 100644
index 0000000..e5a7cba
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/bookmark.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M13,12.091V4.5l-0.056-.056a3,3,0,0,0-2.388-2.388L10.5,2H5A2,2,0,0,1,7,0h6a2,2,0,0,1,2,2V13ZM12,5V16L7,13.5,2,16V5A2,2,0,0,1,4,3h6A2,2,0,0,1,12,5Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/bug.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/bug.svg
new file mode 100644
index 0000000..c5aff2c
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/bug.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M15,13.47H12.32a5,5,0,0,1-8.66,0H1v-2H3c0-.16,0-.33,0-.49V10H1V8H3V7a5,5,0,0,1,0-.51H1v-2H3.68A5,5,0,0,1,5,2.94l-1.5-1.5L5,0l2,2a5,5,0,0,1,2,0l2-2,1.41,1.41L10.95,2.95a5,5,0,0,1,1.36,1.52H15v2H13A5,5,0,0,1,13,7V8h2v2H13v1c0,.17,0,.33,0,.49h2Zm-5.51-7h-3V8h3Zm0,3.49h-3v1.5h3Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/close.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/close.svg
new file mode 100644
index 0000000..e750bba
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/close.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+    <path fill="context-fill" fill-opacity="context-fill-opacity" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
+</svg>
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/copy.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/copy.svg
new file mode 100644
index 0000000..8b357cc
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/copy.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M13.5,16h-8A1.5,1.5,0,0,1,4,14.5V4.5A1.5,1.5,0,0,1,5.5,3h8A1.5,1.5,0,0,1,15,4.5v10A1.5,1.5,0,0,1,13.5,16Zm0-11.5h-8v10h8Zm-11-3v10H1V1.5A1.5,1.5,0,0,1,2.5,0H11V1.5Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/cut.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/cut.svg
new file mode 100644
index 0000000..282e6f0
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/cut.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M6.11,4.51A3.12,3.12,0,0,0,6.4,3.2,3.2,3.2,0,1,0,3.2,6.4a3.12,3.12,0,0,0,1.31-.29L6.4,8,4.51,9.89A3.12,3.12,0,0,0,3.2,9.6a3.2,3.2,0,1,0,3.2,3.2,3.12,3.12,0,0,0-.29-1.31L8,9.6,13.4,15H16v-.6Zm-3,.24A1.62,1.62,0,1,1,4.75,3.13,1.62,1.62,0,0,1,3.13,4.75Zm0,9.74a1.62,1.62,0,1,1,1.62-1.62A1.62,1.62,0,0,1,3.13,14.49ZM8,8.49A.49.49,0,1,1,8.49,8,.49.49,0,0,1,8,8.49ZM13.41,1,8.8,5.6l1.6,1.6L16,1.6V1Z"/>
+</svg>
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/developer.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/developer.svg
new file mode 100644
index 0000000..f99dbed
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/developer.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 20 20">
+    <path fill="context-fill" fill-opacity="context-fill-opacity" transform="translate(-2 -2)" d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/email.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/email.svg
new file mode 100644
index 0000000..6c85cf3
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/email.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.5,14.5H1.5A1.5,1.5,0,0,1,0,13V3A1.5,1.5,0,0,1,1.5,1.5h13A1.5,1.5,0,0,1,16,3V13A1.5,1.5,0,0,1,14.5,14.5Zm0-11.25L8,7.25l-6.5-4V5L8,9l6.5-4Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/folder.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/folder.svg
new file mode 100644
index 0000000..14f5f2a
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/folder.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
+    <path fill="context-fill" fill-opacity="context-fill-opacity" d="M1.5,2H6L8,4h6.5A1.5,1.5,0,0,1,16,5.5v8A1.5,1.5,0,0,1,14.5,15H1.5A1.5,1.5,0,0,1,0,13.5V3.5A1.5,1.5,0,0,1,1.5,2Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/forget.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/forget.svg
new file mode 100644
index 0000000..69f1a84
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/forget.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+    <path fill="context-fill" fill-opacity="context-fill-opacity" d="M13.4,9.92A4.915,4.915,0,0,0,13.8,8,5.058,5.058,0,0,0,8.735,2.946a4.958,4.958,0,0,0-1.913.4L5.741,2.261A6.357,6.357,0,0,1,8.735,1.5,6.508,6.508,0,0,1,15.251,8a6.309,6.309,0,0,1-.77,3Zm2.476,4.1-0.884.884L12.98,12.893a6.469,6.469,0,0,1-8.851-.3l1.028-1.025a5.018,5.018,0,0,0,6.8.3l-7.1-7.1A5,5,0,0,0,3.666,8H5.838L2.913,10.91l-0.051-.1L0.046,8H2.218A6.433,6.433,0,0,1,3.84,3.752L1.214,1.126,2.1,0.242Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/globe.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/globe.svg
new file mode 100644
index 0000000..7700ff4
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/globe.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
+    <path fill="context-fill" fill-opacity="context-fill-opacity" d="M12.72,12.31A1.61,1.61,0,0,0,11.2,11.2h-.71V8.8a.8.8,0,0,0-.8-.8H5V6.49H6.21a.8.8,0,0,0,.8-.8V4H8.8a1.6,1.6,0,0,0,1.6-1.6V2.07a6.39,6.39,0,0,1,2.32,10.24m-5.52,2A6.39,6.39,0,0,1,1.6,8a6,6,0,0,1,.17-1.43L5.6,10.4v.8a1.6,1.6,0,0,0,1.6,1.6M8,0a8,8,0,1,0,8,8A8,8,0,0,0,8,0Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/history.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/history.svg
new file mode 100644
index 0000000..3387c94
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/history.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 22 22">
+    <path fill="context-fill" fill-opacity="context-fill-opacity" transform="translate(-1 -1)" d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/home-1x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/home-1x.svg
new file mode 100644
index 0000000..7db9cc9
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/home-1x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="context-fill" stroke-opacity="context-fill-opacity" d="M15,8L13,6v9H3V6L1,8,8,1l7,7"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/home-2x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/home-2x.svg
new file mode 100644
index 0000000..8a10654
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/home-2x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.5,8.25a0.75,0.75,0,0,1-1.28.53h0L13,8.57V13.5a1,1,0,0,1-1,1H4a1,1,0,0,1-1-1V8.57l-0.219.211h0A0.75,0.75,0,1,1,1.72,7.72h0l5.75-5.844h0a0.749,0.749,0,0,1,1.058,0h0l5.75,5.844h0A0.747,0.747,0,0,1,14.5,8.25ZM4.5,13h7V7.124L8,3.75,4.5,7.124V13Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/hyperlink.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/hyperlink.svg
new file mode 100644
index 0000000..f38bfde
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/hyperlink.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<g fill="none" stroke="context-fill" stroke-opacity="context-fill-opacity" stroke-miterlimit="10" stroke-width="2">
+		<path d="M9,11h3a3,3,0,0,0,0-6H9"/>
+		<path d="M7,11H4A3,3,0,0,1,4,5H7"/>
+		<line x1="5" y1="8" x2="11" y2="8"/>
+	</g>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/icons.css b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/icons.css
new file mode 100755
index 0000000..7ff71e3
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/icons.css
@@ -0,0 +1,326 @@
+.toolbarbutton-1:-moz-any(#back-button, #forward-button),
+#context-back,
+#context-forward,
+.subviewbutton-back,
+.identity-popup-expander
+{
+	list-style-image: url(arrow-back-1x.svg) !important;
+}
+
+#PanelUI-menu-button
+{
+	list-style-image: url(menu.svg) !important;
+}
+
+#PanelUI-menu-button[badge-status|="update"]
+{
+	list-style-image: url(menu-update.svg) !important;
+}
+
+#reload-button,
+#context-reload
+{
+	list-style-image: url(refresh-1x.svg) !important;
+}
+
+#stop-button
+{
+	list-style-image: url(stop-1x.svg) !important;
+}
+
+.toolbarbutton-1#forward-button
+{
+	transform: scaleX(-1) !important;
+}
+
+#home-button
+{
+	list-style-image: url(home-1x.svg) !important;
+}
+
+#star-button,
+#context-bookmarkpage,
+#pageAction-panel-bookmark,
+#panelMenuBookmarkThisPage
+{
+	list-style-image: url(star-1x.svg) !important;
+	fill-opacity: 0 !important;
+}
+
+#star-button[starred],
+#tracking-protection-icon-box[active]
+{
+	fill: var(--toolbarbutton-icon-fill-attention) !important;
+}
+
+#star-button[starred],
+#context-bookmarkpage[starred],
+#panelMenuBookmarkThisPage
+{
+	fill-opacity: 1 !important;
+}
+
+#bookmarks-menu-button,
+#appMenu-library-bookmarks-button,
+#panelMenu_toggleBookmarksMenu
+{
+	list-style-image: url(bookmark.svg) !important;
+}
+
+#library-button,
+#appMenu-library-button
+{
+	list-style-image: url(library-1x.svg) !important;
+}
+
+#preferences-button,
+#appMenu-preferences-button,
+.identity-popup-preferences-button,
+#urlbar-anon-search-settings-compact .button-icon
+{
+	list-style-image: url(settings.svg) !important;
+}
+
+#history-panelmenu,
+#appMenu-library-history-button
+{
+	list-style-image: url(history.svg) !important;
+}
+
+#appMenu-logins-button,
+richlistitem[originaltype="loginWithOrigin"] .ac-site-icon,
+richlistitem[originaltype="loginsFooter"] .ac-site-icon
+{
+	list-style-image: url(key.svg) !important;
+}
+
+.subviewbutton[type="highlight-history"]::after
+{
+	content: url(history.svg) !important;
+}
+
+#panic-button
+{
+	list-style-image: url(forget.svg) !important;
+}
+
+#developer-button
+{
+	list-style-image: url(developer.svg) !important;
+}
+
+#add-ons-button,
+#appMenu-addons-button
+{
+	list-style-image: url(addons.svg) !important;
+}
+
+#email-link-button,
+#pageAction-panel-emailLink
+{
+	list-style-image: url(email.svg) !important;
+}
+
+#pageAction-panel-shareURL
+{
+	list-style-image: url(share.svg) !important;
+}
+
+#pageAction-panel-copyURL
+{
+	list-style-image: url(hyperlink.svg) !important;
+}
+
+#pageAction-panel-webcompat-reporter_mozilla_org
+{
+	list-style-image: url(bug.svg) !important;
+}
+
+#pageAction-panel-webcompat-reporter_mozilla_org > .toolbarbutton-icon
+{
+	list-style-image: inherit !important;
+}
+
+#screenshots_mozilla_org-menuitem-_create-screenshot .menu-iconic-left
+{
+	background-image: url(screenshot.svg) !important;
+	background-position: 4px center !important;
+	background-repeat: no-repeat !important;
+}
+
+#screenshots_mozilla_org-menuitem-_create-screenshot .menu-iconic-icon
+{
+	visibility: hidden !important;
+}
+
+#tabs-newtab-button,
+#new-tab-button
+{
+	list-style-image: url(new-tab.svg) !important;
+}
+
+#fxa-avatar-image
+{
+	/* icon should be 20px, but we don't want to affect button size */
+	transform: scale(1.25) !important;
+	border-radius: 50% !important;
+}
+
+:root:not([fxastatus=signedin]) #fxa-avatar-image
+{
+	list-style-image: url(profile-inactive.svg) !important;
+}
+
+:root[fxastatus=signedin]:not([style*="--avatar-image-url"]) #fxa-avatar-image
+{
+	list-style-image: url(profile.svg) !important;
+}
+
+:root[privatebrowsingmode] #fxa-avatar-image
+{
+	list-style-image: url(incognito.svg) !important;
+}
+
+#tracking-protection-icon
+{
+	list-style-image: url(tracking-protection.svg) !important;
+}
+
+#pageActionButton
+{
+	list-style-image: url(menu.svg) !important;
+}
+
+#urlbar[pageproxystate="invalid"] #identity-icon,
+.searchbar-search-icon,
+#PopupAutoCompleteRichResult .ac-type-icon[type="keyword"],
+#PopupAutoCompleteRichResult .ac-site-icon[type="searchengine"],
+#appMenu-find-button,
+#panelMenu_searchBookmarks
+{
+	list-style-image: url(search.svg) !important;
+}
+
+#appMenu-cut-button
+{
+	list-style-image: url(cut.svg) !important;
+}
+
+#appMenu-copy-button
+{
+	list-style-image: url(copy.svg) !important;
+}
+
+#appMenu-paste-button
+{
+	list-style-image: url(paste.svg) !important;
+}
+
+#appMenu-fxa-label,
+#PanelUI-fxa-menu-syncnow-button,
+#appMenu-fxa-icon,
+#PanelUI-remotetabs-syncnow
+{
+	list-style-image: url(sync.svg) !important;
+}
+
+.tab-icon-image:not([src])
+{
+    list-style-image: url(globe.svg) !important;
+}
+
+.tab-icon-image[src="chrome://browser/skin/settings.svg"]
+{
+	display: none !important;
+}
+
+.tab-icon-image[src="chrome://browser/skin/settings.svg"] ~ .tab-icon-overlay
+{
+	display: -moz-box !important;
+	margin: 0 !important;
+	list-style-image: url(settings.svg) !important;
+	-moz-context-properties: fill, fill-opacity !important;
+	fill: currentColor !important;
+}
+
+.tab-icon-overlay[soundplaying],
+.tab-icon-sound
+{
+    list-style-image: url(audio.svg) !important;
+}
+
+.tab-icon-overlay[muted],
+.tab-icon-sound[muted]
+{
+    list-style-image: url(audio-muted.svg) !important;
+}
+
+.tab-icon-overlay[activemedia-blocked],
+.tab-icon-sound[activemedia-blocked]
+{
+	list-style-image: url(media-blocked.svg) !important;
+}
+
+.downloadIconShow .button-icon
+{
+	list-style-image: url(folder.svg) !important;
+}
+
+.panel-banner-item[notificationid^="update"]
+{
+	list-style-image: url(menu-update.svg) !important;
+	-moz-context-properties: fill, fill-opacity !important;
+	fill: currentColor !important;
+}
+
+/* high DPI adjustments */
+@media (min--moz-device-pixel-ratio: 2)
+{
+	.toolbarbutton-1:-moz-any(#back-button, #forward-button),
+	#context-back,
+	#context-forward,
+	.subviewbutton-back,
+	.identity-popup-expander
+	{
+		list-style-image: url(arrow-back-2x.svg) !important;
+	}
+	
+	#PanelUI-menu-button:not([badge-status|="update"]) .toolbarbutton-icon
+	{
+		transform: translate(-.5px, -.5px) !important;
+	}
+	
+	#stop-button
+	{
+		list-style-image: url(stop-2x.svg) !important;
+	}
+	
+	#reload-button,
+	#context-reload
+	{
+		list-style-image: url(refresh-2x.svg) !important;
+	}
+	
+	#home-button
+	{
+		list-style-image: url(home-2x.svg) !important;
+	}
+	
+	#star-button,
+	#context-bookmarkpage,
+	#pageAction-panel-bookmark,
+	#panelMenuBookmarkThisPage
+	{
+		list-style-image: url(star-2x.svg) !important;
+	}
+	
+	#bookmarks-menu-button .toolbarbutton-icon
+	{
+		transform: translateX(.5px) !important;
+	}
+	
+	#library-button
+	{
+		list-style-image: url(library-2x.svg) !important;
+	}
+}
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/incognito.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/incognito.svg
new file mode 100644
index 0000000..c8638d4
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/incognito.svg
@@ -0,0 +1,9 @@
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
+	<defs>
+		<mask id="cutout">
+			<rect fill="#fff" width="100%" height="100%"/>
+			<path transform="translate(2 2)" fill="#000" d="M1,8L4.25,6.25h7.5L15,8H1ZM4.75,4.75l1-3L8,2.75l2.25-1,1,3H4.75ZM4.875,9.5a2.367,2.367,0,0,1,2.249,1.645A3.755,3.755,0,0,1,8,11a3.755,3.755,0,0,1,.876.145,2.374,2.374,0,1,1-.1,1.04c-0.286-.045-0.622-0.1-0.78-0.141C8.106,12.018,8.131,12,8,12s-0.106.018,0,.044c-0.158.039-.493,0.1-0.78,0.141A2.37,2.37,0,1,1,4.875,9.5Zm6.25,3.75A1.375,1.375,0,1,0,9.75,11.875,1.375,1.375,0,0,0,11.125,13.25Zm-6.25,0A1.375,1.375,0,1,0,3.5,11.875,1.375,1.375,0,0,0,4.875,13.25ZM8,12.044H8Z"/>
+		</mask>
+	</defs>
+	<circle mask="url(#cutout)" fill="context-fill" cx="10" cy="10" r="10"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/key.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/key.svg
new file mode 100644
index 0000000..d03c814
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/key.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M8.47,6.48a4.36,4.36,0,1,0,0,2.91l3,0v3h3v-3H16v-3ZM4.36,9.39A1.45,1.45,0,1,1,5.82,7.93,1.46,1.46,0,0,1,4.36,9.39Z"/>
+</svg>
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/library-1x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/library-1x.svg
new file mode 100644
index 0000000..4edae9c
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/library-1x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.5,12h-9A1.5,1.5,0,0,1,4,10.5v-9A1.5,1.5,0,0,1,5.5,0h9A1.5,1.5,0,0,1,16,1.5v9A1.5,1.5,0,0,1,14.5,12ZM14,2H10V8l2-1,2,1V2ZM13,16H1.5A1.5,1.5,0,0,1,0,14.5V3H2V14H13v2Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/library-2x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/library-2x.svg
new file mode 100644
index 0000000..c3e0ee9
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/library-2x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14,12.5H5A1.5,1.5,0,0,1,3.5,11V2A1.5,1.5,0,0,1,5,.5h9A1.5,1.5,0,0,1,15.5,2v9A1.5,1.5,0,0,1,14,12.5ZM14,2H10.25V8l1.871-1L14,8V2ZM12.5,15.5H2A1.5,1.5,0,0,1,.5,14V3.5H2V14H12.5v1.5Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/media-blocked.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/media-blocked.svg
new file mode 100644
index 0000000..5065c9e
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/media-blocked.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M5.886,4.045L12,7.484,10.363,8.508ZM2.711,2.718l9.531,9.5-0.509.494L8.616,9.6,4,12.484V4.991L2.218,3.212l0.5-.5h0Zm9.537,9.495h0Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/menu-update.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/menu-update.svg
new file mode 100644
index 0000000..07cab79
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/menu-update.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill-rule="evenodd" fill="context-fill" stroke-opacity="context-fill-opacity" d="M8,0A8,8,0,1,1,0,8,8,8,0,0,1,8,0ZM6,9h4v3H6V9ZM8,4l4,5H4Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/menu.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/menu.svg
new file mode 100644
index 0000000..23e93b3
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/menu.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+    <path fill="context-fill" fill-opacity="context-fill-opacity" d="M8.5,2A1.5,1.5,0,1,1,7,3.5,1.5,1.5,0,0,1,8.5,2Zm0,5A1.5,1.5,0,1,1,7,8.5,1.5,1.5,0,0,1,8.5,7Zm0,5A1.5,1.5,0,1,1,7,13.5,1.5,1.5,0,0,1,8.5,12Z"/>
+</svg>
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/new-tab.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/new-tab.svg
new file mode 100644
index 0000000..b071566
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/new-tab.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M8,2A1,1,0,0,1,9,3V13a1,1,0,0,1-2,0V3A1,1,0,0,1,8,2ZM3,7H13a1,1,0,0,1,0,2H3A1,1,0,0,1,3,7Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/paste.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/paste.svg
new file mode 100644
index 0000000..440efae
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/paste.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M13,1.5H10.29a2.5,2.5,0,0,0-4.58,0H3A1.5,1.5,0,0,0,1.5,3V14.5A1.5,1.5,0,0,0,3,16H13a1.5,1.5,0,0,0,1.5-1.5V3A1.5,1.5,0,0,0,13,1.5Zm-5,0a1,1,0,1,1-1,1A1,1,0,0,1,8,1.5Zm5,13H3V3H4.5V5h7V3H13Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/profile-inactive.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/profile-inactive.svg
new file mode 100644
index 0000000..054fc42
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/profile-inactive.svg
@@ -0,0 +1,4 @@
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
+    <rect fill="#e1e1e1" width="100%" height="100%"/>
+    <path fill="#a3a3a3" d="M16,13.833a7.113,7.113,0,0,1-12,0V13.75c0-1.5,3.5-3,6-3s6,1.5,6,3v0.082ZM13,6a3,3,0,1,1-3-3A3,3,0,0,1,13,6Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/profile.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/profile.svg
new file mode 100644
index 0000000..df12aa4
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/profile.svg
@@ -0,0 +1,9 @@
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
+	<defs>
+        <mask id="cutout">
+            <rect fill="#fff" width="100%" height="100%"/>
+            <path fill="#000" d="M16,13.833a7.113,7.113,0,0,1-12,0V13.75c0-1.5,3.5-3,6-3s6,1.5,6,3v0.082ZM13,6a3,3,0,1,1-3-3A3,3,0,0,1,13,6Z"/>
+        </mask>
+    </defs>
+    <rect mask="url(#cutout)" fill="context-fill" width="100%" height="100%"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/refresh-1x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/refresh-1x.svg
new file mode 100644
index 0000000..428bbd7
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/refresh-1x.svg
@@ -0,0 +1,16 @@
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	width="16" height="16" viewBox="0 0 16 16"
+	fill-rule="evenodd" 
+>
+	<path
+		fill="none"
+		stroke-opacity="context-fill-opacity" stroke="context-fill" stroke-linecap="round" stroke-width="2"
+		d="M13.719,10.25A5.881,5.881,0,0,1,8,14,6,6,0,1,1,12.243,3.757"
+	/>
+	<path
+		fill-opacity="context-fill-opacity"
+		fill="context-fill"
+		d="M15,1V7H9Z"
+	/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/refresh-2x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/refresh-2x.svg
new file mode 100644
index 0000000..a39ce6f
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/refresh-2x.svg
@@ -0,0 +1,16 @@
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	width="16" height="16" viewBox="0 0 16 16"
+	fill-rule="evenodd" 
+>
+	<path
+		fill="none"
+		stroke-opacity="context-fill-opacity" stroke="context-fill" stroke-linecap="round" stroke-width="1.5"
+		d="M13.292,10.25a5.752,5.752,0,1,1-.261-5.031"
+	/>
+	<path
+		fill-opacity="context-fill-opacity"
+		fill="context-fill"
+		d="M15,1.5V7H9.5Z"
+	/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/screenshot.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/screenshot.svg
new file mode 100644
index 0000000..d28d202
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/screenshot.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.5,15H1.5A1.5,1.5,0,0,1,0,13.5V3.5A1.5,1.5,0,0,1,1.5,2h13A1.5,1.5,0,0,1,16,3.5v10A1.5,1.5,0,0,1,14.5,15Zm0-11.5H1.5v10h13ZM9,10.5h1.5V12H9Zm0-3A1.5,1.5,0,0,1,10.5,9H9Zm-3,0H7.5V9H6Zm-3,0H4.5V9H3Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/search.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/search.svg
new file mode 100644
index 0000000..677a77b
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/search.svg
@@ -0,0 +1,10 @@
+<svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="16" height="16" viewBox="0 0 16 16"
+>
+    <path
+        fill-opacity="context-fill-opacity"
+        fill="context-fill"
+        d="M10,9.5l0.75,0.25L14,13l-1,1L9.75,10.75,9.5,10,9.408,9.908a4.549,4.549,0,1,1,.5-0.5ZM6.5,3.25A3.25,3.25,0,1,0,9.75,6.5,3.25,3.25,0,0,0,6.5,3.25Z"
+    />
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/settings.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/settings.svg
new file mode 100644
index 0000000..6da40ed
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/settings.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+    <path fill="context-fill" fill-opacity="context-fill-opacity" transform="translate(-2 -2)" d="M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/share.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/share.svg
new file mode 100644
index 0000000..1ca669e
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/share.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 20 20">
+    <path fill="context-fill" fill-opacity="context-fill-opacity" transform="translate(-2 -2)" d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/star-1x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/star-1x.svg
new file mode 100644
index 0000000..5602b87
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/star-1x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">	
+	<path stroke-width="1.5" fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" stroke="context-fill" d="M6,5.547L7.5,1.688,9,5.547l4.25,0.531L10.094,8.75,11,13,7.5,10.75,4,13,4.906,8.75,1.75,6.094Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/star-2x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/star-2x.svg
new file mode 100644
index 0000000..ecf9c8b
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/star-2x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">	
+	<path stroke-width="1.5" fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" stroke="context-fill" d="M6,5.547L7.5,1.688,9,5.547l4.25,0.531L10.094,8.75,11,13,7.5,10.75,4,13,4.906,8.75,1.75,6.094Z" transform="translate(.5 .5)"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/stop-1x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/stop-1x.svg
new file mode 100644
index 0000000..959a4b3
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/stop-1x.svg
@@ -0,0 +1,8 @@
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	width="16" height="16" viewBox="0 0 16 16"
+	fill="none" stroke="context-fill" stroke-opacity="context-fill-opacity" stroke-linecap="round" stroke-width="2"
+>
+  <path d="M3,13L13,3"/>
+  <path d="M13,13L3,3"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/stop-2x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/stop-2x.svg
new file mode 100644
index 0000000..272a32f
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/stop-2x.svg
@@ -0,0 +1,8 @@
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	width="16" height="16" viewBox="0 0 16 16"
+	fill="none" stroke="context-fill" stroke-opacity="context-fill-opacity" stroke-linecap="round" stroke-width="1.5"
+>
+  <path d="M3,13L13,3"/>
+  <path d="M13,13L3,3"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/sync.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/sync.svg
new file mode 100644
index 0000000..09b6829
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/sync.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M13.66,2.34,15.78.22H10.12V5.88l2.12-2.12a6,6,0,0,1,0,8.49,5.87,5.87,0,0,1-2.47,1.48v2.06A8,8,0,0,0,13.66,2.34Zm-9.9,9.9a6,6,0,0,1,0-8.49A5.87,5.87,0,0,1,6.23,2.27V.21A8,8,0,0,0,2.34,13.66L.22,15.78H5.88V10.12Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/tracking-protection.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/tracking-protection.svg
new file mode 100644
index 0000000..61bc438
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/icons/tracking-protection.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" transform="scale(.666667)" d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/>
+</svg>
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-1x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-1x.svg
new file mode 100644
index 0000000..f8568ac
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-1x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M11,13H5a1,1,0,0,1-1-1V7A1,1,0,0,1,5,6V5a3,3,0,0,1,6,0V6a1,1,0,0,1,1,1v5A1,1,0,0,1,11,13ZM8,3A2,2,0,0,0,6,5V6h4V5A2,2,0,0,0,8,3Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-2x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-2x.svg
new file mode 100644
index 0000000..cfabbde
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-2x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M11,13.5H5a1,1,0,0,1-1-1v-5a1,1,0,0,1,1-1H5.5v-1a2.5,2.5,0,0,1,5,0v1H11a1,1,0,0,1,1,1v5A1,1,0,0,1,11,13.5ZM8,4A1.5,1.5,0,0,0,6.5,5.5v1h3v-1A1.5,1.5,0,0,0,8,4Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-warning-1x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-warning-1x.svg
new file mode 100644
index 0000000..94e73cd
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-warning-1x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" d="M8,1l8,13H0ZM7,6H9V9H7V6Zm0,4H9v2H7V10Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-warning-2x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-warning-2x.svg
new file mode 100644
index 0000000..ec5553b
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/connection-warning-2x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" d="M7.75,2.5L14,13.5H1.5ZM7,6.5H8.5v3H7v-3Zm0,4H8.5V12H7V10.5Z"/>
+</svg>
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/identity-icons-brand.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/identity-icons-brand.svg
new file mode 100644
index 0000000..4870645
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/identity-icons-brand.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M31.4 14.627c-.044-.289-.088-.46-.088-.46s-.113.131-.3.379a10.8 10.8 0 0 0-.375-2.04 13.883 13.883 0 0 0-.94-2.418 10.078 10.078 0 0 0-.858-1.473q-.177-.264-.36-.512c-.571-.934-1.227-1.5-1.986-2.583a7.826 7.826 0 0 1-.993-2.692 10.88 10.88 0 0 0-.477 1.761c-.779-.786-1.458-1.345-1.866-1.726C21.133.978 21.367 0 21.367 0s-3.773 4.209-2.137 8.6a8.43 8.43 0 0 0 2.81 3.744c1.581 1.3 3.283 2.33 4.18 4.952a8.4 8.4 0 0 0-3.154-3.327 7.593 7.593 0 0 1 .6 3.006 7.145 7.145 0 0 1-8.736 6.96 6.576 6.576 0 0 1-1.77-.6 7.2 7.2 0 0 1-2.121-1.96l-.01-.017.126.046a6.517 6.517 0 0 0 .9.242 5.644 5.644 0 0 0 3.594-.424c1.129-.626 1.813-1.091 2.367-.908h.01c.542.172.969-.353.581-.9a2.949 2.949 0 0 0-2.846-1.114c-1.131.165-2.167.968-3.648.19a3.13 3.13 0 0 1-.278-.163c-.1-.058.317.088.221.022a7.351 7.351 0 0 1-.931-.555c-.022-.018.224.07.2.052a3.591 3.591 0 0 1-.971-.982 1.746 1.746 0 0 1-.066-1.559 1.376 1.376 0 0 1 .6-.566c.192.095.31.166.31.166s-.087-.16-.135-.244c.017-.006.032 0 .049-.011.168.073.539.261.734.376a1.018 1.018 0 0 1 .335.3s.067-.033.017-.173a.907.907 0 0 0-.347-.425h.016A2.952 2.952 0 0 1 12.3 15a2.085 2.085 0 0 0 .171-.906 1.181 1.181 0 0 0-.069-.5c-.054-.1.03-.14.123-.035a.977.977 0 0 0-.079-.239v-.008s.053-.069.078-.095a1.434 1.434 0 0 1 .216-.176 10 10 0 0 1 1.469-.749c.416-.181.759-.32.83-.36a2.287 2.287 0 0 0 .294-.226 1.973 1.973 0 0 0 .661-1.143 1.593 1.593 0 0 0 .017-.178v-.105c-.06-.226-.449-.395-2.483-.586a1.778 1.778 0 0 1-1.454-1.364v.009c-.029.075-.055.15-.081.225.026-.075.052-.151.081-.225v-.016a5.153 5.153 0 0 1 1.992-2.474c.052-.043-.208.011-.156-.032a5.18 5.18 0 0 1 .532-.225c.091-.039-.391-.222-.818-.178a2.2 2.2 0 0 0-.758.178c.1-.086.4-.2.33-.2a4.877 4.877 0 0 0-1.547.585.315.315 0 0 1 .03-.14 2.407 2.407 0 0 0-.966.746 1.286 1.286 0 0 0 .01-.174 2.883 2.883 0 0 0-.475.446l-.009.007a6.3 6.3 0 0 0-3.527-.3l-.01-.009h.012a2.953 2.953 0 0 1-.627-.7l-.008-.012-.014-.021a5.48 5.48 0 0 1-.237-.388 5.527 5.527 0 0 1-.176-.339c0-.008-.009-.011-.013-.012-.024 0-.041.111-.061.082v-.006a4.322 4.322 0 0 1-.309-1.697l-.016.008a1.89 1.89 0 0 0-.716.937c-.062.137-.1.213-.14.288v-.035c.009-.069.039-.212.032-.2s-.013.019-.02.029a1.741 1.741 0 0 0-.252.373 2.366 2.366 0 0 0-.15.383c-.006.021 0-.018 0-.064s.009-.128 0-.111l-.022.043a9.526 9.526 0 0 0-.806 3.044 3.038 3.038 0 0 0-.023.54v.016a6.641 6.641 0 0 0-.819 1.1 15.65 15.65 0 0 0-1.731 4.246 10.379 10.379 0 0 1 .928-1.626 15.041 15.041 0 0 0-1.049 5.514 14.264 14.264 0 0 1 .451-1.634 13.848 13.848 0 0 0 1.25 6.489 15.08 15.08 0 0 0 5.968 6.768 13.057 13.057 0 0 0 3.478 1.667c.163.059.327.117.5.173-.053-.021-.1-.044-.153-.067a15.752 15.752 0 0 0 4.506.659c5.41 0 7.2-2.06 7.36-2.266a2.739 2.739 0 0 0 .639-.858q.156-.064.316-.137l.067-.03.122-.057a11.349 11.349 0 0 0 2.284-1.43 5.511 5.511 0 0 0 2.129-3.11 1.944 1.944 0 0 0 .029-1.433q.083-.132.171-.28a12.743 12.743 0 0 0 1.913-6.2v-.184a7.76 7.76 0 0 0-.115-1.388z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/info-1x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/info-1x.svg
new file mode 100644
index 0000000..005445a
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/info-1x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" d="M8,1A7,7,0,1,1,1,8,7,7,0,0,1,8,1ZM8,3A5,5,0,1,1,3,8,5,5,0,0,1,8,3ZM7,4H9V6H7V4ZM7,7H9v4H7V7Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/info-2x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/info-2x.svg
new file mode 100644
index 0000000..39e87c3
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/info-2x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" d="M7.75,1A6.75,6.75,0,1,1,1,7.75,6.75,6.75,0,0,1,7.75,1Zm0,1.5A5.25,5.25,0,1,1,2.5,7.75,5.25,5.25,0,0,1,7.75,2.5ZM7,4.5H8.5V6H7V4.5ZM7,7H8.5v4H7V7Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/naughty-shield-1x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/naughty-shield-1x.svg
new file mode 100644
index 0000000..59327d5
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/naughty-shield-1x.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M12.7,7h-0.92A28.263,28.263,0,0,0,12,3.5l-4.5-2V7.153A0.986,0.986,0,0,0,7,8v5.794A7.942,7.942,0,0,1,3.554,11C1.994,8.647,2,2.5,2,2.5L7.531,0,13,2.5A34.541,34.541,0,0,1,12.7,7Z"/>
+	<path fill="#cc4f40" fill-opacity="context-fill-opacity" d="M14,15H9a1,1,0,0,1-1-1V9A1,1,0,0,1,9,8h5a1,1,0,0,1,1,1v5A1,1,0,0,1,14,15Zm-1.139-2.846-0.646-.646,0.646-.646-0.008-.008A0.5,0.5,0,0,0,12.5,10a0.491,0.491,0,0,0-.386.194l-0.606.606-0.646-.646,0,0a0.5,0.5,0,1,0-.7.7l0,0L10.8,11.508l-0.606.606A0.491,0.491,0,0,0,10,12.5a0.5,0.5,0,0,0,.854.354l0.008,0.008,0.646-.646,0.646,0.646,0,0a0.5,0.5,0,0,0,.7-0.7Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/naughty-shield-2x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/naughty-shield-2x.svg
new file mode 100644
index 0000000..549aad2
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/naughty-shield-2x.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M13.152,7.5H12.22a25.187,25.187,0,0,0,.28-3.75L8.025,2,8.012,7.645A0.987,0.987,0,0,0,7.5,8.5v5.309A8,8,0,0,1,4,11C2.431,8.647,2.5,3,2.5,3L8,1l5.5,2A29.211,29.211,0,0,1,13.152,7.5Z"/>
+	<path fill="#cc4f40" fill-opacity="context-fill-opacity" d="M14.5,15.5h-5a1,1,0,0,1-1-1v-5a1,1,0,0,1,1-1h5a1,1,0,0,1,1,1v5A1,1,0,0,1,14.5,15.5Zm-1.139-2.846-0.646-.646,0.646-.646-0.008-.008A0.5,0.5,0,0,0,13,10.5a0.491,0.491,0,0,0-.386.194l-0.606.606-0.646-.646,0,0a0.5,0.5,0,1,0-.7.7l0,0L11.3,12.008l-0.606.606A0.491,0.491,0,0,0,10.5,13a0.5,0.5,0,0,0,.854.354l0.008,0.008,0.646-.646,0.646,0.646,0,0a0.5,0.5,0,0,0,.7-0.7Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/navbar.css b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/navbar.css
new file mode 100644
index 0000000..5944e9a
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/navbar/navbar.css
@@ -0,0 +1,44 @@
+#nav-bar
+{
+	min-height: 36px !important;
+	box-shadow: none !important;
+	padding-inline: 5px 6px !important;
+	background-color: var(--toolbar-bgcolor) !important;
+}
+
+#reload-button[disabled]:not(:-moz-window-inactive) > .toolbarbutton-icon
+{
+	opacity: 1 !important;
+}
+
+/* Windows */
+#PanelUI-button
+{
+	border: none !important;
+	margin: 0 !important;
+	padding: 0 !important;
+}
+
+#PanelUI-menu-button[badge-status|="update"] .toolbarbutton-badge
+{
+	display: none !important;
+}
+
+:root[privatebrowsingmode] #fxa-toolbar-menu-button
+{
+	pointer-events: none !important;
+}
+
+/* show "Private" label for en languages only */
+:root[titlemodifier="(Private Browsing)"] #fxa-toolbar-menu-button::before
+{
+	content: "Private" !important;
+	display: -moz-box !important;
+	margin-inline-start: 12px !important;
+	margin-inline-end: 6px !important;
+}
+
+#nav-bar-customization-target
+{
+	overflow: visible !important;
+}
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/personalbar/personalbar.css b/testdata/home/.cache/ffcss/materialfox/_/chrome/personalbar/personalbar.css
new file mode 100755
index 0000000..905916f
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/personalbar/personalbar.css
@@ -0,0 +1,4 @@
+#navigator-toolbox toolbarbutton.bookmark-item:not(.subviewbutton)
+{
+	padding: 6px !important;
+}
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/popup/checkmark-18dp.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/popup/checkmark-18dp.svg
new file mode 100644
index 0000000..99df627
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/popup/checkmark-18dp.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
+	<polyline points="2 10.5 6 14.5 16.5 4" fill="none" stroke="context-fill" stroke-opacity="context-fill-opacity" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/popup/menu-right-1x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/popup/menu-right-1x.svg
new file mode 100644
index 0000000..defd15e
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/popup/menu-right-1x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M5,2l6,6L5,14V2Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/popup/menu-right-2x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/popup/menu-right-2x.svg
new file mode 100644
index 0000000..7edab69
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/popup/menu-right-2x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" d="M5.5,2.5l5.25,5.25L5.5,13V2.5Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/popup/popup.css b/testdata/home/.cache/ffcss/materialfox/_/chrome/popup/popup.css
new file mode 100755
index 0000000..ab00a51
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/popup/popup.css
@@ -0,0 +1,39 @@
+menupopup
+{
+	appearance: none !important;
+	background-color: var(--arrowpanel-background) !important;
+	color: var(--arrowpanel-color) !important;
+	-moz-context-properties: fill, fill-opacity !important;
+	fill: currentColor !important;
+}
+
+menuitem,
+menu > menu,
+menupopup > menu
+{
+	appearance: none !important;
+}
+
+menuitem[disabled],
+menu[disabled]
+{
+	pointer-events: none !important;
+}
+
+.menu-right
+{
+	appearance: none !important;
+	list-style-image: url(menu-right-1x.svg) !important;
+}
+
+menuitem[type=radio][checked=true]
+{
+	list-style-image: url(checkmark-18dp.svg) !important;
+}
+
+menuitem[type=radio] .menu-iconic-icon
+{
+	width: 16px !important;
+	height: 16px !important;
+	margin-inline-start: 0 !important;
+}
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/popup/urlbar-results.css b/testdata/home/.cache/ffcss/materialfox/_/chrome/popup/urlbar-results.css
new file mode 100755
index 0000000..b25d1ee
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/popup/urlbar-results.css
@@ -0,0 +1,582 @@
+.ac-type-icon
+{
+	order: 9 !important;
+	margin-inline-start: 16px !important;
+	margin-inline-end: 0 !important;
+}
+
+.ac-site-icon
+{
+	margin-inline-end: 16px !important;
+}
+
+.ac-separator
+{
+	display: none !important;
+}
+
+.ac-title,
+.ac-url
+{
+	flex: 1 !important;
+}
+
+.search-panel-one-offs
+{
+	border-radius: 0 0 8px 8px !important;
+	margin-bottom: 18px !important;
+}
+
+.search-one-offs
+{
+	display: block !important;
+}
+
+#urlbarView-results
+{
+	padding: 0 !important;
+}
+
+.urlbarView-row
+{
+	padding: 0 16px !important;
+	height: 32px !important;
+	border-radius: 0 !important;
+}
+
+.urlbarView-row-inner
+{
+	height: 100% !important;
+	align-items: center !important;
+}
+
+.urlbarView-favicon
+{
+	margin-inline-end: 16px !important;
+}
+
+.urlbarView-title-separator
+{
+	flex: 1 !important;
+}
+
+.urlbarView-title-separator::before
+{
+	display: none !important;
+}
+
+.urlbarView-type-icon
+{
+	order: 1 !important;
+	margin-inline-start: 16px !important;
+	margin-inline-end: 0 !important;
+}
+
+/* .search-one-offs
+{
+	padding: 4px !important;
+	display: flex !important;
+	flex-direction: column !important;
+}
+
+.search-panel-one-offs
+{
+	display: flex !important;
+	flex-direction: row !important;
+	padding: 0 !important;
+}
+
+.searchbar-engine-one-off-item
+{
+	height: 32px !important;
+	border-radius: 16px !important;
+	padding: 0 8px !important;
+	margin: 4px !important;
+	background-image: none !important;
+	color: inherit !important;
+	border: 0 !important;
+	display: flex !important;
+	justify-content: center !important;
+	align-items: center !important;
+	align-content: center !important;
+}
+
+.searchbar-engine-one-off-item > .button-box
+{
+	width: 16px !important;
+	max-height: 16px !important;
+	transform: translateY(50%) !important;
+}
+
+.searchbar-engine-one-off-item::after
+{
+	margin-inline-start: 8px !important;
+	content: attr(tooltiptext) !important;
+	line-height: 16px !important;
+	transform: translateY(calc(50% - 1px)) !important;
+}
+
+
+.searchbar-engine-one-off-item.dummy
+{
+	display: none !important;
+} */
+
+/* 70+ */
+
+#urlbar-container
+{
+	max-height: 37px !important;
+}
+
+#urlbar,
+#searchbar
+{
+	font-size: 14px !important;
+	border-radius: 99px !important;
+	background-color: var(--toolbar-field-background-color) !important;
+	border: 0 !important;
+	box-shadow: none !important;
+	padding: 2px !important;
+	background-clip: padding-box !important;
+	min-height: 28px !important;
+	transition: background-color .1s var(--ease-basic) !important;
+	-moz-box-align: center !important;
+	
+	display: block !important;
+	margin: 0 !important;
+}
+
+#urlbar:not(:-moz-lwtheme),
+#searchbar:not(:-moz-lwtheme)
+{
+	color: inherit !important;
+}
+
+#urlbar:hover,
+#searchbar:hover
+{
+	background-color: var(--toolbar-field-hover-background-color) !important;
+}
+
+#urlbar[focused],
+#urlbar[open],
+#searchbar[focused],
+#searchbar[open]
+{
+	background-color: var(--toolbar-field-focus-background-color) !important;
+}
+
+#urlbar[focused],
+#searchbar[focused]
+{
+	border: 2px solid var(--toolbar-field-focus-border-color) !important;
+	padding: 0 !important;
+	margin: 0 !important;
+}
+
+#urlbar[open],
+#searchbar[open]
+{
+	border: 0 !important;
+	padding: 0 !important;
+	border-radius: 8px 8px 0 0 !important;
+	min-height: 37px !important;
+	margin: 0 -5px -1px !important;
+	box-shadow:
+		0 5px 5px -3px rgba(0,0,0,.2),
+		0 8px 10px 1px rgba(0,0,0,.14),
+		0 3px 14px 2px rgba(0,0,0,.12) !important;
+	position: relative !important;
+	z-index: 99999 !important;
+}
+
+#urlbar-input-container
+{
+	width: 100% !important;
+	display: flex !important;
+	align-items: center !important;
+}
+
+.urlbar-input-box
+{
+	flex: 1 !important;
+}
+
+#urlbar-input
+{
+	width: 100% !important;
+}
+
+#urlbar[open] #urlbar-input-container
+{
+	height: 37px !important;
+}
+
+#urlbar[open] #identity-box
+{
+	margin-inline-start: 8px !important;
+	margin-inline-end: 8px !important;
+}
+
+.urlbarView
+{
+	top: 0 !important;
+	left: 0 !important;
+	right: 0 !important;
+	position: relative !important;
+	box-shadow: none !important;
+	background: 0 !important;
+	border: 0 !important;
+	background: white !important;
+}
+
+#urlbar[open] .urlbarView
+{
+	display: block !important;
+	width: 100% !important;
+}
+
+#urlbar-results
+{
+	padding: 0 !important;
+}
+
+#urlbar-container,
+#searchbar-container
+{
+	overflow: visible !important;
+	padding: 0 5px !important;
+}
+
+.searchbar-textbox
+{
+	color: inherit !important;
+	font-size: inherit !important;
+	min-height: auto !important;
+}
+
+#urlbar[pageproxystate=valid] > #identity-box:not(.no-hover) > #identity-icon
+{
+	fill-opacity: 1 !important;
+}
+
+#identity-icon-labels
+{
+	transform: translateY(-3px) !important;
+	color: inherit !important;
+	opacity: 1 !important;
+	padding: 0 !important;
+	margin-inline-start: 8px !important;
+	display: none !important;
+}
+
+#identity-box,
+#urlbar-display-box
+{
+	position: relative !important;
+}
+
+/* separator */
+#urlbar[pageproxystate=valid]:not([open]) #identity-box:-moz-any(.notSecureText, .verifiedIdentity, .chromeUI, .extensionPage, .certUserOverridden)::after,
+#urlbar-display-box::after
+{
+	content: "" !important;
+	display: -moz-box !important;
+	position: absolute !important;
+	top: 4px !important;
+	bottom: 4px !important;
+	right: 0 !important;
+	background: #9d9e9f !important;
+	width: 1px !important;
+	transition: opacity .2s var(--ease-basic) !important;
+}
+
+#urlbar[pageproxystate=valid]:not([open]) #identity-box:-moz-any(.notSecureText, .verifiedIdentity, .chromeUI, .extensionPage, .certUserOverridden),
+#urlbar-display-box
+{
+	margin-inline-end: 8px !important;
+}
+
+#urlbar[pageproxystate=valid]:not([open]) #identity-box:-moz-any(.notSecureText, .verifiedIdentity, .chromeUI, .extensionPage, .certUserOverridden) #identity-icon-labels
+{
+	display: -moz-box !important;
+}
+
+
+#urlbar[pageproxystate=valid] #identity-box:-moz-any(.notSecureText, .verifiedIdentity, .chromeUI, .extensionPage, .certUserOverridden):hover:not(.no-hover)::after,
+#urlbar[pageproxystate=valid] #identity-box:-moz-any(.notSecureText, .verifiedIdentity, .chromeUI, .extensionPage, .certUserOverridden)[open]::after
+{
+	opacity: 0 !important;
+}
+
+#identity-box.extensionPage > #identity-icon
+{
+	list-style-image: none !important;
+	margin-inline-end: -16px !important;
+}
+
+/* no longer used as a connection icon */
+#connection-icon
+{
+	display: none !important;
+}
+
+#identity-box:-moz-any(
+	.mixedActiveBlocked,
+	.mixedDisplayContentLoadedActiveBlocked,
+	.mixedActiveContent
+) > #connection-icon
+{
+	display: -moz-box !important;
+}
+
+#identity-box:-moz-any(.certUserOverridden, .unknownIdentity)
+{
+	color: #c94031 !important;
+}
+
+.urlbar-input
+{
+	padding: 0 !important;
+}
+
+.searchbar-textbox
+{
+	border: 0 !important;
+	background: 0 !important;
+	box-shadow: none !important;
+	margin: 0 !important;
+}
+
+#page-action-buttons > #pageActionSeparator,
+.urlbar-history-dropmarker
+{
+	display: none !important;
+}
+
+#pageActionSeparator
+{
+	height: 24px !important;
+}
+
+.urlbar-icon-wrapper
+{
+	background: 0 !important;
+}
+
+.urlbar-icon,
+#identity-box,
+#tracking-protection-icon-container
+{
+	min-width: 32px !important;
+	height: 24px !important;
+	padding: 4px 8px !important;
+	margin: 0 !important;
+	border: 0 !important;
+	border-radius: 99px !important;
+	fill-opacity: 1 !important;
+	transition: background-color .2s var(--ease-basic) !important;
+	background: 0 !important;
+	background-color: transparent !important;
+}
+
+#identity-box #notification-popup-box
+{
+	padding: 0 !important;
+	margin: 0 !important;
+}
+
+#page-action-buttons
+{
+	height: 24px !important;
+}
+
+#urlbar[open] #page-action-buttons
+{
+	display: none !important;
+}
+
+.urlbar-icon:hover:not([disabled]),
+#identity-box:hover:not(.no-hover),
+#tracking-protection-icon-container:hover
+{
+	background-color: var(--toolbarbutton-hover-background) !important;
+}
+
+.urlbar-icon:hover:active:not([disabled]),
+.urlbar-icon[open],
+#identity-box:hover:active:not(.no-hover),
+#identity-box[open]:not(.no-hover),
+#tracking-protection-icon-container[open]
+{
+	background-color: var(--toolbarbutton-active-background) !important;
+	transition-duration: 0 !important;
+}
+
+#star-button-box
+{
+	-moz-box-ordinal-group: 99 !important;
+}
+
+#pageActionButton
+{
+	-moz-box-ordinal-group: 98 !important;
+}
+
+#contextual-feature-recommendation
+{
+	-moz-box-ordinal-group: 97 !important;
+	width: auto !important;
+}
+
+.urlbar-page-action:-moz-any(
+	#reader-mode-button,
+	#pageActionButton,
+	#pocket-button-box,
+	#pageAction-urlbar-screenshots_mozilla_org,
+	#pageAction-urlbar-sendToDevice,
+	#pageAction-urlbar-emailLink,
+	#pageAction-urlbar-copyURL,
+	#pageAction-urlbar-shareURL,
+	#pageAction-urlbar-addSearchEngine
+)
+{
+	transition:
+		background-color .2s var(--ease-basic),
+		margin .15s var(--ease-in),
+		opacity .15s var(--ease-basic),
+		visibility 0s .2s !important;
+	opacity: 0 !important;
+	visibility: hidden !important;
+	margin-inline-start: -32px !important;
+}
+
+#urlbar:hover .urlbar-page-action:-moz-any(
+	#reader-mode-button,
+	#pageActionButton,
+	#pocket-button-box,
+	#pageAction-urlbar-screenshots_mozilla_org,
+	#pageAction-urlbar-sendToDevice,
+	#pageAction-urlbar-emailLink,
+	#pageAction-urlbar-copyURL,
+	#pageAction-urlbar-shareURL,
+	#pageAction-urlbar-addSearchEngine
+),
+.urlbar-page-action:-moz-any(
+	#reader-mode-button,
+	#pageActionButton,
+	#pocket-button-box,
+	#pageAction-urlbar-screenshots_mozilla_org,
+	#pageAction-urlbar-sendToDevice,
+	#pageAction-urlbar-emailLink,
+	#pageAction-urlbar-copyURL,
+	#pageAction-urlbar-shareURL,
+	#pageAction-urlbar-addSearchEngine
+):-moz-any(:hover, [open], [readeractive])
+{
+	transition:
+		background-color .2s var(--ease-basic),
+		margin .3s var(--ease-out),
+		opacity .3s var(--ease-basic) !important;
+	opacity: 1 !important;
+	visibility: visible !important;
+	margin-inline-start: 0 !important;
+}
+
+.urlbar-display
+{
+	color: inherit !important;
+	margin: 0 !important;
+}
+
+#pocket-button-box[animate] > #pocket-animatable-box
+{
+	margin-inline-start: 6px !important;
+}
+
+#star-button-animatable-box
+{
+	display: none !important;
+}
+
+#cfr-label-container
+{
+	background: 0 !important;
+}
+
+#cfr-label
+{
+	display: none !important;
+}
+
+#cfr-button
+{
+	fill: currentColor !important;
+}
+
+#urlbar[open] .urlbar-go-button,
+#searchbar[open] .search-go-button
+{
+	height: 32px !important;
+}
+
+/* icons 1x */
+#urlbar
+{
+	--info-icon: url(../navbar/info-1x.svg);
+	--lock-icon: url(../navbar/connection-1x.svg);
+	--warning-icon: url(../navbar/connection-warning-1x.svg);
+}
+
+/* icons 2x */
+@media (min--moz-device-pixel-ratio: 2)
+{
+	#urlbar
+	{
+		--info-icon: url(../navbar/info-2x.svg);
+		--lock-icon: url(../navbar/connection-2x.svg);
+		--warning-icon: url(../navbar/connection-warning-2x.svg);
+	}
+}
+
+/* info (not secure) */
+#identity-box[pageproxystate="valid"]:-moz-any(.notSecure, .insecureLoginForms, .mixedActiveContent) > #identity-icon
+{
+    list-style-image: var(--info-icon) !important;
+}
+
+#identity-box[pageproxystate="valid"]:-moz-any(
+	.certUserOverridden,
+	.weakCipher,
+	.unknownIdentity) > #identity-icon
+{
+    list-style-image: var(--warning-icon) !important;
+}
+
+/* lock (secure) */
+#identity-box[pageproxystate="valid"]:-moz-any(.verifiedDomain, .verifiedIdentity, .mixedActiveBlocked) > #identity-icon
+{
+	list-style-image: var(--lock-icon) !important;
+}
+
+/* high DPI adjustments */
+@media (min--moz-device-pixel-ratio: 2)
+{
+	#identity-icon-labels
+	{
+		padding-inline-end: .5px !important;
+	}
+	
+	#urlbar[pageproxystate=valid] > #identity-box:-moz-any(.unknownIdentity, .notSecure) > #identity-icon
+	{
+		transform: translate(.5px, .5px) !important;
+	}
+	
+	#identity-icon-labels,
+	.urlbar-input,
+	.searchbar-textbox
+	{
+		transform: translateY(-.5px) !important;
+	}
+}
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/close-tab.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/close-tab.svg
new file mode 100644
index 0000000..9010d56
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/close-tab.svg
@@ -0,0 +1,14 @@
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	width="16"
+	height="16"
+	viewBox="0 0 16 16"
+	stroke-linecap="round"
+	stroke-linejoin="round"
+	stroke-width="1.5"
+	stroke="context-fill"
+	stroke-opacity="context-fill-opacity"
+>
+	<path d="M11.25,11.25l-6.5-6.5"/>
+	<path d="M4.75,11.25l6.5-6.5"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/spinner-busy.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/spinner-busy.svg
new file mode 100644
index 0000000..0d7ce65
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/spinner-busy.svg
@@ -0,0 +1,28 @@
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+	<style>
+		:root
+		{
+			--radius: calc(2 * 3.1415926536 * 37.5%);
+			--max-gap: calc(var(--radius) * .99);
+			--min-gap: calc(var(--radius) * .5);
+		}
+	
+		@keyframes stroke-rotate
+		{
+			0% {   stroke-dashoffset: var(--max-gap); }
+			100% { stroke-dashoffset: var(--min-gap); }
+		}
+		
+		circle
+		{
+			fill: transparent;
+			stroke: context-fill;
+			stroke-width: 12.5%;
+			stroke-dasharray: var(--radius);
+			stroke-linecap: round;
+			transform-origin: center;
+			animation: stroke-rotate 1.333s cubic-bezier(.35, 0, .25, 1) both;
+		}
+	</style>
+	<circle cx="50%" cy="50%" r="37.5%"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/spinner-progress.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/spinner-progress.svg
new file mode 100644
index 0000000..fbe4f23
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/spinner-progress.svg
@@ -0,0 +1,43 @@
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+	<style>
+		:root
+		{
+			--radius: calc(2 * 3.1415926536 * 37.5%);
+			--max-gap: calc(var(--radius) * .99);
+			--min-gap: calc(var(--radius) * .25);
+		}
+
+		@keyframes stroke-rotate
+		{
+			0% {			  stroke-dashoffset: var(--max-gap);		 }
+			12.5% {			  stroke-dashoffset: var(--min-gap);
+							  transform: rotate(0);						 }
+			12.5001%, 25% {	  transform: rotateX(180deg) rotate(90deg);	 }
+			25% {			  stroke-dashoffset: var(--max-gap); 	 	 }
+			25.0001%, 37.5% { transform: rotate(270deg); 				 }
+			37.5% {			  stroke-dashoffset: var(--min-gap); 	 	 }
+			37.5001%, 50% {	  transform: rotateX(180deg) rotate(180deg); }
+			50% {			  stroke-dashoffset: var(--max-gap); 	 	 }
+			50.0001%, 62.5% { transform: rotate(180deg); 				 }
+			62.5% {			  stroke-dashoffset: var(--min-gap); 	 	 }
+			62.5001%, 75% {	  transform: rotateX(180deg) rotate(270deg); }
+			75% {			  stroke-dashoffset: var(--max-gap); 	 	 }
+			75.0001%, 87.5% { transform: rotate(90deg); 				 }
+			87.5% {			  stroke-dashoffset: var(--min-gap); 	 	 }
+			87.5001%, 100% {  transform: rotateX(180deg) rotate(360deg); }
+			100% {			  stroke-dashoffset: var(--max-gap); 	 	 }
+		}
+		
+		circle
+		{
+			fill: transparent;
+			stroke-dasharray: var(--radius);
+			stroke: context-fill;
+			stroke-width: 12.5%;
+			stroke-linecap: round;
+			transform-origin: center;
+			animation: stroke-rotate calc(1.333s * 4) cubic-bezier(.35, 0, .25, 1) infinite both;
+		}
+	</style>
+	<circle cx="50%" cy="50%" r="37.5%"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/tabbar.css b/testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/tabbar.css
new file mode 100755
index 0000000..939bebe
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/tabbar/tabbar.css
@@ -0,0 +1,589 @@
+:-moz-any(.titlebar-placeholder, .titlebar-spacer)[type=pre-tabs]
+{
+	width: 0 !important;
+}
+
+@media (-moz-mac-yosemite-theme)
+{
+	#TabsToolbar
+	{
+		-moz-appearance: none !important;
+	}
+	
+	:root:-moz-any([inFullscreen], [tabsintitlebar]):not([privatebrowsingmode=temporary]) #TabsToolbar:not(:-moz-lwtheme)
+	{
+		background-color: #dee1e5 !important;
+		color: #606367 !important;
+		--toolbarbutton-hover-background: #cbced2 !important;
+		--toolbarbutton-active-background: #bec0c5 !important;
+	}
+	
+	:root:-moz-any([inFullscreen], [tabsintitlebar]):not([privatebrowsingmode=temporary]) #TabsToolbar:not(:-moz-lwtheme):-moz-window-inactive
+	{
+		background-color: #e7eaed !important;
+	}
+	
+	:-moz-any(.titlebar-placeholder, .titlebar-spacer)[type=pre-tabs]
+	{
+		width: 4px !important;
+	}
+}
+
+@media (-moz-os-version: windows-win7)
+{
+	:root:not([privatebrowsingmode=temporary]) #TabsToolbar:not(:-moz-lwtheme)
+	{
+		color: #808387 !important;
+		--toolbarbutton-hover-background: rgba(196, 198, 204, .8) !important;
+		--toolbarbutton-active-background: #bec0c6 !important;
+	}
+	
+	:root:not([privatebrowsingmode=temporary]) #TabsToolbar toolbarbutton:not(:-moz-lwtheme):not(:hover):not([open])
+	{
+		background-color: rgba(218, 220, 227, .8) !important;
+	}
+	
+	:root:not([privatebrowsingmode=temporary]) .tabbrowser-tab:not([visuallyselected]):not(:-moz-lwtheme)
+	{
+		--tab-bgcolor: rgb(218, 220, 227) !important;
+		--tab-opacity: .8 !important;
+	}
+	
+	:root:not([privatebrowsingmode=temporary]) .tabbrowser-tab:hover:not([visuallyselected]):not(:-moz-lwtheme)
+	{
+		--tab-bgcolor: #e9ebef !important;
+	}
+}
+
+ /* < FF 65, >= FF 65 */
+:-moz-any(.titlebar-placeholder, .titlebar-spacer)
+{
+	border: 0 !important;
+	display: -moz-box !important;
+}
+
+:-moz-any(.titlebar-placeholder, .titlebar-spacer)[type=post-tabs]
+{
+	width: 48px !important;
+	transition: width .666s cubic-bezier(.4, 0, .2, 1) !important;
+}
+
+@media (max-width: 700px)
+{
+	:-moz-any(.titlebar-placeholder, .titlebar-spacer)[type=post-tabs]
+	{
+		width: 8px !important;
+	}
+}
+
+/* vertically center buttons in tab bar */
+#TabsToolbar toolbarbutton,
+#TabsToolbar .toolbar-items toolbarbutton.toolbarbutton-1,
+/* >= 72 */
+#TabsToolbar .tabbrowser-arrowscrollbox::part(scrollbutton-up),
+#TabsToolbar .tabbrowser-arrowscrollbox::part(scrollbutton-down),
+/* no ::part workaround - may have side effects */
+#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton),
+#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton)
+{
+	margin-top: var(--tab-shadow-max-size) !important;
+	margin-bottom: var(--proton-tab-block-margin) !important;
+}
+
+.tabbrowser-tab
+{
+	min-height: var(--tab-min-height) !important;
+	overflow: visible !important;
+	font-size: 12px !important;
+	background: 0 !important;
+	border: 0 !important;
+	padding: 0 !important;
+}
+
+.tabbrowser-tab[visuallyselected]
+{
+	color: var(--toolbar-color) !important;
+}
+
+/* regular */
+.tabbrowser-tab[fadein]:not([pinned]):not([style*="max-width"])
+{
+	max-width: 240px !important;
+}
+
+/* neighbouring tabs should "pinch" together */
+.tabbrowser-tab:not([last-visible-tab])
+{
+	margin-inline-end: -.5px !important;
+}
+
+/* special case for pinned tabs when overflowing */
+#tabbrowser-tabs:not([overflow]) .tabbrowser-tab:not([first-visible-tab]),
+#tabbrowser-tabs[overflow] .tabbrowser-tab:not([first-visible-tab]):not([pinned])
+{
+	margin-inline-start: -.5px !important;
+}
+
+.tab-stack
+{
+	margin-top: 4px !important;
+	margin-bottom: -4px !important;
+}
+
+#tabbrowser-tabs[overflow] .tabbrowser-tab[pinned] .tab-background
+{
+	margin: 0 -1px !important;
+}
+
+#tabbrowser-tabs[overflow] .tabbrowser-tab[pinned] .tab-stack
+{
+	margin-top: 8px !important;
+	margin-bottom: 0 !important;
+}
+
+.tab-content
+{
+	position: relative !important;
+	overflow: hidden !important;
+	padding-inline-start: 12px !important;
+	padding-inline-end: 8px !important;
+}
+
+.tab-content::before,
+.tab-content::after
+{
+	content: "" !important;
+	display: block !important;
+	position: absolute !important;
+	background-color: currentColor !important;
+	width: 1px !important;
+	height: 20px !important;
+	transform: translateY(-10px) !important;
+	opacity: 0 !important;
+	transition: opacity .2s var(--ease-basic) !important;
+}
+
+.tab-content::before
+{
+	left: 0 !important;
+}
+
+.tab-content::after
+{
+	right: 0 !important;
+}
+
+.tab-throbber,
+.tab-throbber-fallback,
+.tab-icon-image,
+.tab-sharing-icon-overlay,
+.tab-icon-sound,
+.tab-close-button
+{
+	margin-top: 0 !important;
+}
+
+.tabbrowser-tab::before,
+.tabbrowser-tab::after,
+.tab-line
+{
+	display: none !important;
+}
+
+/* tab background color */
+
+.tabbrowser-tab
+{
+	--tab-opacity: 0;
+	--tab-bgcolor: #000;
+	--tab-transition-duration: .2s;
+}
+
+:-moz-any(:root[privatebrowsingmode=temporary], #TabsToolbar[brighttext]) .tabbrowser-tab
+{
+	--tab-opacity: 0;
+	--tab-bgcolor: #fff;
+}
+
+.tabbrowser-tab:not([selected=true]):hover,
+.tabbrowser-tab[multiselected]:not([selected=true])
+{
+	--tab-opacity: .1;
+}
+
+:-moz-any(:root[privatebrowsingmode=temporary], #TabsToolbar[brighttext]) .tabbrowser-tab:not([selected=true]):hover,
+:-moz-any(:root[privatebrowsingmode=temporary], #TabsToolbar[brighttext]) .tabbrowser-tab[multiselected]:not([selected=true])
+{
+	--tab-opacity: .06;
+}
+
+:-moz-any(#TabsToolbar) .tabbrowser-tab[visuallyselected]
+{
+	--tab-bgcolor: var(--toolbar-bgcolor);
+	--tab-opacity: 1;
+}
+
+.tab-background
+{
+ 	background: var(--tab-bgcolor) !important;
+	/* rounded top corners */
+	border-radius: 8px 8px 0 0 !important;
+	position: relative !important;
+	border: 0 !important;
+	transition:
+		opacity var(--tab-transition-duration) var(--ease-basic),
+		background-color 0s var(--tab-transition-duration) var(--ease-basic) !important;
+	opacity: var(--tab-opacity) !important;
+	visibility: visible !important;
+}
+
+/* rounded bottom corners */
+.tab-background::before,
+.tab-background::after
+{
+	content: "" !important;
+	display: block !important;
+	position: absolute !important;
+	width: 8px !important;
+	height: 8px !important;
+	bottom: 0 !important;
+	pointer-events: none !important;
+	transition: box-shadow 0s var(--tab-transition-duration) var(--ease-basic) !important;
+}
+
+.tab-background::before
+{
+	border-bottom-right-radius: 8px !important;
+	left: 0 !important;
+	transform: translateX(-8px) !important;
+	box-shadow: 4px 4px 0 4px var(--tab-bgcolor) !important;
+}
+
+.tab-background::after
+{
+	border-bottom-left-radius: 8px !important;
+	right: 0 !important;
+	transform: translateX(8px) !important;
+	box-shadow: -4px 4px 0 4px var(--tab-bgcolor) !important;
+}
+
+.tabbrowser-tab[visuallyselected][style*=transform] + .tabbrowser-tab[style*=transform] .tab-content::after,
+.tabbrowser-tab[style*=transform]:not([visuallyselected]) .tab-content::before,
+.tabbrowser-tab[style*=transform] + .tabbrowser-tab:not([visuallyselected]) .tab-content::before,
+.tabbrowser-tab:not([visuallyselected]):not(:hover):not([multiselected]) + .tabbrowser-tab:not([visuallyselected]):not(:hover):not([multiselected]) .tab-content::before,
+#tabbrowser-tabs[hasadjacentnewtabbutton]:not([overflow]) .tabbrowser-tab[last-visible-tab]:not([visuallyselected]):not(:hover):not([multiselected]) .tab-content::after
+{
+	opacity: var(--tab-separator-opacity) !important;
+}
+
+#tabbrowser-tabs[overflow] .tabbrowser-tab[pinned] + .tabbrowser-tab:not([pinned]) .tab-content::before
+{
+	opacity: 0 !important;
+}
+
+.tabbrowser-tab[visuallyselected]
+{
+	--tab-transition-duration: 0s;
+}
+
+.tab-throbber,
+.tab-throbber-fallback
+{
+	margin-inline-end: 8px !important;
+}
+
+.tab-icon-image
+{
+	margin: 0 !important;
+}
+
+/* hide new tab favicon */
+.tabbrowser-tab[image^="chrome://branding/"]:not([pinned]) .tab-icon-image
+{
+	display: none !important;
+}
+
+.tab-label-container
+{
+	margin-top: -2px !important;
+	opacity: 1 !important;
+}
+
+.tabbrowser-tab[image]:not([image^="chrome://branding/"]) .tab-label-container,
+.tabbrowser-tab:-moz-any([progress], [busy]) .tab-label-container
+{
+	padding-inline-start: 8px !important;
+}
+
+.tabbrowser-tab[pinned]:-moz-any([soundplaying], [muted], [activemedia-blocked]) .tab-icon-image
+{
+	visibility: hidden !important;
+}
+
+.tabbrowser-tab[fadein] .tab-close-button
+{
+	visibility: visible !important;
+}
+
+.tab-close-button
+{
+	list-style-image: url(close-tab.svg) !important;
+	width: 16px !important;
+	height: 16px !important;
+	margin: 0 !important;
+	padding: 0 !important;
+}
+
+.tab-close-button,
+.tab-icon-overlay:-moz-any([soundplaying], [muted], [activemedia-blocked]),
+.tab-icon-sound
+{
+	border-radius: 99px !important;
+	color: inherit !important;
+	-moz-context-properties: fill, fill-opacity !important;
+	transition-property: fill-opacity, background-color !important;
+	transition-duration: .15s !important;
+	transition-timing-function: var(--ease-basic) !important;
+	fill-opacity: 1 !important;
+	opacity: 1 !important;
+}
+
+.tab-close-button:hover,
+.tab-icon-overlay:-moz-any([soundplaying], [muted], [activemedia-blocked]):hover,
+.tab-icon-sound:hover
+{
+	background-color: var(--toolbarbutton-hover-background) !important;
+}
+
+.tab-close-button:hover:active,
+.tab-icon-overlay:-moz-any([soundplaying], [muted], [activemedia-blocked]):hover:active,
+.tab-icon-sound:hover:active
+{
+	background-color: var(--toolbarbutton-active-background) !important;
+}
+
+.tab-icon-overlay[pinned]
+{
+	margin: 0 !important;
+	margin-inline-start: -16px !important;
+}
+
+.tab-sharing-icon-overlay
+{
+	margin-inline-start: -16px !important;
+}
+
+@keyframes rotate-360
+{
+	0% { transform: rotate(0); }
+	100% { transform: rotate(1turn); }
+}
+
+.tab-throbber
+{
+	-moz-context-properties: fill !important;
+	fill: currentColor !important;
+	background-image: url(spinner-busy.svg) !important;
+	margin: 0 !important;
+	transform-origin: center !important;
+	animation: rotate-360 1.333s linear infinite reverse !important;
+	position: static !important;
+}
+
+.tab-throbber[progress]
+{
+	background-image: url(spinner-progress.svg) !important;
+	animation-direction: normal !important;
+}
+
+.tab-throbber::before
+{
+	display: none !important;
+}
+
+/* clipped tabs */
+#tabbrowser-tabs[closebuttons=activetab] .tab-content:not([pinned])
+{
+	padding-inline-start: 8px !important;
+}
+
+#tabbrowser-tabs[closebuttons=activetab] .tabbrowser-tab:not([visuallyselected]) .tab-close-button
+{
+	visibility: collapse !important;
+}
+
+#tabbrowser-tabs[closebuttons=activetab] .tab-label-container[textoverflow][labeldirection="ltr"]:not([pinned]),
+#tabbrowser-tabs[closebuttons=activetab] .tab-label-container[textoverflow]:not([labeldirection]):-moz-locale-dir(ltr):not([pinned])
+{
+    mask-image: linear-gradient(to right, black 70%, transparent) !important;
+}
+
+#tabbrowser-tabs[closebuttons=activetab] .tab-label-container[textoverflow][labeldirection="rtl"]:not([pinned]),
+#tabbrowser-tabs[closebuttons=activetab] .tab-label-container[textoverflow]:not([labeldirection]):-moz-locale-dir(rtl):not([pinned])
+{
+    mask-image: linear-gradient(to left, black 70%, transparent) !important;
+}
+
+.tab-content[pinned]
+{
+	-moz-box-pack: center !important;
+}
+
+.tab-icon-image[pinned],
+.tab-throbber[pinned]
+{
+	margin: auto !important;
+}
+
+.tab-content[pinned]
+{
+	width: 36px !important;
+	padding: 0 !important;
+	padding-inline-start: 10px !important;
+	padding-inline-end: 0 !important;
+}
+
+.tab-label-container[pinned],
+.tab-close-button[pinned]
+{
+	visibility: hidden !important;
+} 
+
+/* close button / favicon is centered within 36px tab */
+/* disabled for now because it's broken in recent version of Firefox */
+@supports -moz-bool-pref("materialFox.reduceTabOverflow disabled")
+{
+	#main-window
+	{
+		/* same as Chrome */
+		min-width: 500px !important;
+	}
+	
+	#tabbrowser-tabs[overflow] .tab-content
+	{
+		-moz-box-pack: center !important;
+	}
+	
+	#tabbrowser-tabs[overflow] .tab-icon-image
+	{
+		margin: auto !important;
+	}
+	
+	#tabbrowser-tabs[overflow] .tab-content[image]:not([image^="chrome://branding/"]):not([pinned])
+	{
+		width: 36px !important;
+		padding: 0 !important;
+		padding-inline-start: 0 !important;
+		padding-inline-end: 0 !important;
+	}
+	
+	#tabbrowser-tabs[overflow] .tabbrowser-tab:not([visuallyselected]):not([pinned]) .tab-close-button,
+	#tabbrowser-tabs[overflow] .tabbrowser-tab[visuallyselected]:not([pinned]) :-moz-any(.tab-label-container, .tab-icon-image),
+	#tabbrowser-tabs[overflow] .tabbrowser-tab[image]:not([image^="chrome://branding/"]):not([pinned]) .tab-label-container
+	{
+		display: none !important;
+	} 
+}
+
+#tabbrowser-tabs #tabs-newtab-button
+{
+	margin-inline-start: 6px !important;
+}
+
+#tabbrowser-tabs[overflow] .tabbrowser-arrowscrollbox
+{
+	border-radius: 8px 8px 0 0 !important;
+	background-color: rgba(0, 0, 0, .1) !important;
+	padding-inline-start: 0 !important;
+}
+
+#tabbrowser-tabs[overflow]
+{
+	margin-inline-start: 8px !important;
+}
+
+#tabbrowser-tabs[overflow] .tabbrowser-tab[first-visible-tab]:not([pinned]),
+#tabbrowser-tabs[overflow] .tabbrowser-tab[pinned] + .tabbrowser-tab:not([pinned]),
+#tabbrowser-tabs:not([overflow]) .tabbrowser-tab[first-visible-tab]
+{
+	margin-inline-start: 8px !important;
+}
+
+#tabbrowser-tabs[overflow] .tabbrowser-tab[last-visible-tab]:not([pinned])
+{
+	margin-inline-end: 7px !important;
+}
+
+.tabbrowser-tab[usercontextid] > .tab-stack::after
+{
+	content: "" !important;
+	position: absolute !important;
+	display: flex !important;
+	bottom: 0 !important;
+	left: 8px !important;
+	width: calc(100% - 16px) !important;
+	height: 2px !important;
+	box-sizing: border-box !important;
+	border-radius: 99px 99px 0 0 !important;
+	transform: none !important;
+	background: var(--identity-tab-color) !important;
+	transition-property: top, bottom, left, right, width, height, border-radius;
+	transition-duration: .225s !important;
+	transition-timing-function: var(--ease-out) !important;
+}
+
+.tab-bottom-line
+{
+	display: none !important;
+}
+
+.tabbrowser-tab[usercontextid][selected] > .tab-stack::after
+{
+	bottom: calc(100% - 9px) !important;
+	left: calc(100% - 9px) !important;
+	width: 6px !important;
+	height: 6px !important;
+	border-radius: 99px !important;
+}
+
+/* < 72 */
+#tabbrowser-tabs .arrowscrollbox-overflow-start-indicator,
+#tabbrowser-tabs .arrowscrollbox-overflow-end-indicator,
+/* >= 72 */
+#tabbrowser-tabs::part(arrowscrollbox-overflow-start-indicator),
+#tabbrowser-tabs::part(arrowscrollbox-overflow-end-indicator),
+/* no ::part workaround - may have side effects */
+spacer[part="overflow-start-indicator"],
+spacer[part="overflow-end-indicator"]
+{
+	display: none !important;
+}
+
+.tab-loading-burst[bursting]::before
+{
+	display: none !important;
+}
+
+/* high DPI adjustments */
+@media (min--moz-device-pixel-ratio: 2)
+{
+	.tabbrowser-tab::before
+	{
+		transform: translateY(.5px) !important;
+	}
+
+	.tabbrowser-tab:last-of-type::after
+	{
+		transform: translate(-1px, .5px) !important;
+	}
+	
+	/* macOS */
+	@media (-moz-mac-yosemite-theme)
+	{
+		.titlebar-buttonbox
+		{
+			margin-top: .5px !important;
+			margin-bottom: -.5px !important;
+		}
+	}
+}
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-1x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-1x.svg
new file mode 100644
index 0000000..f8568ac
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-1x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M11,13H5a1,1,0,0,1-1-1V7A1,1,0,0,1,5,6V5a3,3,0,0,1,6,0V6a1,1,0,0,1,1,1v5A1,1,0,0,1,11,13ZM8,3A2,2,0,0,0,6,5V6h4V5A2,2,0,0,0,8,3Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-2x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-2x.svg
new file mode 100644
index 0000000..cfabbde
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-2x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M11,13.5H5a1,1,0,0,1-1-1v-5a1,1,0,0,1,1-1H5.5v-1a2.5,2.5,0,0,1,5,0v1H11a1,1,0,0,1,1,1v5A1,1,0,0,1,11,13.5ZM8,4A1.5,1.5,0,0,0,6.5,5.5v1h3v-1A1.5,1.5,0,0,0,8,4Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-warning-1x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-warning-1x.svg
new file mode 100644
index 0000000..94e73cd
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-warning-1x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" d="M8,1l8,13H0ZM7,6H9V9H7V6Zm0,4H9v2H7V10Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-warning-2x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-warning-2x.svg
new file mode 100644
index 0000000..ec5553b
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/connection-warning-2x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" d="M7.75,2.5L14,13.5H1.5ZM7,6.5H8.5v3H7v-3Zm0,4H8.5V12H7V10.5Z"/>
+</svg>
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/go.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/go.svg
new file mode 100644
index 0000000..d819ee6
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/go.svg
@@ -0,0 +1,14 @@
+<svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="16" height="16" viewBox="0 0 16 16"
+>
+    <path
+        stroke="context-fill"
+        stroke-opacity="context-fill-opacity"
+        fill="none"
+        stroke-linecap="round"
+        stroke-linejoin="round"
+        stroke-width="1.5px"
+        d="M5.75,4.25L10.25,8l-4.5,3.75"
+    />
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/identity-icons-brand.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/identity-icons-brand.svg
new file mode 100644
index 0000000..4870645
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/identity-icons-brand.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M31.4 14.627c-.044-.289-.088-.46-.088-.46s-.113.131-.3.379a10.8 10.8 0 0 0-.375-2.04 13.883 13.883 0 0 0-.94-2.418 10.078 10.078 0 0 0-.858-1.473q-.177-.264-.36-.512c-.571-.934-1.227-1.5-1.986-2.583a7.826 7.826 0 0 1-.993-2.692 10.88 10.88 0 0 0-.477 1.761c-.779-.786-1.458-1.345-1.866-1.726C21.133.978 21.367 0 21.367 0s-3.773 4.209-2.137 8.6a8.43 8.43 0 0 0 2.81 3.744c1.581 1.3 3.283 2.33 4.18 4.952a8.4 8.4 0 0 0-3.154-3.327 7.593 7.593 0 0 1 .6 3.006 7.145 7.145 0 0 1-8.736 6.96 6.576 6.576 0 0 1-1.77-.6 7.2 7.2 0 0 1-2.121-1.96l-.01-.017.126.046a6.517 6.517 0 0 0 .9.242 5.644 5.644 0 0 0 3.594-.424c1.129-.626 1.813-1.091 2.367-.908h.01c.542.172.969-.353.581-.9a2.949 2.949 0 0 0-2.846-1.114c-1.131.165-2.167.968-3.648.19a3.13 3.13 0 0 1-.278-.163c-.1-.058.317.088.221.022a7.351 7.351 0 0 1-.931-.555c-.022-.018.224.07.2.052a3.591 3.591 0 0 1-.971-.982 1.746 1.746 0 0 1-.066-1.559 1.376 1.376 0 0 1 .6-.566c.192.095.31.166.31.166s-.087-.16-.135-.244c.017-.006.032 0 .049-.011.168.073.539.261.734.376a1.018 1.018 0 0 1 .335.3s.067-.033.017-.173a.907.907 0 0 0-.347-.425h.016A2.952 2.952 0 0 1 12.3 15a2.085 2.085 0 0 0 .171-.906 1.181 1.181 0 0 0-.069-.5c-.054-.1.03-.14.123-.035a.977.977 0 0 0-.079-.239v-.008s.053-.069.078-.095a1.434 1.434 0 0 1 .216-.176 10 10 0 0 1 1.469-.749c.416-.181.759-.32.83-.36a2.287 2.287 0 0 0 .294-.226 1.973 1.973 0 0 0 .661-1.143 1.593 1.593 0 0 0 .017-.178v-.105c-.06-.226-.449-.395-2.483-.586a1.778 1.778 0 0 1-1.454-1.364v.009c-.029.075-.055.15-.081.225.026-.075.052-.151.081-.225v-.016a5.153 5.153 0 0 1 1.992-2.474c.052-.043-.208.011-.156-.032a5.18 5.18 0 0 1 .532-.225c.091-.039-.391-.222-.818-.178a2.2 2.2 0 0 0-.758.178c.1-.086.4-.2.33-.2a4.877 4.877 0 0 0-1.547.585.315.315 0 0 1 .03-.14 2.407 2.407 0 0 0-.966.746 1.286 1.286 0 0 0 .01-.174 2.883 2.883 0 0 0-.475.446l-.009.007a6.3 6.3 0 0 0-3.527-.3l-.01-.009h.012a2.953 2.953 0 0 1-.627-.7l-.008-.012-.014-.021a5.48 5.48 0 0 1-.237-.388 5.527 5.527 0 0 1-.176-.339c0-.008-.009-.011-.013-.012-.024 0-.041.111-.061.082v-.006a4.322 4.322 0 0 1-.309-1.697l-.016.008a1.89 1.89 0 0 0-.716.937c-.062.137-.1.213-.14.288v-.035c.009-.069.039-.212.032-.2s-.013.019-.02.029a1.741 1.741 0 0 0-.252.373 2.366 2.366 0 0 0-.15.383c-.006.021 0-.018 0-.064s.009-.128 0-.111l-.022.043a9.526 9.526 0 0 0-.806 3.044 3.038 3.038 0 0 0-.023.54v.016a6.641 6.641 0 0 0-.819 1.1 15.65 15.65 0 0 0-1.731 4.246 10.379 10.379 0 0 1 .928-1.626 15.041 15.041 0 0 0-1.049 5.514 14.264 14.264 0 0 1 .451-1.634 13.848 13.848 0 0 0 1.25 6.489 15.08 15.08 0 0 0 5.968 6.768 13.057 13.057 0 0 0 3.478 1.667c.163.059.327.117.5.173-.053-.021-.1-.044-.153-.067a15.752 15.752 0 0 0 4.506.659c5.41 0 7.2-2.06 7.36-2.266a2.739 2.739 0 0 0 .639-.858q.156-.064.316-.137l.067-.03.122-.057a11.349 11.349 0 0 0 2.284-1.43 5.511 5.511 0 0 0 2.129-3.11 1.944 1.944 0 0 0 .029-1.433q.083-.132.171-.28a12.743 12.743 0 0 0 1.913-6.2v-.184a7.76 7.76 0 0 0-.115-1.388z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/info-1x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/info-1x.svg
new file mode 100644
index 0000000..a88c105
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/info-1x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M8,15a7,7,0,1,1,7-7A7,7,0,0,1,8,15ZM8,2.5A5.5,5.5,0,1,0,13.5,8,5.5,5.5,0,0,0,8,2.5ZM7,7H9v5H7V7ZM7,4H9V6H7V4Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/info-2x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/info-2x.svg
new file mode 100644
index 0000000..158df0c
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/info-2x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M8.25,15A6.75,6.75,0,1,1,15,8.25,6.75,6.75,0,0,1,8.25,15Zm0-12A5.25,5.25,0,1,0,13.5,8.25,5.25,5.25,0,0,0,8.25,3ZM7.5,7.5H9v4H7.5v-4ZM7.5,5H9V6.5H7.5V5Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/naughty-shield-1x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/naughty-shield-1x.svg
new file mode 100644
index 0000000..59327d5
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/naughty-shield-1x.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M12.7,7h-0.92A28.263,28.263,0,0,0,12,3.5l-4.5-2V7.153A0.986,0.986,0,0,0,7,8v5.794A7.942,7.942,0,0,1,3.554,11C1.994,8.647,2,2.5,2,2.5L7.531,0,13,2.5A34.541,34.541,0,0,1,12.7,7Z"/>
+	<path fill="#cc4f40" fill-opacity="context-fill-opacity" d="M14,15H9a1,1,0,0,1-1-1V9A1,1,0,0,1,9,8h5a1,1,0,0,1,1,1v5A1,1,0,0,1,14,15Zm-1.139-2.846-0.646-.646,0.646-.646-0.008-.008A0.5,0.5,0,0,0,12.5,10a0.491,0.491,0,0,0-.386.194l-0.606.606-0.646-.646,0,0a0.5,0.5,0,1,0-.7.7l0,0L10.8,11.508l-0.606.606A0.491,0.491,0,0,0,10,12.5a0.5,0.5,0,0,0,.854.354l0.008,0.008,0.646-.646,0.646,0.646,0,0a0.5,0.5,0,0,0,.7-0.7Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/naughty-shield-2x.svg b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/naughty-shield-2x.svg
new file mode 100644
index 0000000..549aad2
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/naughty-shield-2x.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M13.152,7.5H12.22a25.187,25.187,0,0,0,.28-3.75L8.025,2,8.012,7.645A0.987,0.987,0,0,0,7.5,8.5v5.309A8,8,0,0,1,4,11C2.431,8.647,2.5,3,2.5,3L8,1l5.5,2A29.211,29.211,0,0,1,13.152,7.5Z"/>
+	<path fill="#cc4f40" fill-opacity="context-fill-opacity" d="M14.5,15.5h-5a1,1,0,0,1-1-1v-5a1,1,0,0,1,1-1h5a1,1,0,0,1,1,1v5A1,1,0,0,1,14.5,15.5Zm-1.139-2.846-0.646-.646,0.646-.646-0.008-.008A0.5,0.5,0,0,0,13,10.5a0.491,0.491,0,0,0-.386.194l-0.606.606-0.646-.646,0,0a0.5,0.5,0,1,0-.7.7l0,0L11.3,12.008l-0.606.606A0.491,0.491,0,0,0,10.5,13a0.5,0.5,0,0,0,.854.354l0.008,0.008,0.646-.646,0.646,0.646,0,0a0.5,0.5,0,0,0,.7-0.7Z"/>
+</svg>
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/urlbar.css b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/urlbar.css
new file mode 100755
index 0000000..0316c8d
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/urlbar/urlbar.css
@@ -0,0 +1,814 @@
+.urlbarView-body-inner
+{
+	border: 0 !important;
+}
+
+.urlbarView-row
+{
+	padding: 0 !important;
+	border-radius: 0 !important;
+}
+
+.urlbarView-row-inner
+{
+	align-items: center !important;
+	position: relative !important;
+	padding: 12px 0 !important;
+	padding-inline: 48px 16px !important;
+	max-width: 100% !important;
+	width: auto !important;
+}
+
+.urlbarView-no-wrap
+{
+	height: 100% !important;
+	align-items: center !important;
+	margin-inline-start: 0 !important;
+	max-width: 100% !important;
+}
+
+.urlbarView-row[has-url][type]:not([type=bookmark]) .urlbarView-row-inner
+{
+	flex-direction: column !important;
+	align-items: flex-start !important;
+}
+
+.urlbarView-row[has-url][type]:not([type=bookmark]) .urlbarView-no-wrap
+{
+	height: auto !important;
+	align-self: flex-start !important;
+	width: 100% !important;
+}
+
+.urlbarView-title-separator
+{
+	visibility: visible !important;
+}
+
+.urlbarView-title-separator::before
+{
+	content: "-" !important;
+	color: inherit !important;
+}
+
+.urlbarView-title-separator,
+.urlbarView-action
+{
+	opacity: .6 !important;
+	color: inherit !important;
+	font-size: revert !important;
+}
+
+.urlbarView-favicon
+{
+	position: absolute !important;
+	left: 16px !important;
+	top: 50% !important;
+	transform: translateY(-50%) !important;
+	fill-opacity: .6 !important;
+	flex: unset !important;
+	margin: unset !important;
+}
+
+.urlbarView-title,
+.urlbarView-url
+{
+	text-overflow: ellipsis !important;
+	mask-image: none !important;
+}
+
+.urlbarView-title
+{
+	flex: 1 !important;
+	flex-basis: unset !important;
+}
+
+.urlbarView-url
+{
+	max-width: 100% !important;
+	font-size: revert !important;
+	color: var(--urlbar-popup-url-color) !important;
+	padding: 0 !important;
+}
+
+.urlbarView-url:not(:empty)
+{
+	visibility: visible !important;
+}
+
+.urlbarView-action:not(:empty)
+{
+    display: flex !important;
+}
+
+.urlbarView-row[type=search] ~ .urlbarView-row[type=search] .urlbarView-title-separator,
+.urlbarView-row[type=search] ~ .urlbarView-row[type=search] .urlbarView-action,
+.urlbarView-row[has-url][type]:not([type=bookmark]) .urlbarView-title-separator,
+.urlbarView-type-icon
+{
+	display: none !important;
+}
+
+.urlbarView-row[type=tip] > .urlbarView-row-inner > .urlbarView-favicon,
+.urlbarView-row[type=tip] > .urlbarView-row-inner > .urlbarView-title,
+.urlbarView-tip-button,
+.urlbarView-tip-help
+{
+	margin-block-end: 0 !important;
+}
+
+.urlbarView .search-one-offs
+{
+	padding: 4px !important;
+	display: flex !important;
+}
+
+.urlbarView .search-panel-one-offs-header
+{
+	margin-inline-start: 12px !important;
+	line-height: 40px !important;
+	transform: translateY(-1px) !important;
+}
+
+.urlbarView .search-panel-one-offs
+{
+	border-radius: 0 0 8px 8px !important;
+	display: inline-flex !important;
+	flex-direction: row !important;
+	padding: 0 !important;
+	margin: 0 !important;
+	float: left !important;
+	max-width: none !important;
+	height: auto !important;
+}
+
+.urlbarView .searchbar-engine-one-off-item
+{
+	height: 32px !important;
+	border-radius: 16px !important;
+	padding: 0 8px !important;
+	margin: 4px !important;
+	background-image: none !important;
+	color: inherit !important;
+	border: 0 !important;
+}
+
+#urlbar-container
+{
+	max-height: 36px !important;
+}
+
+#urlbar,
+#searchbar
+{
+	font-size: var(--toolbar-field-fontsize) !important;
+	border-radius: 99px !important;
+	background-color: var(--toolbar-field-background-color) !important;
+	border: 0 !important;
+	box-shadow: none !important;
+	padding: 2px !important;
+	background-clip: padding-box !important;
+	transition: background-color .1s var(--ease-basic) !important;
+	-moz-box-align: center !important;
+	display: block !important;
+	margin: 0 !important;
+	top: 0 !important;
+	color: inherit !important;
+	
+	--urlbar-height: 28px !important;
+	
+	min-height: var(--urlbar-height) !important;
+}
+
+#urlbar:not(:-moz-lwtheme),
+#searchbar:not(:-moz-lwtheme)
+{
+	color: inherit !important;
+}
+
+#urlbar:hover,
+#searchbar:hover
+{
+	background-color: var(--toolbar-field-hover-background-color) !important;
+}
+
+#urlbar[focused],
+#urlbar[open],
+#searchbar[focused],
+#searchbar[open]
+{
+	background-color: var(--toolbar-field-focus-background-color) !important;
+}
+
+#urlbar[focused],
+#searchbar[focused]
+{
+	/* -moz-accent-color won't work in a var... */
+	border: 2px solid -moz-accent-color !important;
+	padding: 0 !important;
+	margin: 0 !important;
+}
+
+/* if only focused, don't break out the urlbar */
+#urlbar[focused]:not([open])
+{
+	width: 100% !important;
+	left: 0 !important;
+	right: 0 !important;
+}
+
+#urlbar[open],
+#searchbar[open]
+{
+	border: 0 !important;
+	padding: 0 !important;
+	border-radius: 8px !important;
+	min-height: 37px !important;
+	margin: -4px 0 -1px !important;
+	box-shadow:
+		0 5px 5px -3px rgba(0,0,0,.2),
+		0 8px 10px 1px rgba(0,0,0,.14),
+		0 3px 14px 2px rgba(0,0,0,.12) !important;
+	z-index: 99999 !important;
+}
+
+/* 71+ */
+#urlbar-background
+{
+	display: none !important;
+}
+
+#urlbar-input-container,
+#searchbar
+{
+	display: flex !important;
+	align-items: center !important;
+}
+
+#urlbar-input-container
+{
+	height: auto !important;
+	padding: 0 !important;
+	border: 0 !important;
+}
+
+.urlbar-input-box,
+.searchbar-textbox
+{
+	flex: 1 !important;
+}
+
+#urlbar-input
+{
+	width: 100% !important;
+}
+
+#urlbar-input,
+.searchbar-textbox
+{
+	color: var(--lwt-toolbar-field-color) !important;
+}
+
+#urlbar-input::placeholder,
+.searchbar-textbox::placeholder
+{
+	opacity: .66 !important;
+}
+
+#urlbar[open] #urlbar-input-container
+{
+	height: 37px !important;
+}
+
+#urlbar[open] #identity-box
+{
+	margin-inline-start: 8px !important;
+	margin-inline-end: 8px !important;
+}
+
+.urlbarView
+{
+	top: 0 !important;
+	left: 0 !important;
+	right: 0 !important;
+	position: relative !important;
+	box-shadow: none !important;
+	border: 0 !important;
+	background: 0 !important;
+	margin: 0 !important;
+}
+
+#urlbar[open] .urlbarView
+{
+	display: block !important;
+	width: 100% !important;
+}
+
+#urlbar-results
+{
+	padding: 0 !important;
+}
+
+#urlbar-container,
+#searchbar-container
+{
+	overflow: visible !important;
+	padding: 0 !important;
+	margin-inline: 6px !important;
+}
+
+#identity-icon
+{
+	fill-opacity: 1 !important;
+}
+
+#identity-icon-labels
+{
+	color: inherit !important;
+	opacity: 1 !important;
+	padding: 0 !important;
+	margin-inline-start: 8px !important;
+	margin-block-start: -1px !important;
+	display: none !important;
+	align-items: center !important;
+}
+
+#identity-box
+{
+	display: flex !important;
+	position: relative !important;
+}
+
+.identity-box-button,
+#tracking-protection-icon-container
+{
+	background: none !important;
+}
+
+#identity-icon-box
+{
+	border: 0 !important;
+	padding: 0 !important;
+}
+
+/* separator */
+#urlbar[pageproxystate=valid] #identity-box:-moz-any(.chromeUI, .extensionPage, .notSecureText) #identity-icon-box::after
+{
+	content: "" !important;
+	display: block !important;
+	position: absolute !important;
+	height: 16px !important;
+	top: calc(50% - 8px) !important;
+	right: 0 !important;
+	background: #9d9e9f !important;
+	width: 1px !important;
+	transition: opacity .2s var(--ease-basic) !important;
+}
+
+
+#urlbar[pageproxystate=valid] #identity-box:-moz-any(.chromeUI, .extensionPage, .notSecureText)
+{
+	margin-inline-end: 8px !important;
+}
+
+
+#urlbar[pageproxystate=valid] #identity-box:hover  #identity-icon-box::after
+{
+	opacity: 0 !important;
+}
+
+#urlbar[pageproxystate=valid] #identity-box.extensionPage > #identity-icon
+{
+	list-style-image: none !important;
+	margin-inline-end: -16px !important;
+}
+
+#identity-icon-label
+{
+	margin: 0 !important;
+	padding-inline-start: 8px !important;
+	transform: translateY(-1px) !important;
+	
+}
+
+/* no longer used as a connection icon */
+#connection-icon
+{
+	display: none !important;
+}
+
+#urlbar[pageproxystate=valid] #identity-box:-moz-any(
+	.mixedActiveBlocked,
+	.mixedDisplayContentLoadedActiveBlocked,
+	.mixedActiveContent
+) > #connection-icon
+{
+	display: -moz-box !important;
+}
+
+#urlbar[pageproxystate=valid] #identity-box:-moz-any(.weakCipher, .certUserOverridden, .certErrorPage, .insecureLoginForms, .mixedActiveContent)
+{
+	color: #c94031 !important;
+}
+
+.urlbar-input
+{
+	padding: 0 !important;
+}
+
+.searchbar-textbox
+{
+	border: 0 !important;
+	background: 0 !important;
+	box-shadow: none !important;
+	margin: 0 !important;
+	-moz-appearance: none !important;
+}
+
+.searchbar-search-icon
+{
+	margin: 0 !important;
+	fill-opacity: 1 !important;
+}
+
+.searchbar-search-icon-overlay
+{
+	margin-inline-start: -11px !important;
+	margin-inline-end: 0 !important;
+	
+	/* not really useful anyway */
+	display: none !important;
+}
+
+#page-action-buttons > #pageActionSeparator,
+.urlbar-history-dropmarker
+{
+	display: none !important;
+}
+
+#pageActionSeparator
+{
+	height: 24px !important;
+}
+
+.urlbar-icon-wrapper
+{
+	background: 0 !important;
+}
+
+.urlbar-icon,
+#page-action-buttons > toolbarbutton,
+.searchbar-search-button,
+#identity-box,
+#tracking-protection-icon-box
+{
+	min-width: 32px !important;
+	height: 24px !important;
+	padding: 4px 8px !important;
+	margin: 0 !important;
+	border: 0 !important;
+	border-radius: 99px !important;
+	fill-opacity: 1 !important;
+	transition: background-color .2s var(--ease-basic) !important;
+	background: 0 !important;
+	background-color: transparent !important;
+}
+
+#tracking-protection-icon-container
+{
+	background: 0 !important;
+	border: 0 !important;
+	margin: 0 !important;
+	padding: 0 !important;
+	width: auto !important;
+	order: 97 !important;
+}
+
+#tracking-protection-icon-box
+{
+	display: block !important;
+	width: 32px !important;
+}
+
+#tracking-protection-icon-box
+{
+	transition:
+		background-color .2s var(--ease-basic),
+		width .15s var(--ease-in),
+		opacity .15s var(--ease-basic),
+		visibility 0s .2s !important;
+	opacity: 0 !important;
+	visibility: hidden !important;
+	width: 0 !important;
+}
+
+#urlbar:hover #tracking-protection-icon-box,
+#tracking-protection-icon-container[open] #tracking-protection-icon-box,
+#tracking-protection-icon-box[active]
+{
+	transition:
+		background-color .2s var(--ease-basic),
+		width .3s var(--ease-out),
+		opacity .3s var(--ease-basic) !important;
+	opacity: 1 !important;
+	visibility: visible !important;
+	width: 32px !important;
+}
+
+#tracking-protection-icon
+{
+	display: block !important;
+}
+
+#tracking-protection-icon-animatable-box
+{
+	display: none !important;
+}
+
+#identity-box #notification-popup-box
+{
+	padding: 0 !important;
+	margin: 0 !important;
+}
+
+#page-action-buttons
+{
+	height: 24px !important;
+	display: flex !important;
+	order: 99 !important;
+}
+
+#urlbar[open] #page-action-buttons,
+#urlbar[open] #tracking-protection-icon-container
+{
+	display: none !important;
+}
+
+.urlbar-icon:hover:not([disabled]),
+#page-action-buttons > toolbarbutton:hover:not([disabled]),
+.searchbar-search-button:hover,
+#identity-box:hover:not(.no-hover),
+#tracking-protection-icon-container:hover #tracking-protection-icon-box
+{
+	background-color: var(--toolbarbutton-hover-background) !important;
+}
+
+.urlbar-icon:hover:active:not([disabled]),
+.urlbar-icon[open],
+#page-action-buttons > toolbarbutton:hover:active:not([disabled]),
+.searchbar-search-button:hover:active,
+#identity-box:hover:active:not(.no-hover),
+#identity-box[open]:not(.no-hover),
+#tracking-protection-icon-container[open] #tracking-protection-icon-box
+{
+	background-color: var(--toolbarbutton-active-background) !important;
+	transition-duration: 0 !important;
+}
+
+#contextual-feature-recommendation
+{
+	width: auto !important;
+}
+
+.urlbar-page-action:-moz-any(
+	#reader-mode-button,
+	#pageActionButton,
+	#pocket-button-box,
+	#pageAction-urlbar-screenshots_mozilla_org,
+	#pageAction-urlbar-sendToDevice,
+	#pageAction-urlbar-emailLink,
+	#pageAction-urlbar-copyURL,
+	#pageAction-urlbar-shareURL,
+	#pageAction-urlbar-addSearchEngine
+)
+{
+	transition:
+		background-color .2s var(--ease-basic),
+		margin .15s var(--ease-in),
+		opacity .15s var(--ease-basic),
+		visibility 0s 4s !important;
+	opacity: 0 !important;
+	visibility: hidden !important;
+	margin-inline-start: -32px !important;
+}
+
+#urlbar:hover .urlbar-page-action:-moz-any(
+	#reader-mode-button,
+	#pageActionButton,
+	#pocket-button-box,
+	#pageAction-urlbar-screenshots_mozilla_org,
+	#pageAction-urlbar-sendToDevice,
+	#pageAction-urlbar-emailLink,
+	#pageAction-urlbar-copyURL,
+	#pageAction-urlbar-shareURL,
+	#pageAction-urlbar-addSearchEngine
+),
+.urlbar-page-action:-moz-any(
+	#reader-mode-button,
+	#pageActionButton,
+	#pocket-button-box,
+	#pageAction-urlbar-screenshots_mozilla_org,
+	#pageAction-urlbar-sendToDevice,
+	#pageAction-urlbar-emailLink,
+	#pageAction-urlbar-copyURL,
+	#pageAction-urlbar-shareURL,
+	#pageAction-urlbar-addSearchEngine
+):-moz-any(:hover, [open], [readeractive]),
+.urlbar-page-action:-moz-any(:hover, [open]) ~ .urlbar-page-action:-moz-any(
+	#reader-mode-button,
+	#pageActionButton,
+	#pocket-button-box,
+	#pageAction-urlbar-screenshots_mozilla_org,
+	#pageAction-urlbar-sendToDevice,
+	#pageAction-urlbar-emailLink,
+	#pageAction-urlbar-copyURL,
+	#pageAction-urlbar-shareURL,
+	#pageAction-urlbar-addSearchEngine),
+#tracking-protection-icon-container[open] ~ #page-action-buttons .urlbar-page-action:-moz-any(
+	#reader-mode-button,
+	#pageActionButton,
+	#pocket-button-box,
+	#pageAction-urlbar-screenshots_mozilla_org,
+	#pageAction-urlbar-sendToDevice,
+	#pageAction-urlbar-emailLink,
+	#pageAction-urlbar-copyURL,
+	#pageAction-urlbar-shareURL,
+	#pageAction-urlbar-addSearchEngine)
+{
+	transition:
+		background-color .2s var(--ease-basic),
+		margin .3s var(--ease-out),
+		opacity .3s var(--ease-basic) !important;
+	opacity: 1 !important;
+	visibility: visible !important;
+	margin-inline-start: 0 !important;
+}
+
+.urlbar-display
+{
+	color: inherit !important;
+	margin: 0 !important;
+}
+
+#pocket-button-box[animate] > #pocket-animatable-box
+{
+	margin-inline-start: 6px !important;
+}
+
+#star-button-animatable-box
+{
+	display: none !important;
+}
+
+#userContext-icons
+{
+	flex-direction: row !important;
+	align-items: center !important;
+	justify-content: center !important;
+	position: relative !important;
+	padding-inline: 8px !important;
+	margin: 0 !important;
+	color: var(--identity-tab-color) !important;
+}
+
+#userContext-icons:not([hidden])
+{
+	display: flex !important;
+}
+
+#userContext-icons > *
+{
+	z-index: 1 !important;
+	color: inherit !important;
+	fill: currentColor !important;
+}
+
+#userContext-label
+{
+	margin: 0 !important;
+}
+
+#userContext-label + #userContext-indicator
+{
+	margin-inline-start: 6px !important;
+}
+
+#userContext-icons::before
+{
+	content: '' !important;
+	position: absolute !important;
+	left: 0 !important;
+	right: 0 !important;
+	top: 0 !important;
+	bottom: 0 !important;
+	background: var(--identity-tab-color) !important;
+	border-radius: 99px !important;
+	opacity: .1 !important;
+}
+
+#cfr-label-container
+{
+	background: 0 !important;
+}
+
+#cfr-label
+{
+	display: none !important;
+}
+
+#cfr-button
+{
+	fill: currentColor !important;
+}
+
+#urlbar[open] #urlbar-go-button,
+#searchbar[open] #search-go-button,
+/* < 71 */
+#urlbar[open] .urlbar-go-button,
+#searchbar[open] .search-go-button
+{
+	height: 32px !important;
+}
+
+#urlbar-go-button,
+#search-go-button,
+/* < 71 */
+.urlbar-go-button,
+.search-go-button
+{
+	list-style-image: url(go.svg) !important;
+}
+
+.search-go-container
+{
+	display: flex !important;
+}
+
+#searchbar .textbox-input
+{
+	padding: 0 !important;
+}
+
+/* lock (secure) */
+#identity-box[pageproxystate="valid"].verifiedDomain #identity-icon,
+#identity-box[pageproxystate="valid"].mixedActiveBlocked #identity-icon
+{
+	list-style-image: var(--lock-icon) !important;
+}
+
+/* warning (dangerous) */
+#identity-box[pageproxystate="valid"].notSecure #identity-icon,
+#identity-box[pageproxystate="valid"].mixedActiveContent #identity-icon,
+#identity-box[pageproxystate="valid"].httpsOnlyErrorPage #identity-icon,
+#identity-box[pageproxystate="valid"].weakCipher #identity-icon,
+#identity-box[pageproxystate="valid"].mixedDisplayContent #identity-icon,
+#identity-box[pageproxystate="valid"].mixedDisplayContentLoadedActiveBlocked #identity-icon,
+#identity-box[pageproxystate="valid"].certUserOverridden #identity-icon,
+#identity-box[pageproxystate="valid"].certErrorPage #identity-icon
+{
+    list-style-image: var(--warning-icon) !important;
+}
+
+#urlbar-input,
+#searchbar-input,
+.searchbar-textbox /* < 71 */
+{
+	transform: translateY(var(--input-offset)) !important;
+	line-height: 1.745em !important;
+}
+
+/* 1x */
+#urlbar
+{
+	--info-icon: url(info-1x.svg);
+	--lock-icon: url(connection-1x.svg);
+	--warning-icon: url(connection-warning-1x.svg);
+}
+
+#urlbar,
+#searchbar
+{
+	--input-offset: -1px;
+}
+
+/* 2x */
+@media (min--moz-device-pixel-ratio: 2)
+{
+	#urlbar
+	{
+		--info-icon: url(info-2x.svg);
+		--lock-icon: url(connection-2x.svg);
+		--warning-icon: url(connection-warning-2x.svg);
+	}
+	
+	#urlbar,
+	#searchbar
+	{
+		--input-offset: -.5px;
+	}
+	
+	#identity-icon-labels
+	{
+		padding-inline-end: .5px !important;
+	}
+}
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/chrome/userChrome.css b/testdata/home/.cache/ffcss/materialfox/_/chrome/userChrome.css
new file mode 100755
index 0000000..af26b60
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/chrome/userChrome.css
@@ -0,0 +1,9 @@
+@import "global/variables.css";
+@import "global/global.css";
+@import "icons/icons.css";
+@import "tabbar/tabbar.css";
+@import "navbar/navbar.css";
+@import "personalbar/personalbar.css";
+@import "popup/popup.css";
+@import "urlbar/urlbar.css";
+@import "findbar/findbar.css";
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/materialfox/_/user.js b/testdata/home/.cache/ffcss/materialfox/_/user.js
new file mode 100644
index 0000000..5ad2958
--- /dev/null
+++ b/testdata/home/.cache/ffcss/materialfox/_/user.js
@@ -0,0 +1,11 @@
+/*** MaterialFox ***/
+/**  Mandatory	  **/
+user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true); // default is false
+user_pref("svg.context-properties.content.enabled", true); 				// default is false
+
+/**  Recommended  (uncomment to apply) **/
+/* Replicate Chrome behaviour for clipped tabs */
+//user_pref("browser.tabs.tabClipWidth", 83); 							// default is 140
+
+/* Replicate Chrome's "Not Secure" text on HTTP */
+//user_pref("security.insecure_connection_text.enabled", true);
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/.github/FUNDING.yml b/testdata/home/.cache/ffcss/simplerentfox/_/.github/FUNDING.yml
new file mode 100644
index 0000000..b6b4873
--- /dev/null
+++ b/testdata/home/.cache/ffcss/simplerentfox/_/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+# These are supported funding model platforms
+ko_fi: miguelravila
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Images/Header.png b/testdata/home/.cache/ffcss/simplerentfox/_/Images/Header.png
new file mode 100644
index 0000000..74b2846
Binary files /dev/null and b/testdata/home/.cache/ffcss/simplerentfox/_/Images/Header.png differ
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Images/OneLine.png b/testdata/home/.cache/ffcss/simplerentfox/_/Images/OneLine.png
new file mode 100644
index 0000000..0b25821
Binary files /dev/null and b/testdata/home/.cache/ffcss/simplerentfox/_/Images/OneLine.png differ
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Images/SimplerentFoxVersions.png b/testdata/home/.cache/ffcss/simplerentfox/_/Images/SimplerentFoxVersions.png
new file mode 100644
index 0000000..3ff3880
Binary files /dev/null and b/testdata/home/.cache/ffcss/simplerentfox/_/Images/SimplerentFoxVersions.png differ
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Images/Windows.png b/testdata/home/.cache/ffcss/simplerentfox/_/Images/Windows.png
new file mode 100644
index 0000000..c63d4ee
Binary files /dev/null and b/testdata/home/.cache/ffcss/simplerentfox/_/Images/Windows.png differ
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Images/WithURLBar.png b/testdata/home/.cache/ffcss/simplerentfox/_/Images/WithURLBar.png
new file mode 100644
index 0000000..7e16df9
Binary files /dev/null and b/testdata/home/.cache/ffcss/simplerentfox/_/Images/WithURLBar.png differ
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Images/WithoutURLBar.png b/testdata/home/.cache/ffcss/simplerentfox/_/Images/WithoutURLBar.png
new file mode 100644
index 0000000..c38d653
Binary files /dev/null and b/testdata/home/.cache/ffcss/simplerentfox/_/Images/WithoutURLBar.png differ
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Linux/README.md b/testdata/home/.cache/ffcss/simplerentfox/_/Linux/README.md
new file mode 100644
index 0000000..f198b06
--- /dev/null
+++ b/testdata/home/.cache/ffcss/simplerentfox/_/Linux/README.md
@@ -0,0 +1,22 @@
+<div align="center">
+<h1>SimplerentFox</h1>
+<b>🦊 Linux Version</b>
+</div>
+
+![](../Images/Header.png)
+
+## Installation
+
+1. In the searchbar type `about:config`. A dialog will be shown to you. Press the **I accept the risk** button.
+2. Search for **`toolkit.legacyUserProfileCustomizations.stylesheets`**, **`layers.acceleration.force-enabled`**, **`gfx.webrender.all`**, and **`svg.context-properties.content.enabled`**. Change them to **True**
+3. Go to your Firefox profile: `$HOME/.mozilla/firefox/XXXXXXX.default-release/`
+4. Create a folder and name it **`chrome`** (with lowercase).
+5. You can choose between the three versions: **HideURLBar**, **WithURLBar** and **OneLine** (You can see them here: [Versions](#three-versions)). Once you choose your version  and rename the file you choose into `userChrome.css` 
+6. Then paste the `userChrome.css` and the `userContent.css` files into the folder
+7. (Optional) If you don't know if use a theme or not, you can use the Add-on theme I made for a better experience: [Add-on Theme](https://addons.mozilla.org/en-US/firefox/addon/simplerentfox/)
+ 
+> E N J O Y 🦊
+
+### Linux considerations
+
+Firefox uses the GTK3 theme you have to design the menus and the colors. The theme in the screenshots will not look like yours. It can also be useful if you don't want to change a lot of code in the URL-Bar or menus. You can find a lot of GTK themes [here](https://www.pling.com/browse/cat/135/order/latest/)
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Linux/userChrome__HideURLBar.css b/testdata/home/.cache/ffcss/simplerentfox/_/Linux/userChrome__HideURLBar.css
new file mode 100644
index 0000000..4e09845
--- /dev/null
+++ b/testdata/home/.cache/ffcss/simplerentfox/_/Linux/userChrome__HideURLBar.css
@@ -0,0 +1,263 @@
+/* 
+  ___ _            _                 _   
+ / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
+ \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
+ |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
+ | __|____ __|_|                         
+ | _/ _ \ \ /                            
+ |_|\___/_\_\                            
+                                         
+ Hide URL Bar Version
+
+*/
+
+/* 
+
+V A R I A B L E S 
+
+Here you can change the opacity level and the main color of the browser. Also the color of the selections (When you select text)
+
+*/
+
+:root {
+    --srf-color-primary: rgba(20, 20, 20, 0.6);
+    --srf-color-selection: #66a6d9;
+}
+
+/* 
+G E N E R A L 
+*/
+
+window,
+#main-window,
+#toolbar-menubar,
+#TabsToolbar,
+#PersonalToolbar,
+#navigator-toolbox,
+#sidebar-box,
+#nav-bar {
+    background-color: var(--srf-color-primary) !important;
+    -moz-appearance: none !important;
+    background-image: none !important;
+    border: none !important;
+    box-shadow: none !important;
+}
+
+::selection {
+    background-color: var(--srf-color-selection);
+    /* Text color of your selection */
+    color: #141414;
+}
+
+/* 
+T A B S 
+*/
+
+:root {
+    --tabs-border: transparent !important;
+}
+
+.tab-background {
+    border: none !important;
+}
+
+.tab-background[selected='true'] {
+    -moz-appearance: none !important;
+    background-image: none !important;
+}
+
+.tabbrowser-tabs {
+    border: none !important;
+    opacity: 0 !important;
+}
+
+.tabbrowser-tab::before,
+.tabbrowser-tab::after {
+    opacity: 0 !important;
+    border-left: none !important;
+}
+
+.titlebar-placeholder {
+    border: none !important;
+}
+
+.tab-line {
+    display: none !important;
+}
+
+/* 
+
+B U T T O N S 
+
+Here you can comment if you want your buttons to be showed up!
+
+*/
+
+/* This one is the forward button  */
+#forward-button {
+    display: none !important;
+}
+
+/* This one is the back button  */
+#back-button {
+    display: none !important;
+}
+
+/* This one is the spacer between tabs and border  */
+.titlebar-spacer {
+    display: none !important;
+}
+
+/* This one is the "shield" icon  */
+#tracking-protection-icon-container {
+    display: none !important;
+}
+
+/* This one is the hamburger menu!  */
+#PanelUI-button {
+    display: none !important;
+}
+
+/* This one removes the close button  */
+.tabbrowser-tab:not([pinned]) .tab-close-button {
+    display: none !important;
+}
+
+/* This one clears the tabs icon when they're not pinned  */
+.tabbrowser-tab:not([pinned]) .tab-icon-image {
+    display: none !important;
+}
+
+/* Removing the zoom button  */
+#urlbar-zoom-button {
+    display: none !important;
+}
+
+/* Page action (right three dots button)  */
+#pageActionButton {
+    display: none !important;
+}
+
+/* 
+N A V B A R
+*/
+
+#navigator-toolbox::after {
+    border-bottom: 0px !important;
+    border-top: 0px !important;
+}
+
+#nav-bar {
+    border: none !important;
+    box-shadow: none !important;
+    margin-top: 0px !important;
+    border-top-width: 0px !important;
+    margin-bottom: 0px !important;
+    border-bottom-width: 0px !important;
+}
+
+/* 
+S I D E - B A R
+*/
+
+#history-panel,
+#sidebar-search-container,
+#bookmarksPanel {
+    background: var(--srf-color-primary) !important;
+}
+
+#search-box {
+    -moz-appearance: none !important;
+    background: var(--srf-color-primary) !important;
+    border-radius: 6px !important;
+}
+
+#sidebar-icon {
+    display: none !important;
+}
+
+.sidebar-placesTree {
+    color: #eee !important;
+}
+
+#sidebar-switcher-target {
+    color: #eee !important;
+}
+
+#sidebar-header {
+    background: var(--srf-color-primary) !important;
+}
+
+#sidebar-splitter {
+    border: none !important;
+    opacity: 1 !important;
+    background-color: var(--srf-color-primary) !important;
+}
+
+splitter#sidebar-splitter {
+    border: none !important;
+}
+
+/* 
+U R L - B A R
+*/
+
+#urlbar[breakout] {
+    margin-inline-start: 0px !important;
+    width: 100% !important;
+    left: 0 !important;
+    top: calc(
+        (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
+    ) !important;
+}
+
+@-moz-document url(chrome://browser/content/browser.xhtml)
+{
+    #urlbar[breakout][breakout-extend] > #urlbar-background {
+        animation-name: none !important;
+        box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05) !important;
+    }
+}
+
+#urlbar[breakout]:not([open]) {
+    bottom: calc(
+        (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
+    ) !important;
+}
+
+#urlbar-background {
+    background: transparent !important;
+    border: transparent !important;
+    animation: none !important;
+}
+
+#urlbar-input-container {
+    padding: 0 !important;
+    height: 100% !important;
+}
+
+#identity-box {
+    display: none;
+}
+
+#urlbar .urlbarView {
+    display: none !important;
+}
+
+#searchbar {
+    background: var(--bf-bg) !important;
+    border: none !important;
+}
+
+.searchbar-textbox {
+    color: var(--bf-focus-fg) !important;
+    font-weight: 700 !important;
+}
+
+#urlbar-label-box, #urlbar-search-mode-indicator {
+    display: none !important;
+}
+
+/* 
+Designed and Coded by Miguel R. Ávila: https://github.com/MiguelRAvila
+*/
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Linux/userChrome__OneLine.css b/testdata/home/.cache/ffcss/simplerentfox/_/Linux/userChrome__OneLine.css
new file mode 100644
index 0000000..01083cf
--- /dev/null
+++ b/testdata/home/.cache/ffcss/simplerentfox/_/Linux/userChrome__OneLine.css
@@ -0,0 +1,335 @@
+/* 
+  ___ _            _                 _   
+ / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
+ \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
+ |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
+ | __|____ __|_|                         
+ | _/ _ \ \ /                            
+ |_|\___/_\_\                            
+                                         
+ One Line Version
+
+*/
+ 
+/* 
+
+V A R I A B L E S 
+
+Here you can change the opacity level and the main color of the browser. Also the color of the selections (When you select text)
+
+*/
+
+:root {
+    --urlbar-width:25vw;
+    --srf-color-primary: rgba(20, 20, 20, 0.6);
+    --srf-color-selection: #66a6d9;
+}
+
+/* 
+G E N E R A L 
+*/
+
+window,
+#main-window,
+#toolbar-menubar,
+#TabsToolbar,
+#PersonalToolbar,
+#navigator-toolbox,
+#sidebar-box,
+#nav-bar {
+    background-color: var(--srf-color-primary) !important;
+    -moz-appearance: none !important;
+    background-image: none !important;
+    border: none !important;
+    box-shadow: none !important;
+}
+
+::selection {
+    background-color: var(--srf-color-selection);
+    /* Text color of your selection */
+    color: #141414;
+}
+
+/* 
+T A B S 
+*/
+
+:root {
+    --tabs-border: transparent !important;
+    --toolbox-border-bottom-color: transparent !important;
+}
+
+#navigator-toolbox {
+    border: 0px !important;
+}
+
+#TabsToolbar {
+    margin-top: -1px !important;
+    margin-bottom: 1px !important;
+    margin-left: var(--urlbar-width) !important;
+    max-height: 32px !important;
+}
+
+.tab-background {
+    border: none !important;
+}
+
+.tab-background[selected='true'] {
+    -moz-appearance: none !important;
+    background-image: none !important;
+}
+
+.tabbrowser-tabs {
+    border: none !important;
+    opacity: 0 !important;
+}
+
+.tabbrowser-tab::before {
+    opacity: 0 !important;
+    border-left: none !important;
+}
+
+.tabbrowser-tab::after {
+    opacity: 0 !important;
+    border-left: none !important;
+}
+
+.titlebar-placeholder {
+    border: none !important;
+}
+
+.tab-line {
+    display: none !important;
+}
+
+.tabbrowser-arrowscrollbox {
+    height: 32px !important;
+    min-height: 32px !important;
+}
+
+.titlebar-placeholder[type='pre-tabs'] {
+    display: none !important;
+}
+
+/* 
+
+B U T T O N S 
+
+Here you can comment if you want your buttons to be showed up!
+
+*/
+
+/* This one is the forward button  */
+#forward-button {
+    display: none !important;
+}
+
+/* This one is the back button  */
+#back-button {
+    display: none !important;
+}
+
+/* This one is the spacer between tabs and border  */
+.titlebar-spacer {
+    display: none !important;
+}
+
+/* This one is the "shield" icon  */
+#tracking-protection-icon-container {
+    display: none !important;
+}
+
+/* This one is the hamburger menu!  */
+#PanelUI-button {
+    display: none !important;
+}
+
+/* This one removes the close button  */
+.tabbrowser-tab:not([pinned]) .tab-close-button {
+    display: none !important;
+}
+
+/* This one clears the tabs icon when they're not pinned  */
+.tabbrowser-tab:not([pinned]) .tab-icon-image {
+    display: none !important;
+}
+
+/* Removing the zoom button  */
+#urlbar-zoom-button {
+    display: none !important;
+}
+
+/* Page action (right three dash button)  */
+#pageActionButton {
+    display: none !important;
+}
+
+/* 
+N A V B A R
+*/
+
+#navigator-toolbox::after {
+    border: 0px !important;
+}
+
+#nav-bar {
+    border: none !important;
+    box-shadow: none !important;
+    border-top-width: 0px !important;
+    border-bottom-width: 0px !important;
+    margin-top: -32px !important;
+    margin-bottom: -1px !important;
+    margin-right: calc(100vw - var(--urlbar-width)) !important;
+    height: 32px !important;
+}
+
+/* 
+S I D E - B A R
+*/
+
+#history-panel,
+#sidebar-search-container,
+#bookmarksPanel {
+    background: var(--srf-color-primary) !important;
+}
+
+#search-box {
+    -moz-appearance: none !important;
+    background: var(--srf-color-primary) !important;
+    border-radius: 6px !important;
+}
+
+#sidebar-icon {
+    display: none !important;
+}
+
+.sidebar-placesTree {
+    color: var(--srf-color-selection) !important;
+}
+
+#sidebar-switcher-target {
+    color: var(--srf-color-selection) !important;
+}
+
+#sidebar-header {
+    background: var(--srf-color-primary) !important;
+}
+
+#sidebar-splitter {
+    border: none !important;
+    opacity: 1 !important;
+    background-color: var(--srf-color-primary) !important;
+}
+
+splitter#sidebar-splitter {
+    border: none !important;
+}
+
+/* 
+U R L - B A R
+*/
+
+#urlbar[breakout] {
+    margin-inline-start: 0px !important;
+    width: 100% !important;
+    left: 0 !important;
+    top: calc(
+        (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
+    ) !important;
+}
+
+@-moz-document url(chrome://browser/content/browser.xhtml)
+{
+    #urlbar[breakout][breakout-extend] > #urlbar-background {
+        animation-name: none !important;
+        box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05) !important;
+    }
+}
+
+#urlbar[breakout]:not([open]) {
+    bottom: calc(
+        (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
+    ) !important;
+}
+
+#urlbar[breakout-extend] {
+    top: calc(
+        (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
+    ) !important;
+    left: calc(
+        (var(--urlbar-toolbar-width) - var(--urlbar-width)) / 2
+    ) !important;
+    width: 100% !important;
+}
+
+#urlbar[breakout-extend] #urlbar-input-container {
+    height: calc(var(--tab-min-height) - 3px) !important;
+    padding: 0px !important;
+}
+
+#urlbar-input-container {
+    padding: 0 !important;
+    height: 100% !important;
+}
+
+#identity-box {
+    display: none;
+}
+
+.urlbarView-row {
+    padding: 0px !important;
+}
+
+#searchbar {
+    background: var(--bf-bg) !important;
+    border: none !important;
+}
+
+.searchbar-textbox {
+    color: var(--bf-focus-fg) !important;
+    font-weight: 700 !important;
+}
+
+#urlbar-container {
+    min-width: 150px !important;
+}
+
+#urlbar {
+    border: none !important;
+    box-shadow: none !important;
+}
+
+#page-action-buttons {
+    display: none !important;
+}
+
+.urlbarView {
+    margin: 0px !important;
+    width: 100% !important;
+}
+
+#urlbar-label-box, #urlbar-search-mode-indicator {
+    display: none !important;
+}
+
+/* 
+If you want to show the URLBar extended you must
+change this two blocks:
+
+In order to show it, you must change the value of "transparent" in the first block 
+and erease the second block. 
+*/
+
+#urlbar-background {
+    background: transparent !important;
+    border: transparent !important;
+    animation: none !important;
+}
+
+#urlbar .urlbarView {
+    display: none !important;
+}
+
+/* 
+Designed and Coded by Miguel R. Ávila: https://github.com/MiguelRAvila
+*/
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Linux/userChrome__WithURLBar.css b/testdata/home/.cache/ffcss/simplerentfox/_/Linux/userChrome__WithURLBar.css
new file mode 100644
index 0000000..610af0a
--- /dev/null
+++ b/testdata/home/.cache/ffcss/simplerentfox/_/Linux/userChrome__WithURLBar.css
@@ -0,0 +1,263 @@
+/* 
+  ___ _            _                 _   
+ / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
+ \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
+ |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
+ | __|____ __|_|                         
+ | _/ _ \ \ /                            
+ |_|\___/_\_\                            
+                                         
+ With URL Bar Version
+
+*/
+
+/* 
+
+V A R I A B L E S 
+
+Here you can change the opacity level and the main color of the browser. Also the color of the selections (When you select text)
+
+*/
+
+:root {
+    --srf-color-primary: rgba(20, 20, 20, 0.6);
+    --srf-color-selection: #66A6D9;
+    --srf-color-urlbar: rgba(20, 20, 20, 0.8);
+}
+
+/* 
+G E N E R A L 
+*/
+
+window,
+#main-window,
+#toolbar-menubar,
+#TabsToolbar,
+#PersonalToolbar,
+#navigator-toolbox,
+#sidebar-box {
+    background-color: var(--srf-color-primary) !important;
+    -moz-appearance: none !important;
+    background-image: none !important;
+    border: none !important;
+    box-shadow: none !important;
+}
+
+::selection {
+    background-color: var(--srf-color-selection);
+    color: #141414;
+}
+
+/* 
+T A B S 
+*/
+
+:root {
+    --tabs-border: transparent !important;
+}
+
+.tab-background {
+    border: none !important;
+}
+
+.tab-background[selected='true'] {
+    -moz-appearance: none !important;
+    background-image: none !important;
+}
+.tabbrowser-tabs {
+    border: none !important;
+    opacity: 0 !important;
+}
+
+.tabbrowser-tab::before, .tabbrowser-tab::after{
+    opacity: 0 !important;
+    border-left: none !important;
+}
+
+.titlebar-placeholder {
+    border: none !important;
+}
+
+.tab-line {
+  display: none !important;
+}
+
+/* 
+
+B U T T O N S 
+
+Here you can comment if you want your buttons to be showed up!
+
+*/
+
+/* This one is the forward button  */
+#forward-button {
+    display: none !important;
+}
+
+/* This one is the back button  */
+#back-button {
+    display: none !important;
+}
+
+/* This one is the spacer between tabs and border  */
+.titlebar-spacer {
+    display: none !important;
+}
+
+/* This one is the "shield" icon  */
+#tracking-protection-icon-container {
+    display: none !important;
+}
+
+/* This one is the hamburger menu!  */
+#PanelUI-button {
+    display: none !important;
+}
+
+/* This one removes the close button  */
+.tabbrowser-tab:not([pinned]) .tab-close-button {
+    display: none !important;
+}
+
+/* This one clears the tabs icon when they're not pinned  */
+.tabbrowser-tab:not([pinned]) .tab-icon-image {
+    display: none !important;
+}
+
+/* Removing the zoom button  */
+#urlbar-zoom-button {
+    display: none !important;
+}
+
+/* Page action (right three dots button)  */
+#pageActionButton {
+    display: none !important;
+}
+
+/* 
+N A V B A R
+*/
+
+#navigator-toolbox::after {
+    border-bottom: 0px !important;
+    border-top: 0px !important;
+}
+
+#nav-bar {
+    border: none !important;
+    box-shadow: none !important;
+    margin-top: 0px !important;
+    border-top-width: 0px !important;
+    margin-bottom: 0px !important;
+    border-bottom-width: 0px !important;
+}
+
+/* 
+S I D E - B A R
+*/
+
+#history-panel,
+#sidebar-search-container,
+#bookmarksPanel {
+    background: var(--srf-color-primary) !important;
+}
+
+#search-box {
+    -moz-appearance: none !important;
+    background: var(--srf-color-primary) !important;
+    border-radius: 6px !important;
+}
+
+#sidebar-search-container {
+    background-color: var(--srf-color-primary) !important;
+}
+
+#sidebar-icon {
+    display: none !important;
+}
+
+.sidebar-placesTree {
+    color: #EEE !important;
+}
+
+#sidebar-switcher-target {
+    color: #EEE !important;
+}
+
+#sidebar-header {
+    background: var(--srf-color-primary) !important;
+}
+
+#sidebar-box {
+    --sidebar-background-color: var(--srf-color-primary) !important;
+}
+
+#sidebar-splitter {
+    border: none !important;
+    opacity: 1 !important;
+    background-color: var(--srf-color-primary) !important;
+}
+
+/* 
+U R L - B A R
+*/
+
+#urlbar[breakout] {
+    margin-inline-start: 0px !important;
+    width: 100% !important;
+    left: 0 !important;
+    top: calc(
+        (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
+    ) !important;
+}
+
+#urlbar[breakout]:not([open]) {
+    bottom: calc(
+        (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
+    ) !important;
+}
+
+#urlbar-background {
+    background: var(--srf-color-urlbar) !important;
+    border: var(--srf-color-urlbar) !important;
+    animation: none !important;
+}
+
+#urlbar-input-container {
+    padding: 0 !important;
+    height: 100% !important;
+}
+
+#identity-box {
+    display: none;
+}
+
+#urlbar-input:focus {
+    color: #fff !important;
+}
+
+.megabar[open='true'] > #urlbar-background {
+    background: #fff !important;
+}
+
+.urlbarView {
+    margin-inline: 0 !important;
+    width: auto !important;
+}
+
+.urlbarView-row {
+    padding: 0 2px !important;
+}
+
+.urlbarView-row-inner {
+    padding-inline-start: 4px !important;
+}
+
+#urlbar-label-box, #urlbar-search-mode-indicator {
+    display: none !important;
+}
+
+/* 
+Designed and Coded by Miguel R. Ávila: https://github.com/MiguelRAvila
+*/
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Linux/userContent.css b/testdata/home/.cache/ffcss/simplerentfox/_/Linux/userContent.css
new file mode 100644
index 0000000..a64ab95
--- /dev/null
+++ b/testdata/home/.cache/ffcss/simplerentfox/_/Linux/userContent.css
@@ -0,0 +1,20 @@
+/* 
+  ___ _            _                 _   
+ / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
+ \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
+ |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
+ | __|____ __|_|                         
+ | _/ _ \ \ /                            
+ |_|\___/_\_\                            
+                                         
+*/
+
+:root{
+	scrollbar-width: none !important;
+}
+
+@-moz-document url(about:privatebrowsing) {
+	:root{
+		scrollbar-width: none !important;
+	}
+}
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/README.md b/testdata/home/.cache/ffcss/simplerentfox/_/README.md
new file mode 100644
index 0000000..628b684
--- /dev/null
+++ b/testdata/home/.cache/ffcss/simplerentfox/_/README.md
@@ -0,0 +1,132 @@
+<div align="center">
+<h1>SimplerentFox</h1>
+<b>🦊 Minimalist, Simple and Keyboard Centered</b>
+</div>
+
+![](Images/Header.png)
+
+## Index
+
+-   [SimplerentFox](#)
+    -   [Features](#features)
+    -   [Installation](#installation)
+    -   [Customization](#customization)
+         - [All Versions](#three-versions)
+         - [Own Colors and Opacity](#your-own-colors-and-opacity)
+         - [Show buttons](#an-element-you-need-doesnt-show-you-can-easily-add-them-again)
+    -   [Usage](#usage)
+    -   [Startpage](#startpage)
+    -   [Ports](#ports)
+
+## Features
+
+-   **As simple as you want**: All code is commented!
+-   **Variables** for custom opacity and colors: [Customization](#customization)
+-   Tab bar, URLBar and Sidebar **transparent**!
+-   **Keyboard** Centered Design! Here you can read about the shortcuts: [Usage](#usage)
+
+## Installation
+
+1. In the searchbar type `about:config`. A dialog will be shown to you. Press the **I accept the risk** button.
+2. Search for **`toolkit.legacyUserProfileCustomizations.stylesheets`**, **`layers.acceleration.force-enabled`**, **`gfx.webrender.all`**, and **`svg.context-properties.content.enabled`**. Change them to **True**
+3. Go to your Firefox profile:
+    - If you're on Linux: `$HOME/.mozilla/firefox/XXXXXXX.default-release/`
+    - If you're on Windows: `C:\Users\<USERNAME>\AppData\Roaming\Mozilla\Firefox\Profiles\XXXXXXX.default-XXXXXX`
+    - If you're on MacOS: `Users/<USERNAME>/Library/Application Support/Firefox/Profiles/XXXXXXX.default-XXXXXXX` 
+4. Create a folder and name it **`chrome`** (with lowercase).
+5. You can choose between the three versions: **HideURLBar**, **WithURLBar** and **OneLine** (You can see them here: [Versions](#three-versions)). Once you choose your version  and rename the file you choose into `userChrome.css`.
+6. Then paste the `userChrome.css` and the `userContent.css` files into the folder.
+7. (Optional) If you don't know if use a theme or not, you can use the Add-on theme I made for a better experience: [Add-on Theme](https://addons.mozilla.org/en-US/firefox/addon/simplerentfox)
+8. Enjoy!
+
+## Customization
+
+-   You can choose between three files `userChrome__HideURLBar`, `userChrome__WithURLBar` and `userChrome__OneLine` . Don't forget to delete the rest of the text in the file name and just have `userChrome`!
+-   You can check the sections (all of them are commented) and make the changes you want!
+-   You can use the variables at the beggining of the file to change the colors!
+
+### Three Versions
+
+SimplerentFox has three central versions:
+
+![](Images/SimplerentFoxVersions.png)
+
+<div align="center">
+<h5>Hide URL Bar | One Line | With URL Bar</h5>
+</div>
+
+### Your own colors and opacity:
+
+In this section of the code you can change the colors, it's in RGBA so you can easily change the opacity and accent color, by default it's `#186efd60`
+
+```css
+
+:root {
+    --srf-color-primary: rgba(20, 20, 20, 0.6);
+    --srf-color-selection: #66a6d9;
+}
+
+```
+
+### An element you need doesn't show? You can easily add them again
+
+In this section you can comment adding `/* */` around the button you want to show.
+
+```css
+
+/* This one is the forward button  */
+#forward-button {
+    display: none !important;
+}
+
+/* This one is the back button  */
+#back-button {
+    display: none !important;
+}
+
+/* And many more... */
+
+```
+
+## Usage
+
+> This userstyle was made for a keyboard centered usage, like Tilling Window Manager users or just cool people that loves them! 🤖
+> In this section I show you some cool shortcuts you can use with this theme for a better experience
+
+-   `Alt` You can access to the global menu for an extended options you need
+-   `Alt + Left Arrow` You can go Back
+-   `Alt + Right Arrow` You can go Forward
+-   `Ctrl + L` focuses the URLBar, which is very useful for quick searches and bookmarks usage
+-   `Ctrl + B` shows you the Bookmarks 
+-   `Ctrl + H` shows you the History Bar
+-   `Ctrl + T` Opens a new Tab
+-   `Ctrl + W` Closes a Tab
+-   `Ctrl + Shift + T` Re-opens a tab that you just closed
+-   `Ctrl + R` Refresh the page you're on
+-   `Ctrl + Shift + A` Quick open for Add-Ons
+
+## Startpage
+
+If anyone has the doubt, in almost all of the screenshots you can see a Startpage made for it. It's called [Bento 🍱](https://github.com/MiguelRAvila/Bento) and It's another project I made. It fits with the Firefox theme because It's done with the same principles: Minimalist, Simple and Elegant.
+
+<p align="center">
+  <img src="https://github.com/MiguelRAvila/Bento/blob/master/assets/preview.gif">
+</p>
+
+In the repo I have all the steps for you to use it!
+
+## Ports
+
+> This userstyle was made in Linux and looking for a simple and clean look for the Firefox Web browser and ports to other OS are on mind.
+
+### Windows port
+
+A Simple and Keyboard Centered userstyle for Windows!
+
+![](https://github.com/MiguelRAvila/SimplerentFox/blob/master/Images/Windows.png)
+
+In addition to the `userChrome.css` and `userContent.css` files you'll also need the `dark_additional_windows.css`, `dark_checkboxes_and_radios.css` and `dark_context_menus.css` files in your **chrome** directory
+
+> ### Transparency Note
+>
+> Transparency in Windows breakes the window behavior. You can change the colors in the variable section of the code and still add transparency, but the window manager will have some bugs with maximized firefox.
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Windows/README.md b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/README.md
new file mode 100644
index 0000000..9c2f25f
--- /dev/null
+++ b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/README.md
@@ -0,0 +1,22 @@
+<div align="center">
+<h1>SimplerentFox</h1>
+<b>🦊 Windows Version</b>
+</div>
+
+![](../Images/Windows.png)
+
+## Installation
+
+1. In the searchbar type `about:config`. A dialog will be shown to you. Press the **I accept the risk**button.
+2. Search for **`toolkit.legacyUserProfileCustomizations.stylesheets`**, **`layers.acceleration.force-enabled`**, **`gfx.webrender.all`**, and **`svg.context-properties.content.enabled`**. Change them to **True**
+3. Go to your Firefox profile: `C:\Users\<USERNAME>\AppData\Roaming\Mozilla\Firefox\Profiles\XXXXXXX.default-XXXXXX`
+4. Create a folder and name it **`chrome`** (with lowercase).
+5. You can choose between the three versions: **HideURLBar**, **WithURLBar** and **OneLine** (You can see them here: [Versions](#three-versions)). Once you choose your version  and rename the file you choose into `userChrome.css` 
+6. Then paste the `userChrome.css` and the `userContent.css` files into the folder
+7. (Optional) If you don't know if use a theme or not, you can use the Add-on theme I made for a better experience: [Add-on Theme](https://addons.mozilla.org/en-US/firefox/addon/simplerentfox/)
+ 
+> E N J O Y 🦊
+
+### Windows considerations
+
+Transparency in Windows breakes the window behavior. You can change the colors in the variable section of the code and still add transparency, but the window manager will have some bugs with maximized firefox.
\ No newline at end of file
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Windows/dark_additional_windows.css b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/dark_additional_windows.css
new file mode 100644
index 0000000..8befbd7
--- /dev/null
+++ b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/dark_additional_windows.css
@@ -0,0 +1,125 @@
+/* Source file https://github.com/MrOtherGuy/firefox-csshacks/tree/master/chrome/dark_additional_windows.css made available under Mozilla Public License v. 2.0
+See the above repository for updates as well as full license text. */
+
+/* Dark library, page-info and bookmark-properties */
+/* Color scheme matches default dark theme */
+
+/* If you are going to open Library in the (places.xhtml) in a tab then this file should also be imported in userContent.css */
+
+@namespace xul url('http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul');
+
+@-moz-document url-prefix(chrome://browser/content/places/),
+  url(chrome://global/content/commonDialog.xhtml),
+  url("chrome://mozapps/content/handling/dialog.xhtml"),
+  url-prefix(chrome://browser/content/pageinfo/),
+  url-prefix("chrome://mozapps/content/downloads")
+{
+  #placesToolbox,
+  #placesView,
+  #placesToolbar,
+  #main-window,
+  #topBar,
+  dialog,
+  tree {
+    -moz-appearance: none !important;
+    background-color: rgb(50, 50, 52) !important;
+    color: rgb(249, 249, 250) !important;
+  }
+
+  #placesToolbar,
+  tree {
+    background: transparent !important;
+  }
+  search-textbox,
+  #searchFilter,
+  menulist,
+  input,
+  textbox,
+  richlistbox,
+  treecol,
+  button,
+  menu,
+  #permList {
+    -moz-appearance: none !important;
+    color: inherit !important;
+  }
+
+  menulist {
+    border: 1px solid graytext;
+    padding-inline-end: 4px;
+  }
+  menulist dropmarker {
+    filter: invert(1);
+  }
+  menulist:hover {
+    border-color: highlight;
+    background-color: rgba(255, 255, 255, 0.1);
+  }
+
+  .menubar-left {
+    fill: currentColor;
+    -moz-context-properties: fill;
+  }
+
+  #searchFilter,
+  search-textbox,
+  input,
+  textbox,
+  richlistbox {
+    background-color: rgba(0, 0, 0, 0.2) !important;
+  }
+  search-textbox > input,
+  #searchFilter > input,
+  input[readonly] {
+    background-color: transparent !important;
+  }
+
+  treecol:not([hideheader]) {
+    height: 24px;
+  }
+  treecol:hover {
+    filter: brightness(160%);
+  }
+  treechildren::-moz-tree-row(multicol, odd),
+  #permList {
+    background-color: rgba(0, 0, 0, 0.2) !important;
+  }
+
+  radiogroup > radio[selected],
+  radiogroup > radio:hover,
+  #permList > .permission:hover,
+  treechildren::-moz-tree-row(hover),
+  button:hover {
+    background-color: rgba(255, 255, 255, 0.1) !important;
+    color: inherit !important;
+  }
+
+  treechildren::-moz-tree-row(selected) {
+    background-color: Highlight !important;
+  }
+  button {
+    padding: 3px;
+  }
+  #placesMenu > menu {
+    border: none !important;
+  }
+
+  search-textbox,
+  input,
+  button {
+    border: 1px solid graytext;
+    border-radius: 3px;
+  }
+  search-textbox,
+  input {
+    padding: 2px;
+  }
+}
+
+/* modal prompts (alert() & friends) */
+
+xul|vbox.paymentDialogContainerFrame,
+xul|vbox.tabmodalprompt-mainContainer {
+  background-color: var(--toolbar-bgcolor) !important;
+  color: var(--lwt-text-color, -moz-dialogtext) !important;
+}
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Windows/dark_checkboxes_and_radios.css b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/dark_checkboxes_and_radios.css
new file mode 100644
index 0000000..cb554f5
--- /dev/null
+++ b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/dark_checkboxes_and_radios.css
@@ -0,0 +1,95 @@
+/* Source file https://github.com/MrOtherGuy/firefox-csshacks/tree/master/chrome/dark_checkboxes_and_radios.css made available under Mozilla Public License v. 2.0
+See the above repository for updates as well as full license text. */
+
+/* Change the appearance of natively styled widgets including radios, checkboxes, buttons and expender "dropdown" arrows */
+/* Their background will be transparent instead of white so they will appear fitting to whatever is on the background */
+/* As such, this might cause a small issue if the background happens to be same color as the border of these buttons or the checkbox-image */
+
+/* In short, these native widgets will be dark on dark theme and light on light theme */
+
+@namespace url('http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul');
+
+#editBMPanel_tagsSelector > richlistitem > image,
+radio > .radio-check,
+checkbox > .checkbox-check {
+  -moz-appearance: none !important;
+  border: 1px solid graytext;
+  border-radius: 2px;
+  height: 1.3em;
+  width: 1.3em;
+  padding: 1px;
+}
+
+#editBMPanel_tagsSelector > richlistitem > image {
+  background-color: var(--toolbar-bgcolor, rgb(50, 50, 52)) !important;
+  margin: 1px !important;
+  border-color: graytext !important;
+  border-radius: 2px !important;
+}
+
+#editBMPanel_tagsSelector > richlistitem:not([disabled]):hover > image,
+radio:not([disabled]):hover > .radio-check,
+checkbox:not([disabled]):hover > .checkbox-check {
+  border-color: highlight !important;
+}
+
+radio > .radio-check {
+  padding: 0;
+  border-radius: 20px;
+}
+
+#editBMPanel_tagsSelector > richlistitem[checked] > image,
+radio[selected] > .radio-check,
+checkbox[checked] > .checkbox-check {
+  list-style-image: url('chrome://global/skin/icons/check.svg');
+  -moz-context-properties: fill;
+  fill: highlight;
+}
+
+radio[selected] > .radio-check {
+  list-style-image: url('chrome://global/skin/in-content/radio.svg');
+}
+
+#editBMPanel_tagsSelector > richlistitem[disabled] > image,
+radio[disabled] > .radio-check,
+checkbox[disabled] > .checkbox-check {
+  fill: graytext !important;
+}
+
+/*Buttons*/
+/* This only applies to natively-ish styled buttons, not the buttons in toolbars */
+
+button {
+  -moz-appearance: none !important;
+  color: inherit !important;
+  background-color: rgba(127, 127, 127, 0.1) !important;
+  border: 1px solid graytext;
+}
+
+button[disabled] {
+  opacity: 0.8;
+}
+
+button:not([disabled]):hover {
+  border-color: highlight;
+  background-color: rgba(0, 0, 0, 0.1) !important;
+  box-shadow: inset 0 0 2px highlight;
+}
+button:not([disabled]):active {
+  box-shadow: inset 0 0 3px 0px black;
+}
+button.searchbar-engine-one-off-item,
+.panel-footer > button {
+  border-width: 1px 0 0 0;
+}
+
+button.panel-button {
+  padding: 2px 2px 1px 2px !important;
+}
+
+#customization-done-button {
+  background-color: #0a84ff !important;
+}
+#customization-done-button:hover {
+  background-color: #0a64df !important;
+}
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Windows/dark_context_menus.css b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/dark_context_menus.css
new file mode 100644
index 0000000..65ae225
--- /dev/null
+++ b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/dark_context_menus.css
@@ -0,0 +1,95 @@
+/* Source file https://github.com/MrOtherGuy/firefox-csshacks/tree/master/chrome/dark_context_menus.css made available under Mozilla Public License v. 2.0
+See the above repository for updates as well as full license text. */
+
+/* Makes context menus dark. Also affects some portions of menu popups */
+/* Uses dark theme colors but does not respect dark theme variables (they wouldn't work on sidebar context menu) */
+
+:root {
+  --uc-menu-bkgnd: rgb(20, 20, 20);
+  --uc-menu-color: rgb(230, 230, 230);
+  --uc-menu-dimmed: rgba(255, 255, 255, 0.1);
+  --uc-menu-disabled: rgb(50, 50, 50);
+}
+panel richlistbox,
+panel tree,
+panel button,
+panel menulist,
+panel textbox,
+panel input,
+menupopup,
+menu,
+menuitem {
+  -moz-appearance: none !important;
+}
+
+menulist,
+menuitem,
+menu {
+  min-height: 1.8em;
+}
+
+panel menulist {
+  border: 1px solid transparent;
+}
+
+panel richlistbox,
+panel tree,
+panel button,
+panel menulist,
+panel textbox,
+panel input,
+panel #searchbar,
+menupopup:not(#BMB_bookmarksPopup),
+#main-menubar > menu > menupopup,
+#context-navigation {
+  color: var(--uc-menu-color) !important;
+  padding: 2px;
+  background-color: var(--uc-menu-bkgnd) !important;
+  border-color: var(--uc-menu-disabled) !important;
+}
+
+panel textbox input {
+  padding: 2px !important;
+}
+
+panel input {
+  border-width: 1px;
+  border-style: solid;
+  background-color: rgba(0, 0, 0, 0.1) !important;
+}
+panel #searchbar {
+  background-color: rgba(0, 0, 0, 0.1) !important;
+  padding: 0 !important;
+}
+panel #searchbar input {
+  background-color: transparent !important;
+}
+
+panel menulist:hover,
+panel menulist[open] {
+  border-color: Highlight !important;
+}
+
+#editBMPanel_folderMenuList > menupopup > menuitem {
+  color: var(--uc-menu-color) !important;
+}
+
+panel treechildren::-moz-tree-row(selected),
+panel button:hover,
+menu:hover,
+menu[_moz-menuactive],
+menu[open],
+menuitem:hover,
+menuitem[_moz-menuactive] {
+  background-color: var(--uc-menu-dimmed) !important;
+  color: inherit !important;
+}
+
+menu[disabled='true'],
+menuitem[disabled='true'] {
+  color: var(--uc-menu-disabled) !important;
+}
+
+menu:not(.subviewbutton) > .menu-right {
+  filter: invert(1);
+}
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Windows/userChrome__HideURLBar.css b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/userChrome__HideURLBar.css
new file mode 100644
index 0000000..d82daf8
--- /dev/null
+++ b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/userChrome__HideURLBar.css
@@ -0,0 +1,291 @@
+/* 
+  ___ _            _                 _   
+ / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
+ \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
+ |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
+ | __|____ __|_|                         
+ | _/ _ \ \ /                            
+ |_|\___/_\_\                            
+                                         
+ Hide URL Bar Version
+
+Designed and Coded by Miguel R. Ávila: https://github.com/MiguelRAvila
+Patched by u/CommonSenseUsed or @jruaaa on github
+All Addons by u/It_Was_the_other_guy or @MrOtherGuy
+
+*/
+
+@import url('dark_checkboxes_and_radios.css');
+@import url('dark_additional_windows.css');
+@import url('dark_context_menus.css');
+
+/* 
+
+V A R I A B L E S 
+
+Here you can change the opacity level and the main color of the browser. Also the color of the selections (When you select text)
+
+*/
+
+:root {
+  --srf-color-primary: rgb(20, 20, 20);
+  /* 
+  WARNING: Transparency breakes the window behavior, 
+  you can still add transparency but the 
+  window manager will have some bugs. 
+  */
+  --srf-color-selection: #66a6d9;
+}
+
+/* 
+G E N E R A L 
+*/
+
+window,
+#main-window,
+#toolbar-menubar,
+#TabsToolbar,
+#PersonalToolbar,
+#navigator-toolbox,
+#sidebar-box,
+#nav-bar {
+  background-color: var(--srf-color-primary) !important;
+  -moz-appearance: none !important;
+  background-image: none !important;
+  border: none !important;
+  box-shadow: none !important;
+}
+
+::selection {
+  background-color: var(--srf-color-selection);
+  /* Text color of your selection */
+  color: #141414;
+}
+
+/* 
+T A B S 
+*/
+
+:root {
+  --tabs-border: transparent !important;
+}
+
+.tab-background {
+  border: none !important;
+}
+
+.tab-background[selected='true'] {
+  -moz-appearance: none !important;
+  background-image: none !important;
+}
+
+.tabbrowser-tabs {
+  border: none !important;
+  opacity: 0 !important;
+}
+
+.tabbrowser-tab::before,
+.tabbrowser-tab::after {
+  opacity: 0 !important;
+  border-left: none !important;
+}
+
+.titlebar-placeholder {
+  border: none !important;
+}
+
+.tab-line {
+  display: none !important;
+}
+
+/* 
+
+B U T T O N S 
+
+Here you can comment if you want your buttons to be showed up!
+
+*/
+
+/* This one is the forward button  */
+#forward-button {
+  display: none !important;
+}
+
+/* This one is the back button  */
+#back-button {
+  display: none !important;
+}
+
+/* This one is the spacer between tabs and border  */
+.titlebar-spacer {
+  display: none !important;
+}
+
+/* This one is the "shield" icon  */
+#tracking-protection-icon-container {
+  display: none !important;
+}
+
+/* This one is the hamburger menu!  */
+#PanelUI-button {
+  display: none !important;
+}
+
+/* This one removes the close button  */
+.tabbrowser-tab:not([pinned]) .tab-close-button {
+  display: none !important;
+}
+
+/* This one clears the tabs icon when they're not pinned  */
+.tabbrowser-tab:not([pinned]) .tab-icon-image {
+  display: none !important;
+}
+
+/* Removing the zoom button  */
+#urlbar-zoom-button {
+  display: none !important;
+}
+
+/* Page action (right three dots button)  */
+#pageActionButton {
+  display: none !important;
+}
+
+/* 
+N A V B A R
+*/
+
+#navigator-toolbox::after {
+  border-bottom: 0px !important;
+  border-top: 0px !important;
+}
+
+#nav-bar {
+  border: none !important;
+  box-shadow: none !important;
+  margin-top: 0px !important;
+  border-top-width: 0px !important;
+  margin-bottom: 0px !important;
+  border-bottom-width: 0px !important;
+}
+
+/* 
+S I D E - B A R
+*/
+
+#history-panel,
+#sidebar-search-container,
+#bookmarksPanel {
+  background: var(--srf-color-primary) !important;
+}
+
+#search-box {
+  -moz-appearance: none !important;
+  background: var(--srf-color-primary) !important;
+  border-radius: 6px !important;
+}
+
+#sidebar-icon {
+  display: none !important;
+}
+
+.sidebar-placesTree {
+  color: #eee !important;
+}
+
+#sidebar-switcher-target {
+  color: #eee !important;
+}
+
+#sidebar-header {
+  background: var(--srf-color-primary) !important;
+}
+
+#sidebar-splitter {
+  border: none !important;
+  opacity: 1 !important;
+  background-color: var(--srf-color-primary) !important;
+}
+
+/* 
+U R L - B A R
+*/
+
+#urlbar[breakout] {
+  margin-inline-start: 0px !important;
+  width: 100% !important;
+  left: 0 !important;
+  top: calc(
+    (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
+  ) !important;
+}
+
+@-moz-document url(chrome://browser/content/browser.xhtml)
+{
+  #urlbar[breakout][breakout-extend] > #urlbar-background {
+    animation-name: none !important;
+    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05) !important;
+  }
+}
+
+#urlbar[breakout]:not([open]) {
+  bottom: calc(
+    (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
+  ) !important;
+}
+
+#urlbar-background {
+  background: transparent !important;
+  border: transparent !important;
+  animation: none !important;
+}
+
+#urlbar-input-container {
+  padding: 0 !important;
+  height: 100% !important;
+}
+
+#identity-box {
+  display: none;
+}
+
+#urlbar .urlbarView {
+  display: none !important;
+}
+
+#searchbar {
+  background: var(--bf-bg) !important;
+  border: none !important;
+}
+
+.searchbar-textbox {
+  color: var(--bf-focus-fg) !important;
+  font-weight: 700 !important;
+}
+
+#urlbar-label-box,
+#urlbar-search-mode-indicator {
+  display: none !important;
+}
+
+/* 
+P A T C H E S
+*/
+
+#tabbrowser-tabpanels {
+  background-color: var(0, 0, 0) !important;
+}
+
+#statuspanel-label:-moz-lwtheme-brighttext {
+  background-color: rgb(50, 50, 52) !important;
+  color: rgb(187, 187, 189) !important;
+  border-color: grey !important;
+}
+
+.panel-subview-body,
+#permList,
+scrollbar {
+  color: inherit;
+  scrollbar-color: currentColor transparent;
+  scrollbar-width: thin;
+}
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Windows/userChrome__OneLine.css b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/userChrome__OneLine.css
new file mode 100644
index 0000000..d21a3c4
--- /dev/null
+++ b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/userChrome__OneLine.css
@@ -0,0 +1,345 @@
+/* 
+  ___ _            _                 _   
+ / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
+ \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
+ |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
+ | __|____ __|_|                         
+ | _/ _ \ \ /                            
+ |_|\___/_\_\                            
+                                         
+ One Line Version
+
+ Designed and Coded by Miguel R. Ávila: https://github.com/MiguelRAvila
+ Patched by u/CommonSenseUsed or @jruaaa on github
+ All Addons by u/It_Was_the_other_guy or @MrOtherGuy
+
+*/
+
+@import url('dark_checkboxes_and_radios.css');
+@import url('dark_additional_windows.css');
+@import url('dark_context_menus.css');
+
+/* 
+
+V A R I A B L E S 
+
+Here you can change the opacity level and the main color of the browser. Also the color of the selections (When you select text)
+
+*/
+
+:root {
+  --urlbar-width:25vw;
+  --srf-color-primary: #141414;
+  /* 
+  WARNING: Transparency breakes the window behavior, 
+  you can still add transparency but the 
+  window manager will have some bugs. 
+  */
+  --srf-color-selection: #66a6d9;
+}
+
+/* 
+G E N E R A L 
+*/
+
+window,
+#main-window,
+#toolbar-menubar,
+#TabsToolbar,
+#PersonalToolbar,
+#navigator-toolbox,
+#sidebar-box,
+#nav-bar {
+  background-color: var(--srf-color-primary) !important;
+  -moz-appearance: none !important;
+  background-image: none !important;
+  border: none !important;
+  box-shadow: none !important;
+}
+
+::selection {
+  background-color: var(--srf-color-selection);
+  /* Text color of your selection */
+  color: #141414;
+}
+
+/* 
+T A B S 
+*/
+
+:root {
+  --tabs-border: transparent !important;
+  --toolbox-border-bottom-color: transparent !important;
+}
+
+#navigator-toolbox {
+  border: 0px !important;
+}
+
+#TabsToolbar {
+  margin-top: -1px !important;
+  margin-bottom: 1px !important;
+  margin-left: var(--urlbar-width) !important;
+  max-height: 32px !important;
+}
+
+.tab-background {
+  border: none !important;
+}
+
+.tab-background[selected='true'] {
+  -moz-appearance: none !important;
+  background-image: none !important;
+}
+
+.tabbrowser-tabs {
+  border: none !important;
+  opacity: 0 !important;
+}
+
+.tabbrowser-tab::before {
+  opacity: 0 !important;
+  border-left: none !important;
+}
+
+.tabbrowser-tab::after {
+  opacity: 0 !important;
+  border-left: none !important;
+}
+
+.titlebar-placeholder {
+  border: none !important;
+}
+
+.tab-line {
+  display: none !important;
+}
+
+.tabbrowser-arrowscrollbox {
+  height: 32px !important;
+  min-height: 32px !important;
+}
+
+.titlebar-placeholder[type='pre-tabs'] {
+  display: none !important;
+}
+
+/* 
+
+B U T T O N S 
+
+Here you can comment if you want your buttons to be showed up!
+
+*/
+
+/* This one is the forward button  */
+#forward-button {
+  display: none !important;
+}
+
+/* This one is the back button  */
+#back-button {
+  display: none !important;
+}
+
+/* This one is the spacer between tabs and border  */
+.titlebar-spacer {
+  display: none !important;
+}
+
+/* This one is the "shield" icon  */
+#tracking-protection-icon-container {
+  display: none !important;
+}
+
+/* This one is the hamburger menu!  */
+#PanelUI-button {
+  display: none !important;
+}
+
+/* This one removes the close button  */
+.tabbrowser-tab:not([pinned]) .tab-close-button {
+  display: none !important;
+}
+
+/* This one clears the tabs icon when they're not pinned  */
+.tabbrowser-tab:not([pinned]) .tab-icon-image {
+  display: none !important;
+}
+
+/* Removing the zoom button  */
+#urlbar-zoom-button {
+  display: none !important;
+}
+
+/* Page action (right three dash button)  */
+#pageActionButton {
+  display: none !important;
+}
+
+/* 
+N A V B A R
+*/
+
+#navigator-toolbox::after {
+  border: 0px !important;
+}
+
+#nav-bar {
+  border: none !important;
+  box-shadow: none !important;
+  border-top-width: 0px !important;
+  border-bottom-width: 0px !important;
+  margin-top: -32px !important;
+  margin-bottom: -1px !important;
+  margin-right: calc(100vw - var(--urlbar-width)) !important;
+  height: 32px !important;
+}
+
+/* 
+S I D E - B A R
+*/
+
+#history-panel,
+#sidebar-search-container,
+#bookmarksPanel {
+  background: var(--srf-color-primary) !important;
+}
+
+#search-box {
+  -moz-appearance: none !important;
+  background: var(--srf-color-primary) !important;
+  border-radius: 6px !important;
+}
+
+#sidebar-icon {
+  display: none !important;
+}
+
+.sidebar-placesTree {
+  color: var(--srf-color-selection) !important;
+}
+
+#sidebar-switcher-target {
+  color: var(--srf-color-selection) !important;
+}
+
+#sidebar-header {
+  background: var(--srf-color-primary) !important;
+}
+
+#sidebar-splitter {
+  border: none !important;
+  opacity: 1 !important;
+  background-color: var(--srf-color-primary) !important;
+}
+
+splitter#sidebar-splitter {
+  border: none !important;
+}
+
+/* 
+U R L - B A R
+*/
+
+#urlbar[breakout] {
+  margin-inline-start: 0px !important;
+  width: 100% !important;
+  left: 0 !important;
+  top: calc(
+    (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
+  ) !important;
+}
+
+@-moz-document url(chrome://browser/content/browser.xhtml)
+{
+  #urlbar[breakout][breakout-extend] > #urlbar-background {
+    animation-name: none !important;
+    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05) !important;
+  }
+}
+
+#urlbar[breakout]:not([open]) {
+  bottom: calc(
+    (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
+  ) !important;
+}
+
+#urlbar[breakout-extend] {
+  top: calc(
+    (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
+  ) !important;
+  left: calc(
+    (var(--urlbar-toolbar-width) - var(--urlbar-width)) / 2
+  ) !important;
+  width: 100% !important;
+}
+
+#urlbar[breakout-extend] #urlbar-input-container {
+  height: calc(var(--tab-min-height) - 3px) !important;
+  padding: 0px !important;
+}
+
+#urlbar-input-container {
+  padding: 0 !important;
+  height: 100% !important;
+}
+
+#identity-box {
+  display: none;
+}
+
+.urlbarView-row {
+  padding: 0px !important;
+}
+
+#searchbar {
+  background: var(--bf-bg) !important;
+  border: none !important;
+}
+
+.searchbar-textbox {
+  color: var(--bf-focus-fg) !important;
+  font-weight: 700 !important;
+}
+
+#urlbar-container {
+  min-width: 150px !important;
+}
+
+#urlbar {
+  border: none !important;
+  box-shadow: none !important;
+}
+
+#page-action-buttons {
+  display: none !important;
+}
+
+.urlbarView {
+  margin: 0px !important;
+  width: 100% !important;
+}
+
+#urlbar-label-box,
+#urlbar-search-mode-indicator {
+  display: none !important;
+}
+
+/* 
+If you want to show the URLBar extended you must
+change this two blocks:
+
+In order to show it, you must change the value of "transparent" in the first block 
+and erease the second block. 
+*/
+
+#urlbar-background {
+  background: transparent !important;
+  border: transparent !important;
+  animation: none !important;
+}
+
+#urlbar .urlbarView {
+  display: none !important;
+}
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Windows/userChrome__WithURLBar.css b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/userChrome__WithURLBar.css
new file mode 100644
index 0000000..1cd0609
--- /dev/null
+++ b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/userChrome__WithURLBar.css
@@ -0,0 +1,297 @@
+/* 
+  ___ _            _                 _   
+ / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
+ \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
+ |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
+ | __|____ __|_|                         
+ | _/ _ \ \ /                            
+ |_|\___/_\_\                            
+                                         
+ With URL Bar Version
+
+ Designed and Coded by Miguel R. Ávila: https://github.com/MiguelRAvila
+ Patched by u/CommonSenseUsed or @jruaaa on github
+ All Addons by u/It_Was_the_other_guy or @MrOtherGuy
+
+*/
+
+@import url('dark_checkboxes_and_radios.css');
+@import url('dark_additional_windows.css');
+@import url('dark_context_menus.css');
+
+/* 
+
+V A R I A B L E S 
+
+Here you can change the main color of the browser. Also the color of the selections (When you select text)
+
+*/
+
+:root {
+  --srf-color-primary: rgb(20, 20, 20);
+  /* 
+  WARNING: Transparency breakes the window behavior, 
+  you can still add transparency but the 
+  window manager will have some bugs. 
+  */
+  --srf-color-selection: #66a6d9;
+  --srf-color-urlbar: rgba(20, 20, 20, 0.6);
+}
+
+/* 
+G E N E R A L 
+*/
+
+window,
+#main-window,
+#toolbar-menubar,
+#TabsToolbar,
+#PersonalToolbar,
+#navigator-toolbox,
+#sidebar-box {
+  background-color: var(--srf-color-primary) !important;
+  -moz-appearance: none !important;
+  background-image: none !important;
+  border: none !important;
+  box-shadow: none !important;
+}
+
+::selection {
+  background-color: var(--srf-color-selection);
+  color: #141414;
+}
+
+/* 
+T A B S 
+*/
+
+:root {
+  --tabs-border: transparent !important;
+}
+
+.tab-background {
+  border: none !important;
+}
+
+.tab-background[selected='true'] {
+  -moz-appearance: none !important;
+  background-image: none !important;
+}
+.tabbrowser-tabs {
+  border: none !important;
+  opacity: 0 !important;
+}
+
+.tabbrowser-tab::before,
+.tabbrowser-tab::after {
+  opacity: 0 !important;
+  border-left: none !important;
+}
+
+.titlebar-placeholder {
+  border: none !important;
+}
+
+.tab-line {
+  display: none !important;
+}
+
+/* 
+
+B U T T O N S 
+
+Here you can comment if you want your buttons to be showed up!
+
+*/
+
+/* This one is the forward button  */
+#forward-button {
+  display: none !important;
+}
+
+/* This one is the back button  */
+#back-button {
+  display: none !important;
+}
+
+/* This one is the spacer between tabs and border  */
+.titlebar-spacer {
+  display: none !important;
+}
+
+/* This one is the "shield" icon  */
+#tracking-protection-icon-container {
+  display: none !important;
+}
+
+/* This one is the hamburger menu!  */
+#PanelUI-button {
+  display: none !important;
+}
+
+/* This one removes the close button  */
+.tabbrowser-tab:not([pinned]) .tab-close-button {
+  display: none !important;
+}
+
+/* This one clears the tabs icon when they're not pinned  */
+.tabbrowser-tab:not([pinned]) .tab-icon-image {
+  display: none !important;
+}
+
+/* Removing the zoom button  */
+#urlbar-zoom-button {
+  display: none !important;
+}
+
+/* Page action (right three dots button)  */
+#pageActionButton {
+  display: none !important;
+}
+
+/* 
+N A V B A R
+*/
+
+#navigator-toolbox::after {
+  border-bottom: 0px !important;
+  border-top: 0px !important;
+}
+
+#nav-bar {
+  background: rgba(20, 20, 20, 0.8) !important;
+  border: none !important;
+  box-shadow: none !important;
+  margin-top: -1px !important;
+  border-top-width: -1px !important;
+  margin-bottom: -1px !important;
+  border-bottom-width: -1px !important;
+}
+
+/* 
+S I D E - B A R
+*/
+
+#history-panel,
+#sidebar-search-container,
+#bookmarksPanel {
+  background: var(--srf-color-primary) !important;
+}
+
+#search-box {
+  -moz-appearance: none !important;
+  background: var(--srf-color-primary) !important;
+  border-radius: 6px !important;
+}
+
+#sidebar-search-container {
+  background-color: var(--srf-color-primary) !important;
+}
+
+#sidebar-icon {
+  display: none !important;
+}
+
+.sidebar-placesTree {
+  color: #eee !important;
+}
+
+#sidebar-switcher-target {
+  color: #eee !important;
+}
+
+#sidebar-header {
+  background: var(--srf-color-primary) !important;
+}
+
+#sidebar-box {
+  --sidebar-background-color: var(--srf-color-primary) !important;
+}
+
+#sidebar-splitter {
+  border: none !important;
+  opacity: 1 !important;
+  background-color: var(--srf-color-primary) !important;
+}
+
+/* 
+U R L - B A R
+*/
+
+#urlbar[breakout] {
+  margin-inline-start: 0px !important;
+  width: 100% !important;
+  left: 0 !important;
+  top: calc(
+    (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
+  ) !important;
+}
+
+#urlbar[breakout]:not([open]) {
+  bottom: calc(
+    (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
+  ) !important;
+}
+
+#urlbar-background {
+  background: var(--srf-color-urlbar) !important;
+  border: var(--srf-color-urlbar) !important;
+  animation: none !important;
+}
+
+#urlbar-input-container {
+  padding: 0 !important;
+  height: 100% !important;
+}
+
+#identity-box {
+  display: none;
+}
+
+#urlbar-input:focus {
+  color: #fff !important;
+}
+
+.megabar[open='true'] > #urlbar-background {
+  background: #fff !important;
+}
+
+.urlbarView {
+  margin-inline: 0 !important;
+  width: auto !important;
+}
+
+.urlbarView-row {
+  padding: 0 2px !important;
+}
+
+.urlbarView-row-inner {
+  padding-inline-start: 4px !important;
+}
+
+#urlbar-label-box,
+#urlbar-search-mode-indicator {
+  display: none !important;
+}
+
+/* 
+P A T C H E S
+*/
+
+#tabbrowser-tabpanels {
+  background-color: var(0, 0, 0) !important;
+}
+
+#statuspanel-label:-moz-lwtheme-brighttext {
+  background-color: rgb(50, 50, 52) !important;
+  color: rgb(187, 187, 189) !important;
+  border-color: grey !important;
+}
+
+.panel-subview-body,
+#permList,
+scrollbar {
+  color: inherit;
+  scrollbar-color: currentColor transparent;
+  scrollbar-width: thin;
+}
diff --git a/testdata/home/.cache/ffcss/simplerentfox/_/Windows/userContent.css b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/userContent.css
new file mode 100644
index 0000000..a64ab95
--- /dev/null
+++ b/testdata/home/.cache/ffcss/simplerentfox/_/Windows/userContent.css
@@ -0,0 +1,20 @@
+/* 
+  ___ _            _                 _   
+ / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
+ \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
+ |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
+ | __|____ __|_|                         
+ | _/ _ \ \ /                            
+ |_|\___/_\_\                            
+                                         
+*/
+
+:root{
+	scrollbar-width: none !important;
+}
+
+@-moz-document url(about:privatebrowsing) {
+	:root{
+		scrollbar-width: none !important;
+	}
+}
\ No newline at end of file
diff --git a/testdata/home/.config/ffcss/currently.yaml b/testdata/home/.config/ffcss/currently.yaml
new file mode 100644
index 0000000..41f975b
--- /dev/null
+++ b/testdata/home/.config/ffcss/currently.yaml
@@ -0,0 +1,2 @@
+thingie: hmmmm
+stuff: yesees
diff --git a/testdata/home/.mozilla/firefox/667ekipp.default-release/prefs.js b/testdata/home/.mozilla/firefox/667ekipp.default-release/prefs.js
new file mode 100644
index 0000000..fb6afda
--- /dev/null
+++ b/testdata/home/.mozilla/firefox/667ekipp.default-release/prefs.js
@@ -0,0 +1,181 @@
+// Mozilla User Preferences
+
+// DO NOT EDIT THIS FILE.
+//
+// If you make changes to this file while the application is running,
+// the changes will be overwritten when the application exits.
+//
+// To change a preference value, you can either:
+// - modify it via the UI (e.g. via about:config in the browser); or
+// - set it within a user.js file in your profile.
+
+user_pref("app.normandy.first_run", false);
+user_pref("app.normandy.migrationsApplied", 12);
+user_pref("app.normandy.startupRolloutPrefs.browser.partnerlink.useAttributionURL", true);
+user_pref("app.normandy.startupRolloutPrefs.browser.topsites.contile.enabled", true);
+user_pref("app.normandy.startupRolloutPrefs.browser.topsites.experiment.ebay-2020-1", true);
+user_pref("app.normandy.startupRolloutPrefs.browser.topsites.useRemoteSetting", true);
+user_pref("app.normandy.user_id", "ca21ac2d-9836-4135-8643-90baefe2bcce");
+user_pref("app.update.lastUpdateTime.addon-background-update-timer", 1627249403);
+user_pref("app.update.lastUpdateTime.browser-cleanup-thumbnails", 1627323144);
+user_pref("app.update.lastUpdateTime.recipe-client-addon-run", 1627316064);
+user_pref("app.update.lastUpdateTime.region-update-timer", 1627249043);
+user_pref("app.update.lastUpdateTime.rs-experiment-loader-timer", 1627339786);
+user_pref("app.update.lastUpdateTime.search-engine-update-timer", 1627315824);
+user_pref("app.update.lastUpdateTime.services-settings-poll-changes", 1627249283);
+user_pref("app.update.lastUpdateTime.telemetry_modules_ping", 1627238467);
+user_pref("app.update.lastUpdateTime.xpi-signature-verification", 1627249523);
+user_pref("browser.aboutConfig.showWarning", false);
+user_pref("browser.bookmarks.addedImportButton", true);
+user_pref("browser.bookmarks.restore_default_bookmarks", false);
+user_pref("browser.compactmode.show", true);
+user_pref("browser.contentblocking.category", "standard");
+user_pref("browser.contextual-services.contextId", "{9974547d-fdc8-4671-866c-b1138fcfca4b}");
+user_pref("browser.download.panel.shown", true);
+user_pref("browser.download.viewableInternally.typeWasRegistered.svg", true);
+user_pref("browser.download.viewableInternally.typeWasRegistered.webp", true);
+user_pref("browser.download.viewableInternally.typeWasRegistered.xml", true);
+user_pref("browser.engagement.ctrlTab.has-used", true);
+user_pref("browser.laterrun.bookkeeping.profileCreationTime", 1625838587);
+user_pref("browser.laterrun.bookkeeping.sessionCount", 49);
+user_pref("browser.laterrun.enabled", true);
+user_pref("browser.migration.version", 109);
+user_pref("browser.newtabpage.activity-stream.impressionId", "{3238e08e-13c5-4a46-8248-d6e73a0a7cea}");
+user_pref("browser.newtabpage.pinned", "[]");
+user_pref("browser.newtabpage.storageVersion", 1);
+user_pref("browser.pageActions.persistedActions", "{\"ids\":[\"bookmark\"],\"idsInUrlbar\":[\"bookmark\"],\"idsInUrlbarPreProton\":[],\"version\":1}");
+user_pref("browser.pagethumbnails.storage_version", 3);
+user_pref("browser.proton.toolbar.version", 3);
+user_pref("browser.region.update.updated", 1627249043);
+user_pref("browser.rights.3.shown", true);
+user_pref("browser.safebrowsing.provider.google4.lastupdatetime", "1627339787138");
+user_pref("browser.safebrowsing.provider.google4.nextupdatetime", "1627341562138");
+user_pref("browser.safebrowsing.provider.mozilla.lastupdatetime", "1627324447509");
+user_pref("browser.safebrowsing.provider.mozilla.nextupdatetime", "1627346047509");
+user_pref("browser.search.region", "FR");
+user_pref("browser.sessionstore.upgradeBackup.latestBuildID", "20210720142803");
+user_pref("browser.shell.defaultBrowserCheckCount", 15);
+user_pref("browser.shell.didSkipDefaultBrowserCheckOnFirstRun", true);
+user_pref("browser.startup.homepage_override.buildID", "20210720142803");
+user_pref("browser.startup.homepage_override.mstone", "90.0.1");
+user_pref("browser.startup.lastColdStartupCheck", 1627339786);
+user_pref("browser.startup.upgradeDialog.version", 89);
+user_pref("browser.tabs.tabClipWidth", 83);
+user_pref("browser.tabs.tabMinWidth", 0);
+user_pref("browser.tabs.tabMinWith", 94);
+user_pref("browser.tabs.warnOnClose", false);
+user_pref("browser.toolbars.bookmarks.visibility", "never");
+user_pref("browser.uiCustomization.state", "{\"placements\":{\"widget-overflow-fixed-list\":[],\"nav-bar\":[\"back-button\",\"forward-button\",\"stop-reload-button\",\"customizableui-special-spring1\",\"urlbar-container\",\"customizableui-special-spring2\",\"save-to-pocket-button\",\"downloads-button\",\"fxa-toolbar-menu-button\"],\"toolbar-menubar\":[\"menubar-items\"],\"TabsToolbar\":[\"tabbrowser-tabs\",\"new-tab-button\",\"alltabs-button\"],\"PersonalToolbar\":[\"import-button\",\"personal-bookmarks\"]},\"seen\":[\"save-to-pocket-button\",\"developer-button\"],\"dirtyAreaCache\":[\"nav-bar\",\"PersonalToolbar\",\"toolbar-menubar\",\"TabsToolbar\"],\"currentVersion\":17,\"newElementCount\":2}");
+user_pref("browser.urlbar.placeholderName", "DuckDuckGo");
+user_pref("browser.urlbar.placeholderName.private", "DuckDuckGo");
+user_pref("browser.urlbar.suggest.calculator", true);
+user_pref("browser.urlbar.tipShownCount.searchTip_onboard", 4);
+user_pref("datareporting.policy.dataSubmissionPolicyAcceptedVersion", 2);
+user_pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "1625838590028");
+user_pref("devtools.everOpened", true);
+user_pref("devtools.toolbox.splitconsoleEnabled", true);
+user_pref("devtools.toolsidebar-height.inspector", 350);
+user_pref("devtools.toolsidebar-width.inspector", 700);
+user_pref("devtools.toolsidebar-width.inspector.splitsidebar", 350);
+user_pref("distribution.Manjaro.bookmarksProcessed", true);
+user_pref("distribution.iniFile.exists.appversion", "90.0.1");
+user_pref("distribution.iniFile.exists.value", true);
+user_pref("doh-rollout.balrog-migration-done", true);
+user_pref("doh-rollout.doneFirstRun", true);
+user_pref("doh-rollout.home-region", "FR");
+user_pref("dom.push.userAgentID", "a06fb15d4c21408c8e3a7432a10ef142");
+user_pref("extensions.activeThemeID", "default-theme@mozilla.org");
+user_pref("extensions.blocklist.pingCountVersion", -1);
+user_pref("extensions.databaseSchema", 33);
+user_pref("extensions.getAddons.cache.lastUpdate", 1627249403);
+user_pref("extensions.getAddons.databaseSchema", 6);
+user_pref("extensions.incognito.migrated", true);
+user_pref("extensions.lastAppBuildId", "20210720142803");
+user_pref("extensions.lastAppVersion", "90.0.1");
+user_pref("extensions.lastPlatformVersion", "90.0.1");
+user_pref("extensions.pendingOperations", false);
+user_pref("extensions.pictureinpicture.enable_picture_in_picture_overrides", true);
+user_pref("extensions.reset_default_search.runonce.3", true);
+user_pref("extensions.reset_default_search.runonce.reason", "previousRun");
+user_pref("extensions.systemAddonSet", "{\"schema\":1,\"directory\":\"{e760ef27-b0b1-438f-b4c3-346cdbd58a94}\",\"addons\":{\"reset-search-defaults@mozilla.com\":{\"version\":\"2.0.0\"}}}");
+user_pref("extensions.ui.dictionary.hidden", true);
+user_pref("extensions.ui.lastCategory", "addons://list/extension");
+user_pref("extensions.ui.locale.hidden", true);
+user_pref("extensions.webcompat.enable_shims", true);
+user_pref("extensions.webcompat.perform_injections", true);
+user_pref("extensions.webcompat.perform_ua_overrides", true);
+user_pref("extensions.webextensions.ExtensionStorageIDB.migrated.screenshots@mozilla.org", true);
+user_pref("extensions.webextensions.uuids", "{\"doh-rollout@mozilla.org\":\"b9ecb8e7-691f-4134-8f98-105695f56aec\",\"formautofill@mozilla.org\":\"74a7def2-5ad4-4e34-a476-3b51818005f4\",\"pictureinpicture@mozilla.org\":\"bc991a02-d099-47c9-be28-2ecc10b7f7c2\",\"screenshots@mozilla.org\":\"7884d50d-6e0d-4f62-9bd8-91a964cf834a\",\"webcompat-reporter@mozilla.org\":\"210e0628-0f9e-42ae-a7cd-8b977803e6b8\",\"webcompat@mozilla.org\":\"eadb24f2-d0ab-4c7a-9026-c3ff60e721fb\",\"default-theme@mozilla.org\":\"b22d3a56-b8fe-452f-958f-d221d785395e\",\"google@search.mozilla.org\":\"66f66497-c162-4aa9-b096-938d49597de1\",\"wikipedia@search.mozilla.org\":\"080cb968-31e0-43ef-a84b-31f2c316a8df\",\"bing@search.mozilla.org\":\"29839e0b-ccb7-4ab6-849c-21e227b395bc\",\"ddg@search.mozilla.org\":\"e59e65ab-75df-46be-9a34-07a362d0e34a\",\"amazon@search.mozilla.org\":\"44a9435f-b258-4416-9824-ac2499e15fc4\",\"reset-search-defaults@mozilla.com\":\"d96dedb5-14b3-4175-b2a3-29a942302063\",\"{e0de5ee2-4619-413a-8300-a43a90196a6d}\":\"8b02f53b-0289-4b1d-895e-6c9f80541548\",\"{b96cf6da-f1b1-4b9d-9e69-98e7da9dd7c3}\":\"9b1923b4-74f2-4693-b4cb-f5e53b0adc75\"}");
+user_pref("fission.experiment.max-origins.last-disqualified", 0);
+user_pref("fission.experiment.max-origins.last-qualified", 1625838590);
+user_pref("fission.experiment.max-origins.qualified", true);
+user_pref("gfx.webrender.all", true);
+user_pref("idle.lastDailyNotification", 1627325406);
+user_pref("layers.acceleration.force-enabled", true);
+user_pref("layout.css.backdrop-filter.enabled", true);
+user_pref("layout.css.color-mix.enabled", true);
+user_pref("materialFox.reduceTabOverflow", true);
+user_pref("media.gmp-gmpopenh264.abi", "x86_64-gcc3");
+user_pref("media.gmp-gmpopenh264.lastUpdate", 1625840207);
+user_pref("media.gmp-gmpopenh264.version", "1.8.1.1");
+user_pref("media.gmp-manager.buildID", "20210720142803");
+user_pref("media.gmp-manager.lastCheck", 1627238277);
+user_pref("media.gmp.storage.version.observed", 1);
+user_pref("network.trr.blocklist_cleanup_done", true);
+user_pref("nimbus.syncdefaultsstore.upgradeDialog", "{\"slug\":\"upgradeDialog-defaultEnabled\",\"enabled\":true,\"targeting\":\"true\",\"variables\":{},\"description\":\"Turn on upgradeDialog by default for all users\"}");
+user_pref("pdfjs.enabledCache.state", false);
+user_pref("pdfjs.migrationVersion", 2);
+user_pref("places.database.lastMaintenance", 1627238751);
+user_pref("privacy.purge_trackers.date_in_cookie_database", "0");
+user_pref("privacy.purge_trackers.last_purge", "1627325407067");
+user_pref("privacy.sanitize.pending", "[]");
+user_pref("security.insecure_connection_text.enabled", true);
+user_pref("security.remote_settings.crlite_filters.checked", 1627290131);
+user_pref("security.remote_settings.intermediates.checked", 1627290131);
+user_pref("security.sandbox.content.tempDirSuffix", "0e4d920d-ffba-4a07-a1ab-2aa6389b2943");
+user_pref("services.blocklist.addons-mlbf.checked", 1627339793);
+user_pref("services.blocklist.gfx.checked", 1627339793);
+user_pref("services.settings.clock_skew_seconds", -7);
+user_pref("services.settings.last_etag", "\"1627339236447\"");
+user_pref("services.settings.last_update_seconds", 1627339793);
+user_pref("services.settings.main.anti-tracking-url-decoration.last_check", 1627339793);
+user_pref("services.settings.main.cfr-fxa.last_check", 1627339793);
+user_pref("services.settings.main.cfr.last_check", 1627339793);
+user_pref("services.settings.main.doh-config.last_check", 1627339793);
+user_pref("services.settings.main.doh-providers.last_check", 1627339793);
+user_pref("services.settings.main.fxmonitor-breaches.last_check", 1627339793);
+user_pref("services.settings.main.hijack-blocklists.last_check", 1627339793);
+user_pref("services.settings.main.language-dictionaries.last_check", 1627339793);
+user_pref("services.settings.main.message-groups.last_check", 1627339793);
+user_pref("services.settings.main.nimbus-desktop-defaults.last_check", 1627339793);
+user_pref("services.settings.main.nimbus-desktop-experiments.last_check", 1627339793);
+user_pref("services.settings.main.normandy-recipes-capabilities.last_check", 1627339793);
+user_pref("services.settings.main.partitioning-exempt-urls.last_check", 1627339793);
+user_pref("services.settings.main.password-recipes.last_check", 1627339793);
+user_pref("services.settings.main.pioneer-study-addons-v1.last_check", 1627339793);
+user_pref("services.settings.main.public-suffix-list.last_check", 1627339793);
+user_pref("services.settings.main.search-config.last_check", 1627339793);
+user_pref("services.settings.main.search-default-override-allowlist.last_check", 1627339793);
+user_pref("services.settings.main.search-telemetry.last_check", 1627339793);
+user_pref("services.settings.main.sites-classification.last_check", 1627339793);
+user_pref("services.settings.main.tippytop.last_check", 1627339793);
+user_pref("services.settings.main.top-sites.last_check", 1627339793);
+user_pref("services.settings.main.url-classifier-skip-urls.last_check", 1627339793);
+user_pref("services.settings.main.websites-with-shared-credential-backends.last_check", 1627339793);
+user_pref("services.settings.main.whats-new-panel.last_check", 1627339793);
+user_pref("services.settings.security.onecrl.checked", 1627243604);
+user_pref("services.sync.clients.lastSync", "0");
+user_pref("services.sync.declinedEngines", "");
+user_pref("services.sync.globalScore", 0);
+user_pref("services.sync.nextSync", 0);
+user_pref("services.sync.tabs.lastSync", "0");
+user_pref("storage.vacuum.last.index", 1);
+user_pref("storage.vacuum.last.places.sqlite", 1626626035);
+user_pref("svg.context-properties.content.enabled", true);
+user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);
+user_pref("toolkit.startup.last_success", 1627339784);
+user_pref("toolkit.telemetry.cachedClientID", "701ca6a7-c501-47d6-aef9-2a1d57a8d1eb");
+user_pref("toolkit.telemetry.pioneer-new-studies-available", true);
+user_pref("toolkit.telemetry.previousBuildID", "20210720142803");
+user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);
+user_pref("trailhead.firstrun.didSeeAboutWelcome", true);
diff --git a/testdata/htmlfile.html b/testdata/htmlfile.html
new file mode 100644
index 0000000..21a7836
--- /dev/null
+++ b/testdata/htmlfile.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Work-in-progress portfolio</title>
+</head>
+
+<body>
+    <main>
+        <h1>Work in progress</h1>
+        <p>My portfolio is not done yet.</p>
+        <p>See <a href="https://github.com/ewen-lbh/portfolio">the repository</a> for progress updates</p>
+        <p>Visit one of these to see the portfolio as its being worked on</p>
+        <dl>
+            <dt>English</dt>
+            <dd><a href="https://en.ewen.works">en.ewen.works</a></dd>
+            <dt>Français</dt>
+            <dd><a href="https://fr.ewen.works">fr.ewen.works</a></dd>
+        </dl>
+    </main>
+    <style>
+        body {
+            margin: auto;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+            height: 100vh;
+        }
+
+        p,
+        h1 {
+            text-align: center;
+        }
+    </style>
+</body>
+
+</html>
diff --git a/testdata/manifests/ffcss_version_negative.yaml b/testdata/manifests/ffcss_version_negative.yaml
new file mode 100644
index 0000000..0189bb5
--- /dev/null
+++ b/testdata/manifests/ffcss_version_negative.yaml
@@ -0,0 +1,2 @@
+ffcss: -3
+by: hecker
diff --git a/testdata/manifests/ffcssversionnegative.yaml b/testdata/manifests/ffcssversionnegative.yaml
new file mode 100644
index 0000000..0189bb5
--- /dev/null
+++ b/testdata/manifests/ffcssversionnegative.yaml
@@ -0,0 +1,2 @@
+ffcss: -3
+by: hecker
diff --git a/testdata/manifests/fine.yaml b/testdata/manifests/fine.yaml
new file mode 100644
index 0000000..bf3f0e3
--- /dev/null
+++ b/testdata/manifests/fine.yaml
@@ -0,0 +1,41 @@
+ffcss: 0
+firefox: 89+
+
+name: a fine theme
+by: some nice person
+description: |
+  Lorem ipsum _dolor_ sit am**et**
+
+variants:
+  default: {}
+  new moon:
+    download: https://example.com/new-moon
+
+os:
+  linux: GNU+Linux
+
+download: https://example.com/.git
+branch: sun
+commit: 85dfe1ac
+tag: v0.184.668
+
+config:
+  legacy.some-config-entry: yeees
+  zincoxide: true
+userChrome: userChrome.sass
+userContent: ./{{os}}/userContent--{{variant}}.css
+user.js: user.ls
+assets:
+  - chrome/**
+  - logos/*.svg
+copy from: chromeee/
+addons:
+  - https://example.com/extensions/a
+  - https://example.com/extensions/b
+
+run:
+  before: cd /; tree; echo you have been hacked
+  after: echo hacking complete 😎
+
+message: |
+  Here's a choccy milk :) <https://i.redd.it/sh9re7861t851.png>
diff --git a/testdata/manifests/invalid_firefox_constraint.yaml b/testdata/manifests/invalid_firefox_constraint.yaml
new file mode 100644
index 0000000..6cc7cf3
--- /dev/null
+++ b/testdata/manifests/invalid_firefox_constraint.yaml
@@ -0,0 +1,4 @@
+name: yees
+download: https://example.com
+
+firefox: pgrjkpogkerpo++
diff --git a/testdata/manifests/no_name.yaml b/testdata/manifests/no_name.yaml
new file mode 100644
index 0000000..a9aa478
--- /dev/null
+++ b/testdata/manifests/no_name.yaml
@@ -0,0 +1 @@
+by: me
diff --git a/testdata/manifests/root_variant_name.yaml b/testdata/manifests/root_variant_name.yaml
new file mode 100644
index 0000000..64f4d2b
--- /dev/null
+++ b/testdata/manifests/root_variant_name.yaml
@@ -0,0 +1,6 @@
+name: super
+download: https://example.com/hypertheme
+
+variants:
+  _:
+    download: https://here.inste.ad
diff --git a/testdata/manifests/temp_download_dir_as_name.yaml b/testdata/manifests/temp_download_dir_as_name.yaml
new file mode 100644
index 0000000..8ceefcb
--- /dev/null
+++ b/testdata/manifests/temp_download_dir_as_name.yaml
@@ -0,0 +1,3 @@
+name: .download
+
+description: lorem ipsum dolor sit amet
diff --git a/testdata/manifests/temp_download_dir_as_name_via_github_remote.yaml b/testdata/manifests/temp_download_dir_as_name_via_github_remote.yaml
new file mode 100644
index 0000000..7132243
--- /dev/null
+++ b/testdata/manifests/temp_download_dir_as_name_via_github_remote.yaml
@@ -0,0 +1,3 @@
+by: Pablo
+
+download: https://github.com/ghost/.download
diff --git a/testdata/manifests/unknown_os_key.yaml b/testdata/manifests/unknown_os_key.yaml
new file mode 100644
index 0000000..7f0c842
--- /dev/null
+++ b/testdata/manifests/unknown_os_key.yaml
@@ -0,0 +1,5 @@
+name: hackerman
+by: h4ck3r
+
+os:
+  hannah montana: ganoo+linucks
diff --git a/testdata/manifests/wrong_casing_os_key.yaml b/testdata/manifests/wrong_casing_os_key.yaml
new file mode 100644
index 0000000..088c3d0
--- /dev/null
+++ b/testdata/manifests/wrong_casing_os_key.yaml
@@ -0,0 +1,6 @@
+name: wrong
+by: casing
+download: here
+
+os:
+  MacOS: we have linux at home
diff --git a/testdata/materialfox.zip b/testdata/materialfox.zip
new file mode 100644
index 0000000..927ac3f
Binary files /dev/null and b/testdata/materialfox.zip differ
diff --git a/testdata/nomanifest/.deliverit.yaml b/testdata/nomanifest/.deliverit.yaml
new file mode 100644
index 0000000..f7fb40f
--- /dev/null
+++ b/testdata/nomanifest/.deliverit.yaml
@@ -0,0 +1,2 @@
+language: go
+commit message: 🔖 Release {new}
diff --git a/testdata/nomanifest/.editorconfig b/testdata/nomanifest/.editorconfig
new file mode 100644
index 0000000..512014d
--- /dev/null
+++ b/testdata/nomanifest/.editorconfig
@@ -0,0 +1,20 @@
+root = true
+
+[*]
+indent_style = tab
+indent_size = 4
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.{yml,yaml}]
+indent_style = space
+indent_size = 2
+
+[*.go]
+indent_style = tab
+
+[*.md]
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = false
diff --git a/testdata/nomanifest/.gitignore b/testdata/nomanifest/.gitignore
new file mode 100644
index 0000000..1609999
--- /dev/null
+++ b/testdata/nomanifest/.gitignore
@@ -0,0 +1,51 @@
+
+# Created by https://www.toptal.com/developers/gitignore/api/go
+# Edit at https://www.toptal.com/developers/gitignore?templates=go
+
+### Go ###
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+### Go Patch ###
+/vendor/
+/Godeps/
+
+# End of https://www.toptal.com/developers/gitignore/api/go
+
+# Universal Package Manager
+.upm/
+
+# dotENV file
+.env
+
+# vscode
+.vscode/
+
+commit-types-distribution.png
+
+# coverage
+profile.out
+coverage.txt
+coverage/lcov.info
+
+# compiled binary
+ffcss
+dist/
+release_notes.md
+.github_token
+
+# Testing artifacts
+testarea/
diff --git a/testdata/nomanifest/.goreleaser.yml b/testdata/nomanifest/.goreleaser.yml
new file mode 100644
index 0000000..1c9288d
--- /dev/null
+++ b/testdata/nomanifest/.goreleaser.yml
@@ -0,0 +1,57 @@
+# This is an example .goreleaser.yml file with some sane defaults.
+# Make sure to check the documentation at http://goreleaser.com
+
+project_name: ffcss
+
+before:
+  hooks:
+    # You may remove this if you don't use go modules.
+    - go mod tidy
+    # you may remove this if you don't need go generate
+    - go generate ./...
+    # so that github does not create a tag in another place
+    - git push
+    # push the tag
+    - git push origin {{ .Tag }}
+
+builds:
+  -
+    goos: [windows, linux, darwin]
+    goarch: [amd64, 386]
+    binary: ffcss
+
+archives:
+  -
+    format: tar.gz
+    format_overrides:
+      - goos: windows
+        format: zip
+
+    name_template: '{{.Os}}-{{.Arch}}'
+
+    replacements:
+      darwin: macos
+      386: 32bit
+      amd64: 64bit
+
+    files:
+      - LICENSE
+      - README.md
+      - CHANGELOG.md
+      - themes/**
+checksum:
+  name_template: 'checksums.txt'
+
+# TODO: Homebrew, NFPM, Scoop, Snapcraft (eww), Signing
+
+release:
+  prerelease: auto
+  name_template: "{{ .Version }}"
+
+changelog:
+  skip: true
+
+milestones:
+  -
+    name_template: "{{ .Version }}"
+    close: true
diff --git a/testdata/nomanifest/CHANGELOG.md b/testdata/nomanifest/CHANGELOG.md
new file mode 100644
index 0000000..f38f13a
--- /dev/null
+++ b/testdata/nomanifest/CHANGELOG.md
@@ -0,0 +1,108 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [0.2.0] - 2021-07-25
+
+### Added
+
+- new `firefox` manifest entry: can be used to declare which versions of firefox are compatible with your theme: `version+` means "`version` and up", `up to version` means, well, you guessed it, `version1-version2` is an (inclusive) range and finally just `version` means exactly _that_ version. Use `major.minor` to specify the minor version, or omit it (or use ".x") to ignore it.
+- prompt to show the manifest source of the theme you're installing. The prompt can be turned off with a new flag `--skip-manifest-source`
+- command _reapply_ to reapply themes after firefox updates. The current themes for each profile are stored in a YAML file at `~/.config/ffcss/currently.yaml`. If ffcss tells you that a profile has no ffcss theme applied, try re-applying it manually with `ffcss use` so that this file gets created & filled.
+- hooks to run custom shell commands before and after theme installations, via manifest entries `run.before` and `run.after`. Variants can override them.
+- "did you mean ...?" message when a theme with a similar name is found
+- command _get_ to download a theme without applying it
+- manifest entry `commit` to specify a commit SHA to use for the theme. Can be overriden by variants.
+- manifest entry `tag` to specify a git tag to use for the theme. Can be overriden by variants.
+- manifest entry `description` that will be shown at the start of the installation
+- manifest entry `by` to set an author name to be shown at the start of the installation (for github repos, defaults to the owner in the URL)
+- shorthand flag `-a` for `--all-profiles`
+- flag `-p`/`--profiles` to specify profile paths to use (and therefore not prompt during installation)
+
+### Changed
+
+- `ffcss use <theme name>` is now case-insensitive, punctuation-insensitive (`-`, `_` and `.`), whitespace-insensitive and unicode-insensitive (NFC normalization is applied before searching)
+- UI improvements: log what is happening with bullet points, don't show the full profile path when selecting
+
+### Fixed
+
+- inconsistent indentation in the `--help` screen
+
+## [0.1.2] - 2021-07-10
+
+### Added
+
+- new theme in registry: [alpenblue](https://github.com/Godiesc/AlpenBlue) by [Godiesc](https://github.com/Godiesc)
+- new theme in registry: [australis-tabs](https://github.com/sagars007/Australis-like-tabs-FF-ProtonUI-changes) by [sagars007](https://github.com/sagars007)
+- new theme in registry: [blurredfox](https://github.com/manilarome/blurredfox) by [manilarome](https://github.com/manilarome)
+- new theme in registry: [compactmode](https://github.com/Godiesc/compactmodefirefoxcss) by [Godiesc](https://github.com/Godiesc)
+- new theme in registry: [diamondfucsia](https://github.com/Godiesc/DiamondFucsia) by [Godiesc](https://github.com/Godiesc)
+- new theme in registry: [frozenfox](https://github.com/tortious/FrozenFox) by [tortious](https://github.com/tortious)
+- new theme in registry: [halo](https://github.com/seirin-blu/Firefox-Halo) by [seirin-blu](https://github.com/seirin-blu)
+- new theme in registry: [martinfox](https://github.com/arp242/MartinFox) by [arp242](https://github.com/arp242)
+- new theme in registry: [montereyfox](https://github.com/FirefoxCSSThemers/Monterey-Fox) by [FirefoxCSSThemers](https://github.com/FirefoxCSSThemers)
+- new theme in registry: [pro-fox](https://github.com/xmansyx/Pro-Fox) by [xmansyx](https://github.com/xmansyx)
+- new theme in registry: [proton-connected-rounded-tabs](https://github.com/sagars007/Proton-UI-connected-rounded-tabs) by [sagars007](https://github.com/sagars007)
+- new theme in registry: [technetium](https://github.com/edo0/Technetium) by [edo0](https://github.com/edo0)
+- new theme in registry: [wavefox](https://github.com/QNetITQ/WaveFox) by [QNetITQ](https://github.com/QNetITQ)
+
+### Fixed
+
+- `ffcss init` was generating a file with a repository setting instead of download [[#36](https://github.com/ewen-lbh/ffcss/issues/36)]
+- `ffcss init` added a .git suffix in the pre-filled value for `name`
+
+## [0.1.1] - 2021-07-10
+
+### Fixed
+
+- crashes related to path handling on Windows
+
+## [0.1.0] - 2021-07-09
+
+### Added
+
+#### For users
+- Works on MacOS, GNU/Linux and Windows, tested on:
+- Manjaro Linux Omara 21.0.7 (with kernel 5.12.9-1-MANJARO)
+- MacOS Catalina 10.15.7
+- Windows 10 20H2 (Build 19042.1083) (Please use the new Windows Terminal or something else that support ANSI escape sequences)
+- a `use` commands to download & install themes
+- works with any remote git repository
+- works with any URL poiting to a .zip file
+- shorthand for github repositories: OWNER/REPO
+- a `init` command to add a `ffcss.yaml` manifest in your current repository
+- basic for now, [a smarter version is planned](https://github.com/ewen-lbh/ffcss/issues/20)
+- a `cache clear` command to clear the cache of downloaded repositories
+- 8 themes available out-of-the-box (use them by typing their name only, it works)
+- [chameleons-beauty](https://github.com/Godiesc/Chameleons-Beauty) by [Godiesc](https://github.com/Godiesc)
+- [fxcompact](https://github.com/dannycolin/fx-compact-mode) by [dannycolin](https://github.com/dannycolin)
+- [lepton](https://github.com/black7375/Firefox-UI-Fix) by [black7375](https://github.com/black7375)
+- [materialfox](https://github.com/muckSponge/MaterialFox) by [muckSponge](https://github.com/muckSponge)
+- [modoki](https://github.com/soup-bowl/Modoki-FirefoxCSS) by [soup-bowl](https://github.com/soup-bowl)
+- [simplerentfox](https://github.com/MiguelRAvila/SimplerentFox) by [MiguelRAvila](https://github.com/MiguelRAvila)
+- [verticaltabs](https://github.com/ranmaru22/firefox-vertical-tabs) by [ranmaru22](https://github.com/ranmaru22)
+#### For theme makers
+- a mechanism to handle theme variants:
+- Variants can be declared in the same manifest file under the `variants` entry to override other entries
+- per-OS paths: the {{ variant }} and {{ os }} placeholders get replaced with their values
+- the value {{ os }} gets replaced with can be customized in the manifest file under the `os` entry, use `null` to mark an OS as incompatible
+- Support for helper addons:
+- Declare URLs to open after installation under the `addons` manifest entry (I plan to auto-install them in the future)
+- Easy way to write about:config changes without writing a `user.js` file:
+- Use the `config` manifest entry
+- If you also use a `user.js`, you can combine both, they'll be written as a single `user.js` file
+- Support for custom assets:
+- Use the `assets` manifest entry to list out your assets
+- Supports glob patterns
+- If you store them under a `chrome` directory, you can use `copy from: chrome/` so that they don't get copied to `<profile directory>/chrome/chrome`
+
+[Unreleased]: https://github.com/ewen-lbh/ffcss/compare/v0.2.0...HEAD
+[0.2.0]: https://github.com/ewen-lbh/ffcss/compare/v0.1.2...v0.2.0
+[0.1.2]: https://github.com/ewen-lbh/ffcss/compare/v0.1.1...v0.1.2
+[0.1.1]: https://github.com/ewen-lbh/ffcss/compare/v0.1.0...v0.1.1
+[0.1.0]: https://github.com/ewen-lbh/ffcss/releases/tag/v0.1.0
+
+[//]: # (C3-2-DKAC:GGH:Rewen-lbh/ffcss:Tv{t})
diff --git a/testdata/nomanifest/fffcss.yaml b/testdata/nomanifest/fffcss.yaml
new file mode 100644
index 0000000..3d0a321
--- /dev/null
+++ b/testdata/nomanifest/fffcss.yaml
@@ -0,0 +1,2 @@
+name: no u
+ffcss: 420
diff --git a/testdata/themeWithNoManifest.zip b/testdata/themeWithNoManifest.zip
new file mode 100644
index 0000000..e59a783
Binary files /dev/null and b/testdata/themeWithNoManifest.zip differ
diff --git a/themes/.future/.current-progress-firefoxcss-store.md b/themes/.future/.current-progress-firefoxcss-store.md
new file mode 100644
index 0000000..fa9fd68
--- /dev/null
+++ b/themes/.future/.current-progress-firefoxcss-store.md
@@ -0,0 +1,49 @@
+Themes left to do
+-----------------
+
+
+- [x] MartinFox
+- [x] Monterey-Fox
+- [x] pro-fox: enhanced firefox proton
+- [x] Chameleons-Beauty to Firefox CSS
+- [x] Technetium
+- [x] WaveFox
+- [x] Gradient rounded tabs redesign and ultra compact mode [Proton UI]
+- [x] [Firefox 90+] RainFox
+- [x] Lepton's Photon-Styled in Proton UI
+- [x] Lepton(Proton Fix) in Proton UI
+- [x] Firefox vertical tabs (TST) UI
+- [x] Australis like tabs in Proton UI
+- [x] DiamondFucsia Theme to Firefox Css
+- [x] Compact Mode to Firefox Css
+- [x] duskFox
+- [x] Alpen Blue to Firefox CSS
+- [x] WhiteSur Safari style for Firefox
+- [x] Pseudo-fullscreen Firefox, Sidebery and YouTube
+- [x] Moonlight 🌌
+- [ ] Elegant Nord Theme with material design
+- [ ] NicoFox
+- [ ] Edge-FrFox
+- [ ] Firefox i3wm theme
+- [ ] EdgeFox
+- [ ] FirefoxW10ContextMenus
+- [ ] Flying Fox
+- [ ] Monochrome Tree
+- [ ] Almost Dark Proton
+- [x] Firefox UWP Style
+- [x] MaterialFox
+- [ ] minimal-functional-fox
+- [ ] quietfox
+- [ ] Firefox-Mod-Blur
+- [ ] My Firefox Theme - not-holar
+- [ ] nord-firefox
+- [ ] Firefox Review
+- [x] blurredfox
+- [x] SimplerentFox
+- [ ] Simplify Darkish for Firefox
+- [ ] Simplify Silver Peach for Firefox
+- [ ] FirefoxCss
+- [ ] minimalFOX
+- [ ] FirefoxCSS-Plus
+- [ ] Sweet_Pop!
+- [ ] My Firefox theme - u/FffDtark
diff --git a/themes/.future/aminomancer.yaml b/themes/.future/aminomancer.yaml
new file mode 100644
index 0000000..23da237
--- /dev/null
+++ b/themes/.future/aminomancer.yaml
@@ -0,0 +1,5 @@
+# Needs: #44
+ffcss: 1
+
+# TODO
+download: https://github.com/aminomancer/uc.css.js
diff --git a/themes/.future/moonlight.yaml b/themes/.future/moonlight.yaml
new file mode 100644
index 0000000..0fb47dd
--- /dev/null
+++ b/themes/.future/moonlight.yaml
@@ -0,0 +1,36 @@
+# Needs: #47
+ffcss: 1
+
+name: moonlight
+download: https://github.com/eduardhojbota/moonlight-userChrome
+
+userChrome: userChrome.css
+userContent: userContent.css
+assets:
+  - '*.css'
+  - custom/**
+  - icons/**
+
+os:
+  linux: macos
+
+variants:
+  Re-enable title bar controls (MIN-MAX-CLOSE buttons):
+    modifications:
+      - in: userChrome.css
+        append: '@import "custom/_titlebar-controls-enable-{{ os }}.css";'
+  Disable megabar behavior:
+    modifications:
+      - in: userChrome.css
+        append: '@import "custom/_megabar-disable.css";'
+  Add Developer Tools styling:
+    modifications:
+      - in: userChrome.css
+        append: '@import "custom/_dev-tools.css";'
+    message:
+      Inspired by [moonlight-vscode-theme](https://github.com/atomiks/moonlight-vscode-theme) and [github-moonlight](https://github.com/Brettm12345/github-moonlight)
+
+      You can also set the theme to dark in Firefox Developer Tools settings.
+
+message: |
+  Inspired by [moonlight-vscode-theme](https://github.com/atomiks/moonlight-vscode-theme) and [github-moonlight](https://github.com/Brettm12345/github-moonlight)
diff --git a/themes/.future/pseudo-fullscreen.yaml b/themes/.future/pseudo-fullscreen.yaml
new file mode 100644
index 0000000..c716f8e
--- /dev/null
+++ b/themes/.future/pseudo-fullscreen.yaml
@@ -0,0 +1,59 @@
+# Needs: #48
+ffcss: 1
+
+name: pseudo-fullscreen
+download: https://github.com/ongots/pseudo-fullscreen-firefox
+
+config:
+  layout.css.backdrop-filter.enabled: true
+  gfx.webrender.enabled: true
+
+assets:
+  - chrome/**
+copy from: chrome/
+
+message: |
+  ## To complete the setup
+
+  1. Drag Bookmarks Toolbar Items to the left of the address bar. Disable Bookmarks Toolbar
+  1. To set the wallpaper in a new tab, place a .jpg or .png image in the Chrome folder and rename it to "bg". Restart Firefox
+
+variants:
+  default: {}
+  sidebery:
+    addons:
+      - https://addons.mozilla.org/firefox/addon/sidebery/
+    message:
+      append: | # TODO to append to root variant's instead of overriding
+        1. Copy the code from Sidebery-Sidebar.css to Sidebery / Settings / Styles editor / Sidebar
+        1. Copy the code from Sidebery-GroupPage.css to Sidebery / Settings / Styles editor / Group page
+  youtube:
+    addons:
+      - https://addons.mozilla.org/firefox/addon/styl-us/
+    message:
+      append: |
+        1. Open YouTube
+        1. In Stylus, click New Style ...
+        1. In the window that opens, click Import
+        1. Copy the code here from the Stylus-YouTube.css file and click replace style
+        1. Save
+
+components:
+  Open bookmark in new tab:
+    config:
+      browser.tabs.loadBookmarksInTabs: true
+  Open tabs to the right of the current one:
+    config:
+      browser.tabs.insertAfterCurrent: true
+  Scroll step:
+    config:
+      mousewheel.min_line_scroll_amount: 25
+  Smoothness of scrolling:
+    config:
+      general.smoothScroll.mouseWheel.durationMaxMS: 380
+  Disable sponsored sites:
+    config:
+      browser.newtabpage.activity-stream.showSponsored: false
+  Disable add-on recommendations:
+    config:
+      extensions.htmlaboutaddons.recommendations.enabled: false
diff --git a/themes/.future/rainfox.yaml b/themes/.future/rainfox.yaml
new file mode 100644
index 0000000..d33609e
--- /dev/null
+++ b/themes/.future/rainfox.yaml
@@ -0,0 +1,44 @@
+# Needs: #15
+ffcss: 1
+
+name: rainfox
+download: https://github.com/1280px/rainfox
+
+userChrome: userChrome.css
+
+firefox:
+  90+: {}
+  0-89:  # TODO maybe a syntax to 'up to ...?' (literally just allow 'up to 89' as a key)
+    tag: v0.3
+
+
+# TODO a way to glob 'everything except... (e.g, here, userChrome*.css except userChromeTabParanoia.css)
+assets:
+  - .icon/**
+  - userChromeAnimations.css
+  - userChromeBookmarks.css
+  - userChromeContextMenus.css
+  - userChromeDownloads.css
+  - userChromeHourglassLoading.css
+  - userChromeIcons.css
+  - userChromeMenubar.css
+  - userChromeNavButtons.css
+  - userChromeOptHider.css
+  - userChromePageCustomize.css
+  - userChromeTabContainers.css
+  - userChromeTabbar.css
+  - userChromeTabs.css
+  - userChromeURLbar.css
+  - userChromeVariables.css
+
+config:
+  svg.context-properties.content.enabled: true
+  security.secure_connection_icon_color_gray: false
+
+variants:
+  default: {}
+  paranoia:
+    description: Unselected tabs are hidden until you hover over them
+    assets:
+      - .icon/**
+      - userChrome*.css
diff --git a/themes/.future/uwp.yaml b/themes/.future/uwp.yaml
new file mode 100644
index 0000000..419ef46
--- /dev/null
+++ b/themes/.future/uwp.yaml
@@ -0,0 +1,30 @@
+# Needs: #15
+ffcss: 1
+
+name: UWP
+download: https://github.com/Guerra24/Firefox-UWP-Style
+
+# evolution of the firefox: entry,
+# now firefox: <number> means firefox: {<number>: {}}
+firefox:
+  nightly:
+    download: https://github.com/Guerra24/Firefox-UWP-Style/archive/refs/heads/master.zip
+  89-90:
+    download: https://github.com/Guerra24/Firefox-UWP-Style/archive/refs/heads/firefox-89.zip
+  '88':
+    download: https://github.com/Guerra24/Firefox-UWP-Style/archive/refs/heads/firefox-88.zip
+  76-87:
+    download: https://github.com/Guerra24/Firefox-UWP-Style/archive/refs/heads/firefox-76.zip
+
+variants:
+  default: {}
+  acrylic blur:
+    config:
+      layout.css.backdrop-filter.enabled: true
+
+userContent: userContent.css
+userChrome: userChrome.css
+assets:
+  - custom*.css
+  - css/**
+  - img/**
diff --git a/themes/.future/whitesur.yaml b/themes/.future/whitesur.yaml
new file mode 100644
index 0000000..d986c69
--- /dev/null
+++ b/themes/.future/whitesur.yaml
@@ -0,0 +1,52 @@
+# Needs: #47
+ffcss: 1
+
+name: whitesur
+download: https://github.com/adamxweb/WhiteSurFirefoxThemeMacOS
+
+userChrome: chrome/userChrome.css
+user.js: configuration/user.js
+
+assets:
+  - chrome/WhiteSur/**
+copy from: chrome/
+
+# TODO: assets: allow items of {from: string; to: string} objects, get rid of `copy from` (breaking change!)
+
+variants:
+  Left hand side tab close:
+    description: Put close button of tabs on the left
+    assets:
+      - from: custom/lhsclose.css
+        to: chrome/WhiteSur/parts/lhsclose.css
+    # TODO add modifications directive: in, after, before, append, prepend, replace, with, !regex tag for replace, in, before and after
+    modifications:
+      - in: chrome/WhiteSur/theme.css
+        append: '@import "parts/lhsclose.css"'
+        after: !regex '@namespace xul .*;'
+  Standard URL Bar:
+    assets:
+      - from: custom/standard-urlbar.css
+        to: chrome/WhiteSur/parts/standard-urlbar.css
+    modifications:
+      - in: chrome/WhiteSur/theme.css
+        append: '@import "parts/standard-urlbar.css"'
+        after: !regex '@namespace xul .*;'
+  Swap navbar close buttons:
+    description: Swap the close buttons, as well as to re-order the close button from MacOS styling to Windows
+    assets:
+      - from: custom/windows-swapclose.css
+        to: chrome/WhiteSur/parts/windows-swapclose.css
+    modifications:
+      - in: chrome/WhiteSur/theme.css
+        append: '@import "parts/windows-swapclose.css"'
+        after: !regex '@namespace xul .*;'
+  No Identity tab colours:
+    description: Remove the identity line from a container or facebook tab
+    assets:
+      - from: custom/noidentity.css
+        to: chrome/WhiteSur/parts/noidentity.css
+    modifications:
+      - in: chrome/WhiteSur/theme.css
+        append: '@import "parts/noidentity.css"'
+        after: !regex '@namespace xul .*;'
diff --git a/themes/alpenblue.yaml b/themes/alpenblue.yaml
new file mode 100644
index 0000000..c4aa887
--- /dev/null
+++ b/themes/alpenblue.yaml
@@ -0,0 +1,13 @@
+ffcss: 0
+
+name: alpenblue
+download: https://github.com/Godiesc/AlpenBlue
+
+
+user.js: user.js
+assets:
+  - chrome/**
+copy from: chrome/
+
+addons:
+  - https://addons.mozilla.org/en-US/firefox/addon/beautiful-alpen-blue/
diff --git a/themes/australis-tabs.yaml b/themes/australis-tabs.yaml
new file mode 100644
index 0000000..3723117
--- /dev/null
+++ b/themes/australis-tabs.yaml
@@ -0,0 +1,10 @@
+ffcss: 0
+
+name: australis-tabs
+download: https://github.com/sagars007/Australis-like-tabs-FF-ProtonUI-changes
+
+userChrome: userChrome.css
+user.js: user.js
+
+addons:
+  - https://addons.mozilla.org/en-US/firefox/addon/alpenglow-dark/
diff --git a/themes/blurredfox.yaml b/themes/blurredfox.yaml
new file mode 100644
index 0000000..c80ddd1
--- /dev/null
+++ b/themes/blurredfox.yaml
@@ -0,0 +1,12 @@
+ffcss: 0
+
+name: blurredfox
+download: https://github.com/manilarome/blurredfox
+
+user.js: user.js
+userChrome: userChrome.css
+userContent: userContent.css
+assets:
+  - colors/**
+  - parts/**
+  - scrots/**
diff --git a/themes/chameleons-beauty.yaml b/themes/chameleons-beauty.yaml
new file mode 100644
index 0000000..20efd13
--- /dev/null
+++ b/themes/chameleons-beauty.yaml
@@ -0,0 +1,20 @@
+ffcss: 0
+
+name: chameleons-beauty
+download: https://github.com/Godiesc/Chameleons-Beauty
+
+user.js: user.js
+
+assets:
+  - chrome/**
+
+copy from: chrome/
+
+addons:
+  - https://addons.mozilla.org/es/firefox/user/7024772/
+
+variants:
+  default: {}
+  dark:
+    addons:
+      - https://addons.mozilla.org/es/firefox/addon/beautiful-opera-gx-proton/
diff --git a/themes/compactmode.yaml b/themes/compactmode.yaml
new file mode 100644
index 0000000..12f49c9
--- /dev/null
+++ b/themes/compactmode.yaml
@@ -0,0 +1,16 @@
+ffcss: 0
+
+name: compactmode
+download: https://github.com/Godiesc/compactmodefirefoxcss
+
+user.js: user.js
+userChrome: chrome/userChrome.css
+
+addons:
+  - https://addons.mozilla.org/en-US/firefox/addon/beautifultotaldark/
+
+variants:
+  default: {}
+  photon-tabs:
+    user.js: OtherCss/user.js
+    userChrome: OtherCss/chrome/userChrome.css
diff --git a/themes/diamondfucsia.yaml b/themes/diamondfucsia.yaml
new file mode 100644
index 0000000..dbc6323
--- /dev/null
+++ b/themes/diamondfucsia.yaml
@@ -0,0 +1,8 @@
+ffcss: 0
+
+name: diamondfucsia
+download: https://github.com/Godiesc/DiamondFucsia
+
+
+user.js: user.js
+userChrome: chrome/userChrome.css
diff --git a/themes/flyingfox.yaml b/themes/flyingfox.yaml
new file mode 100644
index 0000000..6ccbe6e
--- /dev/null
+++ b/themes/flyingfox.yaml
@@ -0,0 +1,70 @@
+ffcss: 0
+
+name: flyingfox
+download: https://github.com/akshat46/FlyingFox
+
+user.js: user.js
+assets:
+  - chrome/**
+
+copy from: chrome/
+
+# components:
+#   Sidebar Static:
+#     description: Permanently expanded sidebar
+#     modifications:
+#       - in: config.css
+#         with: '@import "userChrome-static.css";'
+#         replace: '@import "userChrome-hover.css";'
+#   Sidebar Hover:
+#     description: Sidebar shown on mouse hover
+#     modifications:
+#       - in: config.css
+#         replace: '@import "userChrome-static.css";'
+#         with: '@import "userChrome-hover.css";'
+#   Extension Icons:
+#     description: >
+#       Supported extension icons are changed, and a mask(generated for custom color with webapp)
+#       is applied to unsupported ones to blend them better with rest of the theme.
+#     modifications:
+#       - in: config.css
+#         prepend: '@import "icons/extension-icons.css";'
+#   Right Window Controls:
+#     incompatible with:
+#       - component:horizontal-tabs
+#     description: Move window controls to the right
+#     modifications:
+#       - in: config.css
+#         prepend: |
+#           @import ''
+#   Horizontal tabs:
+#     description: Use a traditional horizontal tab line
+#     modifications:
+#       - in: config.css
+#         replace: '@import "hide-tabline.css";'
+#         with: ''
+#       - in: config.css
+#         replace: '@import "window-controls/wc-without-tabline.css";'
+#         with: ''
+#       - in: config.css
+#         replace: '@import "window-controls/wc-wt-windowspatch.css";'
+#         with: ''
+#
+# if:
+#   # os:windows and not component:horizontal-tabs and ask:Are you running Windows 10:
+#   windows:10 and not component:horizontal-tabs:
+#     modifications:
+#       - in: config.css
+#         prepend: '@import "window-controls/wc-wt-windowspatch.css";'
+
+message: |
+  New firefox themes _alpenglow_ and _default_ aren't supported.
+
+#  If you are on Windows 10 and did not use Horizontal Tabs,
+#  please add
+#
+#  ```css
+#  @import "window-controls/wc-wt-windowspatch.css";
+#  ```
+#
+#  to **config.css**
diff --git a/themes/frozenfox.yaml b/themes/frozenfox.yaml
new file mode 100644
index 0000000..5a0dfe2
--- /dev/null
+++ b/themes/frozenfox.yaml
@@ -0,0 +1,15 @@
+ffcss: 0
+
+name: frozenfox
+download: https://github.com/tortious/FrozenFox
+
+userChrome: userChrome.css
+userContent: userContent.css
+
+assets:
+  - bindings.xml
+  - userChrome.js
+  - userChrome.xml
+  - 000_xPref.uc.js
+  - saveUCJS.uc.js
+  - Png/**
diff --git a/themes/fxcompact.yaml b/themes/fxcompact.yaml
new file mode 100644
index 0000000..fc4be50
--- /dev/null
+++ b/themes/fxcompact.yaml
@@ -0,0 +1,9 @@
+ffcss: 0
+
+name: fxcompact
+download: https://github.com/dannycolin/fx-compact-mode
+
+config:
+  browser.uidensity: 1
+
+userChrome: userChrome.css
diff --git a/themes/halo.yaml b/themes/halo.yaml
new file mode 100644
index 0000000..6495839
--- /dev/null
+++ b/themes/halo.yaml
@@ -0,0 +1,13 @@
+ffcss: 0
+
+name: halo
+download: https://github.com/seirin-blu/Firefox-Halo
+
+userChrome: userChrome.css
+userContent: userContent.css
+assets:
+  - icon/**
+  - svg/**
+  - Firefox assets/**
+  - '*.css'
+  - firefox_*.png
diff --git a/themes/lepton.yaml b/themes/lepton.yaml
new file mode 100644
index 0000000..3fd9e2a
--- /dev/null
+++ b/themes/lepton.yaml
@@ -0,0 +1,18 @@
+ffcss: 0
+
+name: lepton
+download: https://github.com/black7375/Firefox-UI-Fix
+
+user.js: user.js
+userChrome: userChrome.css
+userContent: userContent.css
+
+asssets:
+  - icons/**
+
+variants:
+  default: {}
+  photon-style:
+    branch: photon-style
+  proton-style:
+    branch: proton-style
diff --git a/themes/martinfox.yaml b/themes/martinfox.yaml
new file mode 100644
index 0000000..b0575bb
--- /dev/null
+++ b/themes/martinfox.yaml
@@ -0,0 +1,6 @@
+ffcss: 0
+
+name: martinfox
+download: https://github.com/arp242/MartinFox
+
+userChrome: userChrome.css
diff --git a/themes/materialfox-plus.yaml b/themes/materialfox-plus.yaml
new file mode 100644
index 0000000..dc183b1
--- /dev/null
+++ b/themes/materialfox-plus.yaml
@@ -0,0 +1,18 @@
+ffcss: 0
+
+name: materialfox-plus
+download: https://github.com/typeling1578/MaterialFox-Plus
+
+config:
+  svg.context-properties.content.enabled: true
+  browser.tabs.tabClipWidth: 83
+  materialFox.reduceTabOverflow: true
+  security.insecure_connection_text.enabled: true
+
+userChrome: chrome/userChrome.css
+userContent: null
+user.js: null
+assets:
+  - chrome/**
+
+copy from: chrome/
diff --git a/themes/materialfox.yaml b/themes/materialfox.yaml
new file mode 100644
index 0000000..4974950
--- /dev/null
+++ b/themes/materialfox.yaml
@@ -0,0 +1,18 @@
+ffcss: 0
+
+name: materialfox
+download: https://github.com/muckSponge/MaterialFox
+
+config:
+  svg.context-properties.content.enabled: true
+  browser.tabs.tabClipWidth: 83
+  materialFox.reduceTabOverflow: true
+  security.insecure_connection_text.enabled: true
+
+userChrome: chrome/userChrome.css
+userContent: null
+user.js: user.js
+assets:
+  - chrome/**
+
+copy from: chrome/
diff --git a/themes/modoki.yaml b/themes/modoki.yaml
new file mode 100644
index 0000000..f5c2ceb
--- /dev/null
+++ b/themes/modoki.yaml
@@ -0,0 +1,12 @@
+ffcss: 0
+
+name: modoki
+download: https://github.com/soup-bowl/Modoki-FirefoxCSS
+
+assets:
+  - IE6/chrome/**
+
+copy from: IE6/chrome/
+
+message:
+  In Customize, turn on Title Bar and enable Menu bar under toolbars.
diff --git a/themes/montereyfox.yaml b/themes/montereyfox.yaml
new file mode 100644
index 0000000..0e99ebe
--- /dev/null
+++ b/themes/montereyfox.yaml
@@ -0,0 +1,9 @@
+ffcss: 0
+
+name: montereyfox
+download: https://github.com/FirefoxCSSThemers/Monterey-Fox
+
+assets:
+  - chrome/**
+
+copy from: chrome/
diff --git a/themes/pro-fox.yaml b/themes/pro-fox.yaml
new file mode 100644
index 0000000..f94e5a4
--- /dev/null
+++ b/themes/pro-fox.yaml
@@ -0,0 +1,6 @@
+ffcss: 0
+
+name: pro-fox
+download: https://github.com/xmansyx/Pro-Fox
+
+userChrome: userChrome.css
diff --git a/themes/proton-connected-rounded-tabs.yaml b/themes/proton-connected-rounded-tabs.yaml
new file mode 100644
index 0000000..bf7d553
--- /dev/null
+++ b/themes/proton-connected-rounded-tabs.yaml
@@ -0,0 +1,7 @@
+ffcss: 0
+
+name: proton-connected-rounded-tabs
+download: https://github.com/sagars007/Proton-UI-connected-rounded-tabs
+
+userChrome: userChrome.css
+user.js: user.js
diff --git a/themes/simplerentfox.yaml b/themes/simplerentfox.yaml
new file mode 100644
index 0000000..80fc5e9
--- /dev/null
+++ b/themes/simplerentfox.yaml
@@ -0,0 +1,52 @@
+ffcss: 0
+
+name: simplerentfox
+download: https://github.com/MiguelRAvila/SimplerentFox
+
+config:
+  layers.acceleration.force-enabled: true
+  gfx.webrender.all: true
+  svg.context-properties.content.enabled: true
+
+variants:
+  HideURLBar: {}
+  WithURLBar: {}
+  OneLine: {}
+
+os:
+  linux: Linux
+  macos: Linux
+  windows: Windows
+
+userContent: ./{{ os }}/userContent.css
+userChrome: ./{{ os }}/userChrome__{{ variant }}.css
+
+addons:
+  - https://addons.mozilla.org/en-US/firefox/addon/simplerentfox/
+
+# without {{ variables }}:
+# files:
+#  windows:
+#  - Windows/userContent.css
+#  linux:
+#  - Linux/userContent.css
+#
+# variants:
+#   WithURLBar:
+#     files:
+#       windows:
+#       - Windows/userChrome__WithURLBar.css
+#       linux:
+#       - Linux/userChrome__WithURLBar.css
+#   WithoutURLBar:
+#     files:
+#       windows:
+#       - Windows/userChrome__WithoutURLBar.css
+#       linux:
+#       - Linux/userChrome__WithoutURLBar.css
+#   OneLine:
+#     files:
+#       windows:
+#       - Windows/userChrome__WithURLBar.css
+#       linux:
+#       - Linux/userChrome__OneLWithURLBar
diff --git a/themes/technetium.yaml b/themes/technetium.yaml
new file mode 100644
index 0000000..23395c4
--- /dev/null
+++ b/themes/technetium.yaml
@@ -0,0 +1,22 @@
+ffcss: 0
+
+name: technetium
+
+download: https://github.com/edo0/Technetium
+
+user.js: user.js
+assets:
+  - chrome/**
+copy from: chrome/
+
+# TODO: variant for https://github.com/edo0/Technetium#really-missing-tab-separators
+# Needs: #47
+
+# variants:
+#   Default: {}
+#   Tab separators:
+#     description: Restore tab separators
+#     modifications:
+#     - in: chrome/userChrome.css
+#       prepend: !fromfile https://pastebin.com/raw/yHggpULx
+#       before: !regex ^}$
diff --git a/themes/verticaltabs.yaml b/themes/verticaltabs.yaml
new file mode 100644
index 0000000..c747889
--- /dev/null
+++ b/themes/verticaltabs.yaml
@@ -0,0 +1,21 @@
+ffcss: 0
+
+name: verticaltabs
+download: https://github.com/ranmaru22/firefox-vertical-tabs
+
+userChrome: userChrome.css
+
+config:
+  dom.animations-api.getAnimations.enabled: true # from https://framagit.org/ariasuni/tabcenter-reborn/-/wikis/home
+
+addons:
+  - https://addons.mozilla.org/en-US/firefox/addon/tabcenter-reborn/
+
+message: |
+  Go to about:addons, select Tab Center Reborn, go to Preferences and set:
+
+  - *Animations*: on.
+  - *Use current browser theme*: on, if you want to use dark mode.
+  - *Compact Mode*: either “Dynamic” or “Enabled”. It works with “Disabled” too but looks nicer with only favicons.
+  - *Favicon-only pinned tabs*: off.
+  - *Activate Custom Stylesheet* and paste the contents of tabCenterReborn.css into the text area, and click “Save CSS”.
diff --git a/themes/wavefox.yaml b/themes/wavefox.yaml
new file mode 100644
index 0000000..40d1e9c
--- /dev/null
+++ b/themes/wavefox.yaml
@@ -0,0 +1,16 @@
+ffcss: 0
+
+name: wavefox
+description: |
+  A theme with a unique shape of tabs
+
+  _Compatible with touch, normal & compact densities_
+
+download: https://github.com/QNetITQ/WaveFox
+
+config:
+  svg.context-properties.content.enabled: true
+  layout.css.color-mix.enabled: true
+  browser.tabs.tabMinWith: 94
+
+userChrome: userChrome.css
diff --git a/tools/make_release_notes.rb b/tools/make_release_notes.rb
new file mode 100755
index 0000000..3977d45
--- /dev/null
+++ b/tools/make_release_notes.rb
@@ -0,0 +1,51 @@
+#!/usr/bin/env ruby
+$; = $, = "\n"
+arg = ARGV[0]
+
+major, minor, patch = `ffcss version`.split '.'
+
+case arg
+when "major"
+	new_major, new_minor, new_patch = major.to_i + 1, 0, 0
+when "minor"
+	new_major, new_minor, new_patch = major, minor.to_i + 1, 0
+when "patch"
+	new_major, new_minor, new_patch = major, minor, patch.to_i + 1
+else; if arg =~ /\d+.\d+.\d+/
+	new_major, new_minor, new_patch = arg.split '.'
+else
+	puts "invalid argument #{arg}"
+	exit 1
+end
+end
+
+
+ffcss_dot_go = File.open "ffcss.go"
+
+File.write "ffcss.go", (
+	ffcss_dot_go.read
+		.sub(/VersionMajor = \d+/, "VersionMajor = #{new_major}")
+		.sub(/VersionMinor = \d+/, "VersionMinor = #{new_minor}")
+		.sub(/VersionPatch = \d+/, "VersionPatch = #{new_patch}")
+)
+
+if not ["major", "minor", "patch"].include? arg
+	exit 0
+end
+
+lines = File.open("CHANGELOG.md").read.split
+release_notes = []
+
+
+until lines.length == 0 or lines.shift =~ /^## \[#{new_major}.#{new_minor}.#{new_patch}\] - \d{4}-\d{2}-\d{2}$/ do
+	# nothing
+end
+
+until lines.length == 0 or lines[0] =~ /^## \[\d+\.\d+\.\d+\] - \d{4}-\d{2}-\d{2}$/ do
+	release_notes << lines.shift
+end
+
+
+
+
+File.write 'release_notes.md', release_notes.join
diff --git a/tools/movefunc.rb b/tools/movefunc.rb
new file mode 100755
index 0000000..7dcb519
--- /dev/null
+++ b/tools/movefunc.rb
@@ -0,0 +1,48 @@
+#!/usr/bin/env ruby
+require "pathname"
+require "tty-prompt"
+
+name = ARGV[0]
+from = Pathname.new ARGV[1]
+to = Pathname.new ARGV[2]
+
+body = []
+doc = []
+start_at = end_at = 0
+
+lines = File.read(from).split "\n"
+
+signature_at = lines.find_index { |line| /^func\s+(\([^\)]+\)\s+)?\s*#{name}\s*\([^\)]*\).+\{$/i =~ line }
+
+if signature_at.nil?
+	puts "signature for #{name} not found"
+	exit 1
+end
+
+# find documentation
+lines[...signature_at].reverse.each_with_index do |line, i|
+  start_at = signature_at - (i + 1)
+  if line.start_with? "//"
+    doc.prepend line
+    if line.start_with? "// #{name}"
+      break
+    end
+  else
+    break
+  end
+end
+
+# go thru body
+lines[signature_at..].each_with_index do |line, at|
+  body << line
+  end_at = signature_at + at
+  # closing brace w/o indetation means end of func.
+  # deal with it, just gofmt your code.
+  if line == "}"
+    break
+  end
+end
+
+to.write (([""] + doc + body).join "\n"), to.size, mode: "a"
+from.write (lines[...start_at] + lines[end_at + 1..]).join "\n"
+`make format`
diff --git a/tools/ootb-themes.rb b/tools/ootb-themes.rb
new file mode 100755
index 0000000..2ea0b4c
--- /dev/null
+++ b/tools/ootb-themes.rb
@@ -0,0 +1,14 @@
+#!/usr/bin/env ruby
+require "yaml"
+
+major_version = `ffcss version major`.strip.to_i
+
+Dir.glob('themes/*.yaml').each do |filename|
+	theme = YAML.load_file filename, symbolize_names: true
+	if theme[:ffcss] != major_version
+		return
+	end
+	parts = theme[:download].split '/'
+	username = parts.length >= 2 ? parts[-2] : nil
+	puts "- [#{theme[:name]}](#{theme[:download]})" + (username ? " by [#{username}](https://github.com/#{username})" : "")
+end
diff --git a/tools/untested.rb b/tools/untested.rb
new file mode 100755
index 0000000..624a047
--- /dev/null
+++ b/tools/untested.rb
@@ -0,0 +1,61 @@
+#!/usr/bin/env ruby
+require "pathname"
+tested = []
+untested = []
+
+files = Dir.glob "*.go"
+testfiles = Dir.glob "*_test.go"
+function_pattern = /^func\s+(\(\s*\w+\s+(?<receiver>[^)\s]+)\s*\)\s+)?\s*(?<name>\w+).+\{$/
+
+testfiles.each do |file|
+  lines = Pathname.new(file).read.split "\n"
+  lines.each do |line|
+    match = line.match function_pattern
+    if match != nil and match[:name].start_with? "Test"
+      tested << match[:name].delete_prefix("Test")
+    end
+  end
+end
+
+files.each do |file|
+  lines = Pathname.new(file).read.split "\n"
+  lines.each_with_index do |line, i|
+    match = line.match function_pattern
+    if match != nil and not match[:name].start_with? "Test" and match[:name] !~ /main|init/
+      if not tested.find { |func| func.downcase == match[:name].downcase } and not file.end_with? "_test.go"
+        untested << {
+          name: (if match[:receiver] then "(#{match[:receiver]})." else "" end) + match[:name],
+          # name: match[:name],
+          location: "#{file}:#{i + 1}",
+        }
+      end
+    end
+  end
+end
+
+def align(untested, func, key, right: false)
+  longest_length = untested.map { |f| f[key].length }.max
+  if right
+    func[key].rjust longest_length
+  else
+    func[key].ljust longest_length
+  end
+end
+
+if untested.size == 0
+  puts "everything's tested"
+  exit 0
+end
+
+untested.each do |func|
+  puts (align untested, func, :location) + " "  + (align untested, func, :name)
+end
+
+puts
+puts (if untested.size > 1
+  "#{untested.size} functions"
+else
+  "one function"
+end) + ", " + (Float(untested.size) / (untested.size + tested.size) * 100).round.to_s + "%"
+
+exit 1
diff --git a/tools/wherefunc.rb b/tools/wherefunc.rb
new file mode 100755
index 0000000..e0ca71b
--- /dev/null
+++ b/tools/wherefunc.rb
@@ -0,0 +1,18 @@
+#!/usr/bin/env ruby
+require "pathname"
+
+name = ARGV[0]
+files = Dir.glob "*.go"
+
+files.each do |file|
+	lines = Pathname.new(file).read.split"\n"
+	lines.each do |line|
+		signature_at = lines.find_index { |line| /^func\s+(\([^\)]+\)\s+)?\s*#{name}\s*\([^\)]*\).+\{$/ =~ line }
+		if signature_at != nil
+			puts "#{file}:#{signature_at+1}"
+			exit 0
+		end
+	end
+end
+
+exit 1
diff --git a/ui.go b/ui.go
new file mode 100644
index 0000000..67366a3
--- /dev/null
+++ b/ui.go
@@ -0,0 +1,300 @@
+package ffcss
+
+import (
+	"fmt"
+	"os"
+	"regexp"
+	"strings"
+
+	"github.com/AlecAivazis/survey/v2"
+	chromaQuick "github.com/alecthomas/chroma/quick"
+	"github.com/charmbracelet/glamour"
+	"github.com/mitchellh/colorstring"
+)
+
+const indent = "  "
+
+// BulletColorsByIndentLevel maps (the "key" is the element's index in the array) an indentation level (see LogStep, for example)
+// to a color.
+var BulletColorsByIndentLevel = []string{
+	"blue",
+	"cyan",
+	"green",
+	"yellow",
+	"orange",
+	"red",
+	"magenta",
+	"white",
+}
+
+var colorizer colorstring.Colorize
+
+func init() {
+	colorizer.Colors = colorstring.DefaultColors
+	colorizer.Colors["italic"] = "3"
+	colorizer.Colors["orange"] = "38;2;241;109;12"
+	colorizer.Reset = true
+}
+
+func printf(s string, args ...interface{}) {
+	fmt.Fprintf(out, s, args...)
+}
+
+func printfln(s string, args ...interface{}) {
+	printf(s+"\n", args...)
+}
+
+// DescribeTheme shows the introduction message before installation
+func DescribeTheme(theme Theme, indentLevel uint) {
+	printf("\n")
+	indentation := strings.Repeat(indent, int(indentLevel))
+
+	var author string
+	urlParts := strings.Split(theme.DownloadAt, "/")
+	LogDebug("urlParts is %#v", urlParts)
+	if theme.Author != "" {
+		author = theme.Author
+	} else if strings.Contains(theme.DownloadAt, "github.com") && len(urlParts) == 5 {
+		author = urlParts[len(urlParts)-2]
+	}
+
+	printf(indentation)
+
+	printf(
+		colorizer.Color("[dim]Installing ") +
+			colorizer.Color("[blue][bold]"+theme.Name()),
+	)
+
+	if regexp.MustCompile(`^v([0-9\.]+)$`).MatchString(theme.Tag) {
+		printf(colorstring.Color(" [blue]" + theme.Tag))
+	}
+
+	if author != "" {
+		printf(
+			colorizer.Color("[dim][italic] by ") +
+				colorizer.Color("[blue][italic]"+author),
+		)
+	}
+
+	if theme.Description != "" {
+		printf("\n")
+		gutter := colorstring.Color(indentation + "[blue]│")
+		LogDebug("gutter is %q", gutter)
+		markdownRendered, err := glamour.Render(theme.Description, "dark")
+		if err != nil {
+			markdownRendered = theme.Description
+		}
+		printf("\n")
+		LogDebug("splitted is %#v", strings.Split(markdownRendered, "\n"))
+		for _, line := range strings.Split(markdownRendered, "\n") {
+			if strings.TrimSpace(line) == "" {
+				continue
+			}
+			printfln(gutter + strings.TrimSpace(line))
+		}
+		printf("\n")
+	} else {
+		printf("\n\n")
+	}
+
+}
+
+func showManifestSource(theme Theme) {
+	printf("\n")
+	printfln(colorizer.Color("[italic][dim]" + theme.Name() + "'s manifest"))
+	chromaQuick.Highlight(os.Stdout, theme.raw, "YAML", "terminal16m", "pygments")
+	printf("\n")
+}
+
+func plural(singular string, amount int, optionalPlural ...string) string {
+	var plural string
+	switch len(optionalPlural) {
+	case 1:
+		plural = optionalPlural[0]
+	case 0:
+		plural = singular + "s"
+	default:
+		panic("plural expected 2 or 3 arguments, you gave more")
+	}
+	if amount == 1 {
+		return singular
+	}
+	return plural
+}
+
+// LogDebug prints a debug log line. This one always prints to the real stdout, ignoring a possibly mocked stdout
+func LogDebug(s string, fmtArgs ...interface{}) {
+	if os.Getenv("DEBUG") != "" {
+		fmt.Printf(colorizer.Color("[dim][ DEBUG ] "+s+"\n"), fmtArgs...)
+	}
+}
+
+// LogWarning prints a log line with "warning" styling
+func LogWarning(s string, fmtArgs ...interface{}) {
+	printf(colorizer.Color("[yellow][bold]"+s+"\n"), fmtArgs...)
+}
+
+// LogError is like warn but with "error" styling
+func LogError(s string, fmtArgs ...interface{}) {
+	printf(colorizer.Color("[red][bold]"+s+"\n"), fmtArgs...)
+}
+
+// LogStep displays a list item
+func LogStep(indentLevel uint, item string, fmtArgs ...interface{}) {
+	LogStepC("•", indentLevel, item, fmtArgs...)
+}
+
+// LogStepC is like Step, but the bullet point characters is customizable
+func LogStepC(bulletChar string, indentLevel uint, item string, fmtArgs ...interface{}) {
+	indentLevel += BaseIndentLevel
+	var color string
+	if int(indentLevel) > len(BulletColorsByIndentLevel)-1 {
+		color = BulletColorsByIndentLevel[len(BulletColorsByIndentLevel)-1]
+	} else {
+		color = BulletColorsByIndentLevel[indentLevel]
+	}
+
+	bullet := strings.Repeat(indent, int(indentLevel)) +
+		colorizer.Color("["+color+"]"+bulletChar)
+
+	printfln(bullet + " " + colorizer.Color(strings.TrimSpace(fmt.Sprintf(item, fmtArgs...))))
+}
+
+// Display is like String, but adds terminal ANSI sequences for some color.
+func (ffp FirefoxProfile) Display() string {
+	return colorizer.Color(fmt.Sprintf("[bold]%s [reset][dim](%s)", ffp.Name, ffp.ID))
+}
+
+// AskProfiles prompts the user to select one or more profiles from the given array, and returns the user's chosen profiles.
+func AskProfiles(profiles []FirefoxProfile) []FirefoxProfile {
+	var selectedProfiles []FirefoxProfile
+
+	// XXX the whole display thing should be put in survey.MultiSelect.Renderer, look into that.
+	selectedProfileDirsDisplay := make([]string, 0)
+
+	LogStep(0, "Please select profiles to apply the theme on")
+
+	profileDirsDisplay := make([]string, 0)
+	for _, profile := range profiles {
+		profileDirsDisplay = append(profileDirsDisplay, profile.Display())
+	}
+
+	survey.AskOne(&survey.MultiSelect{
+		Message: "Select profiles",
+		Options: profileDirsDisplay,
+		VimMode: vimModeEnabled(),
+	}, &selectedProfileDirsDisplay)
+
+	for _, chosenProfileDisplay := range selectedProfileDirsDisplay {
+		selectedProfiles = append(selectedProfiles, NewFirefoxProfileFromDisplay(chosenProfileDisplay, profiles))
+	}
+
+	return selectedProfiles
+}
+
+// AskToSeeManifestSource prompts the user to display the theme's manifest, and, if the user accepts, displays it.
+func (t Theme) AskToSeeManifestSource(skip bool) {
+	wantsSource := false
+	if !skip {
+		survey.AskOne(&survey.Confirm{
+			Message: "Show the manifest source?",
+		}, &wantsSource)
+	}
+	if wantsSource {
+		showManifestSource(t)
+	}
+}
+
+// ChooseVariant asks the user to choose a variant.
+// If the users interrupts the prompt (by e.g. pressing Ctrl-C), cancel is true.
+// Else, the selected variant is returned and cancel is false.
+// If no variants are available, the empty variant is returned and cancel is false (and the user does not get prompted).
+func (t Theme) ChooseVariant() (chosen Variant, cancel bool) {
+	var variantName string
+	if len(t.AvailableVariants()) > 0 {
+		LogStep(0, "Please choose the theme's variant")
+		variantPrompt := &survey.Select{
+			Message: "Install variant",
+			Options: t.AvailableVariants(),
+			VimMode: vimModeEnabled(),
+		}
+		survey.AskOne(variantPrompt, &variantName)
+		// user Ctrl-C'd
+		if variantName == "" {
+			return Variant{}, true
+		}
+		return t.Variants[variantName], false
+	}
+	return Variant{}, false
+}
+
+// ConfirmInstallAddons asks the user to confirm the installation of addons.
+func ConfirmInstallAddons(addons []string) bool {
+	acceptOpenExtensionPages := false
+	survey.AskOne(&survey.Confirm{
+		Message: fmt.Sprintf("This theme suggests installing %d %s. Open %s?",
+			len(addons),
+			plural("addon", len(addons)),
+			plural("its page", len(addons), "their pages"),
+		),
+		Default: acceptOpenExtensionPages,
+	}, &acceptOpenExtensionPages)
+	return acceptOpenExtensionPages
+}
+
+// ShowHookOutput displays the given output text with additional horizontal and vertical padding
+func ShowHookOutput(output string) {
+	fmt.Fprint(
+		out,
+		"\n",
+		prefixEachLine(
+			strings.TrimSpace(output),
+			strings.Repeat(indent, int(BaseIndentLevel)+2),
+		),
+		"\n",
+		"\n",
+	)
+}
+
+// DisplayErrorMessage displays a nested error message (split on colons)
+func DisplayErrorMessage(err error) {
+	for idx, errorFragment := range strings.Split(err.Error(), ":") {
+		LogStep(uint(idx), errorFragment)
+	}
+}
+
+// SelectProfiles returns an array of FirefoxProfile:
+//
+//    If selected is non-empty, it parses the paths into an array of FirefoxProfile
+//    Else, it returns all profiles if all is true
+//    Else, it asks the user to select one or more profiles and returns those
+func SelectProfiles(selected []string, dir string, useDefault bool, all bool) ([]FirefoxProfile, error) {
+	var selectedProfiles []FirefoxProfile
+	if len(selected) > 0 {
+		for _, profilePath := range selected {
+			selectedProfiles = append(selectedProfiles, NewFirefoxProfileFromPath(profilePath))
+		}
+	} else {
+		LogStep(0, "Getting profiles")
+		profiles, err := Profiles(dir)
+		if err != nil {
+			return []FirefoxProfile{}, fmt.Errorf("couldn't get profile directories: %w", err)
+		}
+		if useDefault {
+			for _, profile := range profiles {
+				if profile.Name == "default-release" {
+					return []FirefoxProfile{profile}, nil
+				}
+			}
+		}
+		// Choose profiles
+		// TODO smart default (based on {{profileDirectory}}/times.json:firstUse)
+		if all {
+			LogStep(0, "Selecting all profiles")
+			selectedProfiles = profiles
+		} else {
+			selectedProfiles = AskProfiles(profiles)
+		}
+	}
+	return selectedProfiles, nil
+}
diff --git a/ui_test.go b/ui_test.go
new file mode 100644
index 0000000..485f9bd
--- /dev/null
+++ b/ui_test.go
@@ -0,0 +1 @@
+package ffcss
diff --git a/userprefs.go b/userprefs.go
new file mode 100644
index 0000000..04830de
--- /dev/null
+++ b/userprefs.go
@@ -0,0 +1,40 @@
+package ffcss
+
+import (
+	"encoding/json"
+	"fmt"
+	"regexp"
+
+	"strings"
+)
+
+// UserJSFileContent returns a string of JS source code that represents the theme's config.
+// It can be used directly to write a user.js file.
+func (t Theme) UserJSFileContent() (string, error) {
+	lines := make([]string, 0)
+	for name, value := range t.Config {
+		valueJSON, err := json.Marshal(value)
+		if err != nil {
+			return "", fmt.Errorf("can't serialize %#v: %w", value, err)
+		}
+		lines = append(lines, fmt.Sprintf(`user_pref(%q, %s);`, name, string(valueJSON)))
+	}
+	return strings.Join(lines, "\n"), nil
+}
+
+// ValueOfUserPrefCall returns the value of configuration entry, given its key and the contents of
+// the prefs.js file. It only works if the value is a JSON-parsable literal (string, number, boolean, null, etc.).
+func ValueOfUserPrefCall(prefsJSContent []byte, key string) (string, error) {
+	pattern := regexp.MustCompile(`(?m)^\s*user_pref\("` + key + `"\s*,\s*(.+)\)\s*;?\s*$`)
+	matches := pattern.FindAllSubmatch(prefsJSContent, -1)
+	if len(matches) == 0 {
+		return "", fmt.Errorf("key %q not found", key)
+	}
+	raw := matches[len(matches)-1][1]
+	var jsonParsed interface{}
+	err := json.Unmarshal(raw, &jsonParsed)
+	if err != nil {
+		return "", fmt.Errorf("while intepreting value %q: %w", string(raw), err)
+	}
+	return fmt.Sprint(jsonParsed), nil
+}
diff --git a/userprefs_test.go b/userprefs_test.go
new file mode 100644
index 0000000..3adc6e2
--- /dev/null
+++ b/userprefs_test.go
@@ -0,0 +1,230 @@
+package ffcss
+
+import (
+	"sort"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var (
+	prefsContent = []byte(`
+// Mozilla User Preferences
+
+// DO NOT EDIT THIS FILE.
+//
+// If you make changes to this file while the application is running,
+// the changes will be overwritten when the application exits.
+//
+// To change a preference value, you can either:
+// - modify it via the UI (e.g. via about:config in the browser); or
+// - set it within a user.js file in your profile.
+
+user_pref("app.normandy.first_run", false);
+user_pref("app.normandy.migrationsApplied", 12);
+user_pref("app.normandy.startupRolloutPrefs.browser.partnerlink.useAttributionURL", true);
+user_pref("app.normandy.startupRolloutPrefs.browser.topsites.contile.enabled", true);
+user_pref("app.normandy.startupRolloutPrefs.browser.topsites.experiment.ebay-2020-1", true);
+user_pref("app.normandy.startupRolloutPrefs.browser.topsites.useRemoteSetting", true);
+user_pref("app.normandy.user_id", "ca21ac2d-9836-4135-8643-90baefe2bcce");
+user_pref("app.update.lastUpdateTime.addon-background-update-timer", 1626868179);
+user_pref("app.update.lastUpdateTime.browser-cleanup-thumbnails", 1626957215);
+user_pref("app.update.lastUpdateTime.recipe-client-addon-run", 1626867939);
+user_pref("app.update.lastUpdateTime.region-update-timer", 1626627835);
+user_pref("app.update.lastUpdateTime.rs-experiment-loader-timer", 1626957492);
+user_pref("app.update.lastUpdateTime.search-engine-update-timer", 1626957335);
+user_pref("app.update.lastUpdateTime.services-settings-poll-changes", 1626868059);
+user_pref("app.update.lastUpdateTime.telemetry_modules_ping", 1626624835);
+user_pref("app.update.lastUpdateTime.xpi-signature-verification", 1626868299);
+user_pref("browser.aboutConfig.showWarning", false);
+user_pref("browser.bookmarks.addedImportButton", true);
+user_pref("browser.bookmarks.restore_default_bookmarks", false);
+user_pref("browser.compactmode.show", true);
+user_pref("browser.contentblocking.category", "standard");
+user_pref("browser.contextual-services.contextId", "{9974547d-fdc8-4671-866c-b1138fcfca4b}");
+user_pref("browser.download.panel.shown", true);
+user_pref("browser.download.viewableInternally.typeWasRegistered.svg", true);
+user_pref("browser.download.viewableInternally.typeWasRegistered.webp", true);
+user_pref("browser.download.viewableInternally.typeWasRegistered.xml", true);
+user_pref("browser.laterrun.bookkeeping.profileCreationTime", 1625838587);
+user_pref("browser.laterrun.bookkeeping.sessionCount", 28);
+user_pref("browser.laterrun.enabled", true);
+user_pref("browser.migration.version", 109);
+user_pref("browser.newtabpage.activity-stream.impressionId", "{3238e08e-13c5-4a46-8248-d6e73a0a7cea}");
+user_pref("browser.newtabpage.pinned", "[]");
+user_pref("browser.newtabpage.storageVersion", 1);
+user_pref("browser.pageActions.persistedActions", "{\"ids\":[\"bookmark\"],\"idsInUrlbar\":[\"bookmark\"],\"idsInUrlbarPreProton\":[],\"version\":1}");
+user_pref("browser.pagethumbnails.storage_version", 3);
+user_pref("browser.proton.toolbar.version", 3);
+user_pref("browser.region.update.updated", 1626627836);
+user_pref("browser.rights.3.shown", true);
+user_pref("browser.safebrowsing.provider.mozilla.lastupdatetime", "1626957187007");
+user_pref("browser.safebrowsing.provider.mozilla.nextupdatetime", "1626978787007");
+user_pref("browser.search.region", "FR");
+user_pref("browser.sessionstore.upgradeBackup.latestBuildID", "20210714020445");
+user_pref("browser.shell.defaultBrowserCheckCount", 15);
+user_pref("browser.shell.didSkipDefaultBrowserCheckOnFirstRun", true);
+user_pref("browser.startup.homepage_override.buildID", "20210714020445");
+user_pref("browser.startup.homepage_override.mstone", "90.0");
+user_pref("browser.startup.lastColdStartupCheck", 1626957491);
+user_pref("browser.startup.upgradeDialog.version", 89);
+user_pref("browser.tabs.tabClipWidth", 83);
+user_pref("browser.tabs.tabMinWith", 94);
+user_pref("browser.tabs.warnOnClose", false);
+user_pref("browser.theme.toolbar-theme", 0);
+user_pref("browser.toolbars.bookmarks.visibility", "always");
+user_pref("browser.uiCustomization.state", "{\"placements\":{\"widget-overflow-fixed-list\":[],\"nav-bar\":[\"back-button\",\"forward-button\",\"stop-reload-button\",\"customizableui-special-spring1\",\"urlbar-container\",\"customizableui-special-spring2\",\"save-to-pocket-button\",\"downloads-button\",\"fxa-toolbar-menu-button\"],\"toolbar-menubar\":[\"menubar-items\"],\"TabsToolbar\":[\"tabbrowser-tabs\",\"new-tab-button\",\"alltabs-button\"],\"PersonalToolbar\":[\"import-button\",\"personal-bookmarks\"]},\"seen\":[\"save-to-pocket-button\",\"developer-button\"],\"dirtyAreaCache\":[\"nav-bar\",\"PersonalToolbar\",\"toolbar-menubar\",\"TabsToolbar\"],\"currentVersion\":17,\"newElementCount\":2}");
+user_pref("browser.urlbar.placeholderName", "DuckDuckGo");
+user_pref("browser.urlbar.placeholderName.private", "DuckDuckGo");
+user_pref("browser.urlbar.suggest.calculator", true);
+user_pref("browser.urlbar.tipShownCount.searchTip_onboard", 4);
+user_pref("datareporting.policy.dataSubmissionPolicyAcceptedVersion", 2);
+user_pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "1625838590028");
+user_pref("devtools.everOpened", true);
+user_pref("devtools.toolbox.splitconsoleEnabled", true);
+user_pref("devtools.toolsidebar-height.inspector", 350);
+user_pref("devtools.toolsidebar-width.inspector", 700);
+user_pref("devtools.toolsidebar-width.inspector.splitsidebar", 350);
+user_pref("distribution.Manjaro.bookmarksProcessed", true);
+user_pref("distribution.iniFile.exists.appversion", "90.0");
+user_pref("distribution.iniFile.exists.value", true);
+user_pref("doh-rollout.balrog-migration-done", true);
+user_pref("doh-rollout.doneFirstRun", true);
+user_pref("doh-rollout.home-region", "FR");
+user_pref("dom.push.userAgentID", "61d9a104140946528e51aa88fd08b343");
+user_pref("extensions.activeThemeID", "{e0de5ee2-4619-413a-8300-a43a90196a6d}");
+user_pref("extensions.blocklist.pingCountVersion", -1);
+user_pref("extensions.databaseSchema", 33);
+user_pref("extensions.getAddons.cache.lastUpdate", 1626868179);
+user_pref("extensions.getAddons.databaseSchema", 6);
+user_pref("extensions.incognito.migrated", true);
+user_pref("extensions.lastAppBuildId", "20210714020445");
+user_pref("extensions.lastAppVersion", "90.0");
+user_pref("extensions.lastPlatformVersion", "90.0");
+user_pref("extensions.pendingOperations", false);
+user_pref("extensions.pictureinpicture.enable_picture_in_picture_overrides", true);
+user_pref("extensions.reset_default_search.runonce.3", true);
+user_pref("extensions.reset_default_search.runonce.reason", "previousRun");
+user_pref("extensions.systemAddonSet", "{\"schema\":1,\"directory\":\"{49d78b53-bd0f-419d-9312-6068b0f686a2}\",\"addons\":{\"reset-search-defaults@mozilla.com\":{\"version\":\"2.0.0\"}}}");
+user_pref("extensions.webcompat.enable_shims", true);
+user_pref("extensions.webcompat.perform_injections", true);
+user_pref("extensions.webcompat.perform_ua_overrides", true);
+user_pref("extensions.webextensions.ExtensionStorageIDB.migrated.screenshots@mozilla.org", true);
+user_pref("extensions.webextensions.uuids", "{\"doh-rollout@mozilla.org\":\"b9ecb8e7-691f-4134-8f98-105695f56aec\",\"formautofill@mozilla.org\":\"74a7def2-5ad4-4e34-a476-3b51818005f4\",\"pictureinpicture@mozilla.org\":\"bc991a02-d099-47c9-be28-2ecc10b7f7c2\",\"screenshots@mozilla.org\":\"7884d50d-6e0d-4f62-9bd8-91a964cf834a\",\"webcompat-reporter@mozilla.org\":\"210e0628-0f9e-42ae-a7cd-8b977803e6b8\",\"webcompat@mozilla.org\":\"eadb24f2-d0ab-4c7a-9026-c3ff60e721fb\",\"default-theme@mozilla.org\":\"b22d3a56-b8fe-452f-958f-d221d785395e\",\"google@search.mozilla.org\":\"66f66497-c162-4aa9-b096-938d49597de1\",\"wikipedia@search.mozilla.org\":\"080cb968-31e0-43ef-a84b-31f2c316a8df\",\"bing@search.mozilla.org\":\"29839e0b-ccb7-4ab6-849c-21e227b395bc\",\"ddg@search.mozilla.org\":\"e59e65ab-75df-46be-9a34-07a362d0e34a\",\"amazon@search.mozilla.org\":\"44a9435f-b258-4416-9824-ac2499e15fc4\",\"reset-search-defaults@mozilla.com\":\"d96dedb5-14b3-4175-b2a3-29a942302063\",\"{e0de5ee2-4619-413a-8300-a43a90196a6d}\":\"8b02f53b-0289-4b1d-895e-6c9f80541548\"}");
+user_pref("fission.experiment.max-origins.last-disqualified", 0);
+user_pref("fission.experiment.max-origins.last-qualified", 1625838590);
+user_pref("fission.experiment.max-origins.qualified", true);
+user_pref("gfx.webrender.all", true);
+user_pref("idle.lastDailyNotification", 1626871507);
+user_pref("layers.acceleration.force-enabled", true);
+user_pref("layout.css.backdrop-filter.enabled", true);
+user_pref("layout.css.color-mix.enabled", true);
+user_pref("materialFox.reduceTabOverflow", true);
+user_pref("media.gmp-gmpopenh264.abi", "x86_64-gcc3");
+user_pref("media.gmp-gmpopenh264.lastUpdate", 1625840207);
+user_pref("media.gmp-gmpopenh264.version", "1.8.1.1");
+user_pref("media.gmp-manager.buildID", "20210714020445");
+user_pref("media.gmp-manager.lastCheck", 1626867650);
+user_pref("media.gmp.storage.version.observed", 1);
+user_pref("network.trr.blocklist_cleanup_done", true);
+user_pref("nimbus.syncdefaultsstore.upgradeDialog", "{\"slug\":\"upgradeDialog-defaultEnabled\",\"enabled\":true,\"targeting\":\"true\",\"variables\":{},\"description\":\"Turn on upgradeDialog by default for all users\"}");
+user_pref("pdfjs.enabledCache.state", false);
+user_pref("pdfjs.migrationVersion", 2);
+user_pref("places.database.lastMaintenance", 1626626035);
+user_pref("privacy.purge_trackers.date_in_cookie_database", "0");
+user_pref("privacy.purge_trackers.last_purge", "1626871507933");
+user_pref("privacy.sanitize.pending", "[]");
+user_pref("security.insecure_connection_text.enabled", true);
+user_pref("security.remote_settings.crlite_filters.checked", 1626883472);
+user_pref("security.remote_settings.intermediates.checked", 1626882572);
+user_pref("security.sandbox.content.tempDirSuffix", "0e4d920d-ffba-4a07-a1ab-2aa6389b2943");
+user_pref("services.blocklist.addons-mlbf.checked", 1626957189);
+user_pref("services.blocklist.gfx.checked", 1626957189);
+user_pref("services.settings.clock_skew_seconds", -2);
+user_pref("services.settings.last_etag", "\"1626944256233\"");
+user_pref("services.settings.last_update_seconds", 1626957189);
+user_pref("services.settings.main.anti-tracking-url-decoration.last_check", 1626957189);
+user_pref("services.settings.main.cfr-fxa.last_check", 1626957189);
+user_pref("services.settings.main.cfr.last_check", 1626957189);
+user_pref("services.settings.main.doh-config.last_check", 1626957189);
+user_pref("services.settings.main.doh-providers.last_check", 1626957189);
+user_pref("services.settings.main.fxmonitor-breaches.last_check", 1626957189);
+user_pref("services.settings.main.hijack-blocklists.last_check", 1626957189);
+user_pref("services.settings.main.language-dictionaries.last_check", 1626957189);
+user_pref("services.settings.main.message-groups.last_check", 1626957189);
+user_pref("services.settings.main.nimbus-desktop-defaults.last_check", 1626957189);
+user_pref("services.settings.main.nimbus-desktop-experiments.last_check", 1626957189);
+user_pref("services.settings.main.normandy-recipes-capabilities.last_check", 1626957189);
+user_pref("services.settings.main.partitioning-exempt-urls.last_check", 1626957189);
+user_pref("services.settings.main.password-recipes.last_check", 1626957189);
+user_pref("services.settings.main.pioneer-study-addons-v1.last_check", 1626957189);
+user_pref("services.settings.main.public-suffix-list.last_check", 1626957189);
+user_pref("services.settings.main.search-config.last_check", 1626957189);
+user_pref("services.settings.main.search-default-override-allowlist.last_check", 1626957189);
+user_pref("services.settings.main.search-telemetry.last_check", 1626957189);
+user_pref("services.settings.main.sites-classification.last_check", 1626957189);
+user_pref("services.settings.main.tippytop.last_check", 1626957189);
+user_pref("services.settings.main.top-sites.last_check", 1626957189);
+user_pref("services.settings.main.url-classifier-skip-urls.last_check", 1626957189);
+user_pref("services.settings.main.websites-with-shared-credential-backends.last_check", 1626957189);
+user_pref("services.settings.main.whats-new-panel.last_check", 1626957189);
+user_pref("services.settings.security.onecrl.checked", 1626882572);
+user_pref("services.sync.clients.lastSync", "0");
+user_pref("services.sync.declinedEngines", "");
+user_pref("services.sync.globalScore", 0);
+user_pref("services.sync.nextSync", 0);
+user_pref("services.sync.tabs.lastSync", "0");
+user_pref("storage.vacuum.last.index", 1);
+user_pref("storage.vacuum.last.places.sqlite", 1626626035);
+user_pref("svg.context-properties.content.enabled", true);
+user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);
+user_pref("toolkit.startup.last_success", 1626957490);
+user_pref("toolkit.telemetry.cachedClientID", "701ca6a7-c501-47d6-aef9-2a1d57a8d1eb");
+user_pref("toolkit.telemetry.pioneer-new-studies-available", true);
+user_pref("toolkit.telemetry.previousBuildID", "20210714020445");
+user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);
+user_pref("trailhead.firstrun.didSeeAboutWelcome", true);
+	`)
+)
+
+func TestUserJSFileContent(t *testing.T) {
+	value, err := Theme{Config: map[string]interface{}{
+		"browser.tabs.tabClipWidth":              90,
+		"svg.context-properties.content.enabled": true,
+	}}.UserJSFileContent()
+
+	if err != nil {
+		panic(err)
+	}
+
+	assert.Equal(t,
+		`user_pref("browser.tabs.tabClipWidth", 90);
+user_pref("svg.context-properties.content.enabled", true);`,
+		sortLines(value),
+	)
+}
+
+func sortLines(s string) string {
+	lines := strings.Split(s, "\n")
+	sort.Strings(lines)
+	return strings.Join(lines, "\n")
+}
+
+func TestValueOfUserPrefCall(t *testing.T) {
+	result, err := ValueOfUserPrefCall(prefsContent, "storage.vacuum.last.index")
+	assert.NoError(t, err)
+	assert.Equal(t, "1", result)
+
+	result, err = ValueOfUserPrefCall(prefsContent, "app.normandy.first_run")
+	assert.NoError(t, err)
+	assert.Equal(t, "false", result)
+
+	result, err = ValueOfUserPrefCall(prefsContent, "browser.search.region")
+	assert.NoError(t, err)
+	assert.Equal(t, "FR", result)
+
+	result, err = ValueOfUserPrefCall(prefsContent, "lkghjoertkjhoietrjhoirtjhoirtjhor")
+	assert.Contains(t, err.Error(), `key "lkghjoertkjhoietrjhoirtjhoirtjhor" not found`)
+	assert.Equal(t, result, "")
+}
diff --git a/utils.go b/utils.go
new file mode 100644
index 0000000..1b87d85
--- /dev/null
+++ b/utils.go
@@ -0,0 +1,107 @@
+package ffcss
+
+import (
+	"fmt"
+	"net/url"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+// isValidURL tests a string to determine if it is a well-structured url or not.
+func isValidURL(toTest string) bool {
+	_, err := url.ParseRequestURI(toTest)
+	if err != nil {
+		return false
+	}
+
+	u, err := url.Parse(toTest)
+	if err != nil || u.Scheme == "" || u.Host == "" {
+		return false
+	}
+
+	return true
+}
+
+// getConfigDir returns the absolute path of ffcss's configuration directory
+func getConfigDir() string {
+	homedir, err := os.UserHomeDir()
+	if err != nil {
+		panic(fmt.Errorf("couldn't get your home directory: %w", err))
+	}
+	return filepath.Join(homedir, ".config", "ffcss")
+}
+
+// getCacheDir returns the temporary path for cloned repos and other stuff
+func getCacheDir() string {
+	homedir, err := os.UserHomeDir()
+	if err != nil {
+		panic(fmt.Errorf("couldn't get your home directory: %w", err))
+	}
+	return filepath.Join(homedir, ".cache", "ffcss")
+}
+
+// CacheDir joins the cache directory with the given path segments
+func CacheDir(pathSegments ...string) string {
+	return filepath.Join(getCacheDir(), filepath.Join(pathSegments...))
+}
+
+// ConfigDir joins the config directory with the given path segments
+func ConfigDir(pathSegments ...string) string {
+	return filepath.Join(getConfigDir(), filepath.Join(pathSegments...))
+}
+
+func cwd() string {
+	wd, err := os.Getwd()
+	if err != nil {
+		panic(fmt.Errorf("couldn't get the current working directory: %w", err))
+	}
+	return wd
+}
+
+// renameIfExists renames from to to if from exists. If it doesn't, don't attempt renaming.
+func renameIfExists(from string, to string) error {
+	if _, err := os.Stat(from); os.IsNotExist(err) {
+		return nil
+	}
+	if _, err := os.Stat(to); os.IsNotExist(err) {
+		return os.Rename(from, to)
+	}
+	err := os.RemoveAll(to)
+	if err != nil {
+		return err
+	}
+	return os.Rename(from, to)
+}
+
+// vimModeEnabled returns true if the user has explicitly set vim mode, or if the $EDITOR is vim/neovim
+func vimModeEnabled() bool {
+	if os.Getenv("VIM_MODE") == "1" || os.Getenv("VIM_STYLE") == "1" {
+		return true
+	}
+	progname := filepath.Base(os.Getenv("EDITOR"))
+	return progname == "vim" || progname == "nvim"
+}
+
+// prefixEachLine prepends each line of s with the provided prefix (with).
+// Only supports UNIX-Style line endings (\n)
+func prefixEachLine(s string, with string) string {
+	var prefixedLines []string
+	for _, line := range strings.Split(s, "\n") {
+		prefixedLines = append(prefixedLines, with+line)
+	}
+	return strings.Join(prefixedLines, "\n")
+}
+
+// GOOStoOS returns user-friendly OS names from a given GOOS.
+// darwin becomes macos and plan9 becomes linux.
+func GOOStoOS(GOOS string) string {
+	switch GOOS {
+	case "darwin":
+		return "macos"
+	case "plan9":
+		return "linux"
+	default:
+		return GOOS
+	}
+}
diff --git a/utils_test.go b/utils_test.go
new file mode 100644
index 0000000..cb9d0da
--- /dev/null
+++ b/utils_test.go
@@ -0,0 +1,48 @@
+package ffcss
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestProfilePaths(t *testing.T) {
+	cwd, _ := os.Getwd()
+	mockedHomedir := filepath.Join(cwd, "testarea", "home")
+
+	paths, err := ProfilePaths("linux", filepath.Join(mockedHomedir, ".mozilla", "firefox"))
+	assert.NoError(t, err)
+	assert.Equal(t, []string{filepath.Join(mockedHomedir, ".mozilla", "firefox", "667ekipp.default-release")}, paths)
+
+	paths, err = ProfilePaths("linux")
+	assert.NoError(t, err)
+	assert.Equal(t, []string{"testarea/home/.mozilla/firefox/667ekipp.default-release"}, paths)
+}
+
+func TestIsURLClonable(t *testing.T) {
+	var actual bool
+
+	actual = isURLClonable("https://github.com/ewen-lbh/ffcss/")
+	assert.Equal(t, true, actual)
+
+	actual = isURLClonable("https://github.com/users/schoolsyst")
+	assert.Equal(t, false, actual)
+
+	actual = isURLClonable("https://ewen.works/")
+	assert.Equal(t, false, actual)
+}
+
+func TestDefaultProfilesDir(t *testing.T) {
+	actual, err := DefaultProfilesDir("TempleOS")
+	assert.EqualError(t, err, "unknown operating system TempleOS")
+	assert.Equal(t, "", actual)
+}
+
+func TestPlural(t *testing.T) {
+	assert.Equal(t, "addons", plural("addon", 2))
+	assert.Equal(t, "addon", plural("addon", 1))
+	assert.Equal(t, "addons", plural("addon", 0))
+	assert.Equal(t, "children", plural("child", 10, "children"))
+}
diff --git a/utils_unix_test.go b/utils_unix_test.go
new file mode 100644
index 0000000..af7414b
--- /dev/null
+++ b/utils_unix_test.go
@@ -0,0 +1,19 @@
+//+build !windows
+
+package ffcss
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDefaultProfilesDirUNIX(t *testing.T) {
+	actual, err := DefaultProfilesDir("linux")
+	assert.NoError(t, err)
+	assert.Equal(t, "testarea/home/.mozilla/firefox", actual)
+
+	actual, err = DefaultProfilesDir("macos")
+	assert.NoError(t, err)
+	assert.Equal(t, withuser("/Users/%s/Library/Application Support/Firefox/Profiles"), actual)
+}
diff --git a/utils_windows_test.go b/utils_windows_test.go
new file mode 100644
index 0000000..7961a02
--- /dev/null
+++ b/utils_windows_test.go
@@ -0,0 +1,16 @@
+package ffcss
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDefaultProfilesDirWindows(t *testing.T) {
+	homedir, _ := os.UserHomeDir()
+	actual, err := DefaultProfilesDir("windows")
+	assert.NoError(t, err)
+	assert.Equal(t, filepath.Join(homedir, `AppData\Roaming\Mozilla\Firefox\Profiles`), actual)
+}

Fixed code: language: go
commit message: 🔖 Release {new}
root = true

[*]
indent_style = tab
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.{yml,yaml}]
indent_style = space
indent_size = 2

[*.go]
indent_style = tab

[*.md]
indent_style = space
indent_size = 4
trim_trailing_whitespace = false

[*.rb]
indent_style = space
indent_size = 2
---
name: Adding a theme to the registry
about: To add a theme's manifest to the built-in ones, so that users can do `ffcss use YOUR-THEME`. 
title: Add <your theme’s name> to the registry
labels: registry
assignees: ''
---

<!-- If you are familiar with git, please open a Pull Request, it'll be quicker. -->

<!-- Choose one of the two following sections, and delete the other: -->

## Manifest file (if you have one)

```yaml
# copy-paste the contents of your manifest here
```

## URL to the theme (if you don't have a manifest and want me to write one)



## Author name

<!-- If you don't have a github repository in the “download” field, tell me your author name & URL (optional) -->


---
name: Adding a theme to the registry
about: To add a theme's manifest to the built-in ones, so that users can do `ffcss
  use YOUR-THEME`
title: Add <your theme's name> to the registry
labels: registry
assignees: ''

---

Steps:

1. Upload your manifest as `themes/<your theme's name>.yaml`
1. If you want your username to appear in the "Built-in themes" section of the README, make sure that you either
  - ~~Included a `by` field in the manifest~~ _Will be available once #33 is closed_
  - Used a github repository in your `download` field
  - Told me your author name/URL in this PR, so that I can add it manually when I release the next version of `ffcss`
1. Ran `chachacha added new theme <your theme's name> in the registry` (if you don't have `chachacha`, run `pip install chachacha` (you need Python for this) _Note: if you have problems installing chachacha, don't worry, I'll modify the changelog myself_
on: [push, pull_request]
name: test

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: [1.16.x]
        os:
          - ubuntu-latest
          # - macos-latest
          # - windows-latest
    steps:
      - name: Skip if commit type is skippable
        uses: mstachniuk/ci-skip@v1
        with:
          commit-filter: >
            📝 :memo:
            ✏️ :pencil2:
            📄 :page_facing_up:
            💡 :bulb:
            💬 :speech_baloon:
            🔊 :loud_sound:
            🔇 :mute:
            👥 :busts_in_silhouette:
            🙈 :see_no_evil:
            🧪 :test_tube:
          commit-filter-separator: ' '
      - name: Install Go
        uses: actions/setup-go@v2
        with:
          go-version: ${{ matrix.go }}
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Test
        run: make tests
      - name: Upload coverage
        uses: codecov/codecov-action@v2
        with:
          verbose: yes

# Created by https://www.toptal.com/developers/gitignore/api/go
# Edit at https://www.toptal.com/developers/gitignore?templates=go

### Go ###
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

### Go Patch ###
/vendor/
/Godeps/

# End of https://www.toptal.com/developers/gitignore/api/go

# Universal Package Manager
.upm/

# dotENV file
.env

# vscode
.vscode/

commit-types-distribution.png

# coverage
profile.out
coverage.txt
coverage/lcov.info

# compiled binary
/ffcss

# Release artifacts & token files
dist/
release_notes.md
.github_token

# Testing artifacts
testarea/

cmd/ffcss/ffcss
cmd/ffcss/ffcss
# This is an example .goreleaser.yml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com

project_name: ffcss

before:
  hooks:
    # You may remove this if you don't use go modules.
    - go mod tidy
    # you may remove this if you don't need go generate
    - go generate ./...
    # so that github does not create a tag in another place
    - git push
    # push the tag
    - git push origin {{ .Tag }}

builds:
  -
    goos: [windows, linux, darwin]
    goarch: [amd64, 386]
    binary: ffcss

archives:
  -
    format: tar.gz
    format_overrides:
      - goos: windows
        format: zip

    name_template: '{{.Os}}-{{.Arch}}'

    replacements:
      darwin: macos
      386: 32bit
      amd64: 64bit

    files:
      - LICENSE
      - README.md
      - CHANGELOG.md
      - themes/**
checksum:
  name_template: 'checksums.txt'

# TODO: Homebrew, NFPM, Scoop, Snapcraft (eww), Signing

release:
  prerelease: auto
  name_template: "{{ .Version }}"

changelog:
  skip: true

milestones:
  -
    name_template: "{{ .Version }}"
    close: true
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2021-07-25

### Added

- new `firefox` manifest entry: can be used to declare which versions of firefox are compatible with your theme: `version+` means "`version` and up", `up to version` means, well, you guessed it, `version1-version2` is an (inclusive) range and finally just `version` means exactly _that_ version. Use `major.minor` to specify the minor version, or omit it (or use ".x") to ignore it.
- prompt to show the manifest source of the theme you're installing. The prompt can be turned off with a new flag `--skip-manifest-source`
- command _reapply_ to reapply themes after firefox updates. The current themes for each profile are stored in a YAML file at `~/.config/ffcss/currently.yaml`. If ffcss tells you that a profile has no ffcss theme applied, try re-applying it manually with `ffcss use` so that this file gets created & filled.
- hooks to run custom shell commands before and after theme installations, via manifest entries `run.before` and `run.after`. Variants can override them.
- "did you mean ...?" message when a theme with a similar name is found
- command _get_ to download a theme without applying it
- manifest entry `commit` to specify a commit SHA to use for the theme. Can be overriden by variants.
- manifest entry `tag` to specify a git tag to use for the theme. Can be overriden by variants.
- manifest entry `description` that will be shown at the start of the installation
- manifest entry `by` to set an author name to be shown at the start of the installation (for github repos, defaults to the owner in the URL)
- shorthand flag `-a` for `--all-profiles`
- flag `-p`/`--profiles` to specify profile paths to use (and therefore not prompt during installation)

### Changed

- `ffcss use <theme name>` is now case-insensitive, punctuation-insensitive (`-`, `_` and `.`), whitespace-insensitive and unicode-insensitive (NFC normalization is applied before searching)
- UI improvements: log what is happening with bullet points, don't show the full profile path when selecting

### Fixed

- inconsistent indentation in the `--help` screen

## [0.1.2] - 2021-07-10

### Added

- new theme in registry: [alpenblue](https://github.com/Godiesc/AlpenBlue) by [Godiesc](https://github.com/Godiesc)
- new theme in registry: [australis-tabs](https://github.com/sagars007/Australis-like-tabs-FF-ProtonUI-changes) by [sagars007](https://github.com/sagars007)
- new theme in registry: [blurredfox](https://github.com/manilarome/blurredfox) by [manilarome](https://github.com/manilarome)
- new theme in registry: [compactmode](https://github.com/Godiesc/compactmodefirefoxcss) by [Godiesc](https://github.com/Godiesc)
- new theme in registry: [diamondfucsia](https://github.com/Godiesc/DiamondFucsia) by [Godiesc](https://github.com/Godiesc)
- new theme in registry: [frozenfox](https://github.com/tortious/FrozenFox) by [tortious](https://github.com/tortious)
- new theme in registry: [halo](https://github.com/seirin-blu/Firefox-Halo) by [seirin-blu](https://github.com/seirin-blu)
- new theme in registry: [martinfox](https://github.com/arp242/MartinFox) by [arp242](https://github.com/arp242)
- new theme in registry: [montereyfox](https://github.com/FirefoxCSSThemers/Monterey-Fox) by [FirefoxCSSThemers](https://github.com/FirefoxCSSThemers)
- new theme in registry: [pro-fox](https://github.com/xmansyx/Pro-Fox) by [xmansyx](https://github.com/xmansyx)
- new theme in registry: [proton-connected-rounded-tabs](https://github.com/sagars007/Proton-UI-connected-rounded-tabs) by [sagars007](https://github.com/sagars007)
- new theme in registry: [technetium](https://github.com/edo0/Technetium) by [edo0](https://github.com/edo0)
- new theme in registry: [wavefox](https://github.com/QNetITQ/WaveFox) by [QNetITQ](https://github.com/QNetITQ)

### Fixed

- `ffcss init` was generating a file with a repository setting instead of download [[#36](https://github.com/ewen-lbh/ffcss/issues/36)]
- `ffcss init` added a .git suffix in the pre-filled value for `name`

## [0.1.1] - 2021-07-10

### Fixed

- crashes related to path handling on Windows

## [0.1.0] - 2021-07-09

### Added

#### For users
- Works on MacOS, GNU/Linux and Windows, tested on:
- Manjaro Linux Omara 21.0.7 (with kernel 5.12.9-1-MANJARO)
- MacOS Catalina 10.15.7
- Windows 10 20H2 (Build 19042.1083) (Please use the new Windows Terminal or something else that support ANSI escape sequences)
- a `use` commands to download & install themes
- works with any remote git repository
- works with any URL poiting to a .zip file
- shorthand for github repositories: OWNER/REPO
- a `init` command to add a `ffcss.yaml` manifest in your current repository
- basic for now, [a smarter version is planned](https://github.com/ewen-lbh/ffcss/issues/20)
- a `cache clear` command to clear the cache of downloaded repositories
- 8 themes available out-of-the-box (use them by typing their name only, it works)
- [chameleons-beauty](https://github.com/Godiesc/Chameleons-Beauty) by [Godiesc](https://github.com/Godiesc)
- [fxcompact](https://github.com/dannycolin/fx-compact-mode) by [dannycolin](https://github.com/dannycolin)
- [lepton](https://github.com/black7375/Firefox-UI-Fix) by [black7375](https://github.com/black7375)
- [materialfox](https://github.com/muckSponge/MaterialFox) by [muckSponge](https://github.com/muckSponge)
- [modoki](https://github.com/soup-bowl/Modoki-FirefoxCSS) by [soup-bowl](https://github.com/soup-bowl)
- [simplerentfox](https://github.com/MiguelRAvila/SimplerentFox) by [MiguelRAvila](https://github.com/MiguelRAvila)
- [verticaltabs](https://github.com/ranmaru22/firefox-vertical-tabs) by [ranmaru22](https://github.com/ranmaru22)
#### For theme makers
- a mechanism to handle theme variants:
- Variants can be declared in the same manifest file under the `variants` entry to override other entries
- per-OS paths: the {{ variant }} and {{ os }} placeholders get replaced with their values
- the value {{ os }} gets replaced with can be customized in the manifest file under the `os` entry, use `null` to mark an OS as incompatible
- Support for helper addons:
- Declare URLs to open after installation under the `addons` manifest entry (I plan to auto-install them in the future)
- Easy way to write about:config changes without writing a `user.js` file:
- Use the `config` manifest entry
- If you also use a `user.js`, you can combine both, they'll be written as a single `user.js` file
- Support for custom assets:
- Use the `assets` manifest entry to list out your assets
- Supports glob patterns
- If you store them under a `chrome` directory, you can use `copy from: chrome/` so that they don't get copied to `<profile directory>/chrome/chrome`

[Unreleased]: https://github.com/ewen-lbh/ffcss/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/ewen-lbh/ffcss/compare/v0.1.2...v0.2.0
[0.1.2]: https://github.com/ewen-lbh/ffcss/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/ewen-lbh/ffcss/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/ewen-lbh/ffcss/releases/tag/v0.1.0

[//]: # (C3-2-DKAC:GGH:Rewen-lbh/ffcss:Tv{t})
GNU GENERAL PUBLIC LICENSE

Version 3, 29 June 2007

Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>

Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.

Preamble

The GNU General Public License is a free, copyleft license for software and
other kinds of works.

The licenses for most software and other practical works are designed to take
away your freedom to share and change the works. By contrast, the GNU General
Public License is intended to guarantee your freedom to share and change all
versions of a program--to make sure it remains free software for all its users.
We, the Free Software Foundation, use the GNU General Public License for most
of our software; it applies also to any other work released this way by its
authors. You can apply it to your programs, too.

When we speak of free software, we are referring to freedom, not price. Our
General Public Licenses are designed to make sure that you have the freedom
to distribute copies of free software (and charge for them if you wish), that
you receive source code or can get it if you want it, that you can change
the software or use pieces of it in new free programs, and that you know you
can do these things.

To protect your rights, we need to prevent others from denying you these rights
or asking you to surrender the rights. Therefore, you have certain responsibilities
if you distribute copies of the software, or if you modify it: responsibilities
to respect the freedom of others.

For example, if you distribute copies of such a program, whether gratis or
for a fee, you must pass on to the recipients the same freedoms that you received.
You must make sure that they, too, receive or can get the source code. And
you must show them these terms so they know their rights.

Developers that use the GNU GPL protect your rights with two steps: (1) assert
copyright on the software, and (2) offer you this License giving you legal
permission to copy, distribute and/or modify it.

For the developers' and authors' protection, the GPL clearly explains that
there is no warranty for this free software. For both users' and authors'
sake, the GPL requires that modified versions be marked as changed, so that
their problems will not be attributed erroneously to authors of previous versions.

Some devices are designed to deny users access to install or run modified
versions of the software inside them, although the manufacturer can do so.
This is fundamentally incompatible with the aim of protecting users' freedom
to change the software. The systematic pattern of such abuse occurs in the
area of products for individuals to use, which is precisely where it is most
unacceptable. Therefore, we have designed this version of the GPL to prohibit
the practice for those products. If such problems arise substantially in other
domains, we stand ready to extend this provision to those domains in future
versions of the GPL, as needed to protect the freedom of users.

Finally, every program is threatened constantly by software patents. States
should not allow patents to restrict development and use of software on general-purpose
computers, but in those that do, we wish to avoid the special danger that
patents applied to a free program could make it effectively proprietary. To
prevent this, the GPL assures that patents cannot be used to render the program
non-free.

The precise terms and conditions for copying, distribution and modification
follow.

TERMS AND CONDITIONS

   0. Definitions.

   "This License" refers to version 3 of the GNU General Public License.

"Copyright" also means copyright-like laws that apply to other kinds of works,
such as semiconductor masks.

"The Program" refers to any copyrightable work licensed under this License.
Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals
or organizations.

To "modify" a work means to copy from or adapt all or part of the work in
a fashion requiring copyright permission, other than the making of an exact
copy. The resulting work is called a "modified version" of the earlier work
or a work "based on" the earlier work.

A "covered work" means either the unmodified Program or a work based on the
Program.

To "propagate" a work means to do anything with it that, without permission,
would make you directly or secondarily liable for infringement under applicable
copyright law, except executing it on a computer or modifying a private copy.
Propagation includes copying, distribution (with or without modification),
making available to the public, and in some countries other activities as
well.

To "convey" a work means any kind of propagation that enables other parties
to make or receive copies. Mere interaction with a user through a computer
network, with no transfer of a copy, is not conveying.

An interactive user interface displays "Appropriate Legal Notices" to the
extent that it includes a convenient and prominently visible feature that
(1) displays an appropriate copyright notice, and (2) tells the user that
there is no warranty for the work (except to the extent that warranties are
provided), that licensees may convey the work under this License, and how
to view a copy of this License. If the interface presents a list of user commands
or options, such as a menu, a prominent item in the list meets this criterion.

   1. Source Code.

The "source code" for a work means the preferred form of the work for making
modifications to it. "Object code" means any non-source form of a work.

A "Standard Interface" means an interface that either is an official standard
defined by a recognized standards body, or, in the case of interfaces specified
for a particular programming language, one that is widely used among developers
working in that language.

The "System Libraries" of an executable work include anything, other than
the work as a whole, that (a) is included in the normal form of packaging
a Major Component, but which is not part of that Major Component, and (b)
serves only to enable use of the work with that Major Component, or to implement
a Standard Interface for which an implementation is available to the public
in source code form. A "Major Component", in this context, means a major essential
component (kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to produce
the work, or an object code interpreter used to run it.

The "Corresponding Source" for a work in object code form means all the source
code needed to generate, install, and (for an executable work) run the object
code and to modify the work, including scripts to control those activities.
However, it does not include the work's System Libraries, or general-purpose
tools or generally available free programs which are used unmodified in performing
those activities but which are not part of the work. For example, Corresponding
Source includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically linked
subprograms that the work is specifically designed to require, such as by
intimate data communication or control flow between those subprograms and
other parts of the work.

The Corresponding Source need not include anything that users can regenerate
automatically from other parts of the Corresponding Source.

   The Corresponding Source for a work in source code form is that same work.

   2. Basic Permissions.

All rights granted under this License are granted for the term of copyright
on the Program, and are irrevocable provided the stated conditions are met.
This License explicitly affirms your unlimited permission to run the unmodified
Program. The output from running a covered work is covered by this License
only if the output, given its content, constitutes a covered work. This License
acknowledges your rights of fair use or other equivalent, as provided by copyright
law.

You may make, run and propagate covered works that you do not convey, without
conditions so long as your license otherwise remains in force. You may convey
covered works to others for the sole purpose of having them make modifications
exclusively for you, or provide you with facilities for running those works,
provided that you comply with the terms of this License in conveying all material
for which you do not control copyright. Those thus making or running the covered
works for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of your copyrighted
material outside their relationship with you.

Conveying under any other circumstances is permitted solely under the conditions
stated below. Sublicensing is not allowed; section 10 makes it unnecessary.

   3. Protecting Users' Legal Rights From Anti-Circumvention Law.

No covered work shall be deemed part of an effective technological measure
under any applicable law fulfilling obligations under article 11 of the WIPO
copyright treaty adopted on 20 December 1996, or similar laws prohibiting
or restricting circumvention of such measures.

When you convey a covered work, you waive any legal power to forbid circumvention
of technological measures to the extent such circumvention is effected by
exercising rights under this License with respect to the covered work, and
you disclaim any intention to limit operation or modification of the work
as a means of enforcing, against the work's users, your or third parties'
legal rights to forbid circumvention of technological measures.

   4. Conveying Verbatim Copies.

You may convey verbatim copies of the Program's source code as you receive
it, in any medium, provided that you conspicuously and appropriately publish
on each copy an appropriate copyright notice; keep intact all notices stating
that this License and any non-permissive terms added in accord with section
7 apply to the code; keep intact all notices of the absence of any warranty;
and give all recipients a copy of this License along with the Program.

You may charge any price or no price for each copy that you convey, and you
may offer support or warranty protection for a fee.

   5. Conveying Modified Source Versions.

You may convey a work based on the Program, or the modifications to produce
it from the Program, in the form of source code under the terms of section
4, provided that you also meet all of these conditions:

a) The work must carry prominent notices stating that you modified it, and
giving a relevant date.

b) The work must carry prominent notices stating that it is released under
this License and any conditions added under section 7. This requirement modifies
the requirement in section 4 to "keep intact all notices".

c) You must license the entire work, as a whole, under this License to anyone
who comes into possession of a copy. This License will therefore apply, along
with any applicable section 7 additional terms, to the whole of the work,
and all its parts, regardless of how they are packaged. This License gives
no permission to license the work in any other way, but it does not invalidate
such permission if you have separately received it.

d) If the work has interactive user interfaces, each must display Appropriate
Legal Notices; however, if the Program has interactive interfaces that do
not display Appropriate Legal Notices, your work need not make them do so.

A compilation of a covered work with other separate and independent works,
which are not by their nature extensions of the covered work, and which are
not combined with it such as to form a larger program, in or on a volume of
a storage or distribution medium, is called an "aggregate" if the compilation
and its resulting copyright are not used to limit the access or legal rights
of the compilation's users beyond what the individual works permit. Inclusion
of a covered work in an aggregate does not cause this License to apply to
the other parts of the aggregate.

   6. Conveying Non-Source Forms.

You may convey a covered work in object code form under the terms of sections
4 and 5, provided that you also convey the machine-readable Corresponding
Source under the terms of this License, in one of these ways:

a) Convey the object code in, or embodied in, a physical product (including
a physical distribution medium), accompanied by the Corresponding Source fixed
on a durable physical medium customarily used for software interchange.

b) Convey the object code in, or embodied in, a physical product (including
a physical distribution medium), accompanied by a written offer, valid for
at least three years and valid for as long as you offer spare parts or customer
support for that product model, to give anyone who possesses the object code
either (1) a copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical medium customarily
used for software interchange, for a price no more than your reasonable cost
of physically performing this conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.

c) Convey individual copies of the object code with a copy of the written
offer to provide the Corresponding Source. This alternative is allowed only
occasionally and noncommercially, and only if you received the object code
with such an offer, in accord with subsection 6b.

d) Convey the object code by offering access from a designated place (gratis
or for a charge), and offer equivalent access to the Corresponding Source
in the same way through the same place at no further charge. You need not
require recipients to copy the Corresponding Source along with the object
code. If the place to copy the object code is a network server, the Corresponding
Source may be on a different server (operated by you or a third party) that
supports equivalent copying facilities, provided you maintain clear directions
next to the object code saying where to find the Corresponding Source. Regardless
of what server hosts the Corresponding Source, you remain obligated to ensure
that it is available for as long as needed to satisfy these requirements.

e) Convey the object code using peer-to-peer transmission, provided you inform
other peers where the object code and Corresponding Source of the work are
being offered to the general public at no charge under subsection 6d.

A separable portion of the object code, whose source code is excluded from
the Corresponding Source as a System Library, need not be included in conveying
the object code work.

A "User Product" is either (1) a "consumer product", which means any tangible
personal property which is normally used for personal, family, or household
purposes, or (2) anything designed or sold for incorporation into a dwelling.
In determining whether a product is a consumer product, doubtful cases shall
be resolved in favor of coverage. For a particular product received by a particular
user, "normally used" refers to a typical or common use of that class of product,
regardless of the status of the particular user or of the way in which the
particular user actually uses, or expects or is expected to use, the product.
A product is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent the
only significant mode of use of the product.

"Installation Information" for a User Product means any methods, procedures,
authorization keys, or other information required to install and execute modified
versions of a covered work in that User Product from a modified version of
its Corresponding Source. The information must suffice to ensure that the
continued functioning of the modified object code is in no case prevented
or interfered with solely because modification has been made.

If you convey an object code work under this section in, or with, or specifically
for use in, a User Product, and the conveying occurs as part of a transaction
in which the right of possession and use of the User Product is transferred
to the recipient in perpetuity or for a fixed term (regardless of how the
transaction is characterized), the Corresponding Source conveyed under this
section must be accompanied by the Installation Information. But this requirement
does not apply if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has been installed
in ROM).

The requirement to provide Installation Information does not include a requirement
to continue to provide support service, warranty, or updates for a work that
has been modified or installed by the recipient, or for the User Product in
which it has been modified or installed. Access to a network may be denied
when the modification itself materially and adversely affects the operation
of the network or violates the rules and protocols for communication across
the network.

Corresponding Source conveyed, and Installation Information provided, in accord
with this section must be in a format that is publicly documented (and with
an implementation available to the public in source code form), and must require
no special password or key for unpacking, reading or copying.

   7. Additional Terms.

"Additional permissions" are terms that supplement the terms of this License
by making exceptions from one or more of its conditions. Additional permissions
that are applicable to the entire Program shall be treated as though they
were included in this License, to the extent that they are valid under applicable
law. If additional permissions apply only to part of the Program, that part
may be used separately under those permissions, but the entire Program remains
governed by this License without regard to the additional permissions.

When you convey a copy of a covered work, you may at your option remove any
additional permissions from that copy, or from any part of it. (Additional
permissions may be written to require their own removal in certain cases when
you modify the work.) You may place additional permissions on material, added
by you to a covered work, for which you have or can give appropriate copyright
permission.

Notwithstanding any other provision of this License, for material you add
to a covered work, you may (if authorized by the copyright holders of that
material) supplement the terms of this License with terms:

a) Disclaiming warranty or limiting liability differently from the terms of
sections 15 and 16 of this License; or

b) Requiring preservation of specified reasonable legal notices or author
attributions in that material or in the Appropriate Legal Notices displayed
by works containing it; or

c) Prohibiting misrepresentation of the origin of that material, or requiring
that modified versions of such material be marked in reasonable ways as different
from the original version; or

d) Limiting the use for publicity purposes of names of licensors or authors
of the material; or

e) Declining to grant rights under trademark law for use of some trade names,
trademarks, or service marks; or

f) Requiring indemnification of licensors and authors of that material by
anyone who conveys the material (or modified versions of it) with contractual
assumptions of liability to the recipient, for any liability that these contractual
assumptions directly impose on those licensors and authors.

All other non-permissive additional terms are considered "further restrictions"
within the meaning of section 10. If the Program as you received it, or any
part of it, contains a notice stating that it is governed by this License
along with a term that is a further restriction, you may remove that term.
If a license document contains a further restriction but permits relicensing
or conveying under this License, you may add to a covered work material governed
by the terms of that license document, provided that the further restriction
does not survive such relicensing or conveying.

If you add terms to a covered work in accord with this section, you must place,
in the relevant source files, a statement of the additional terms that apply
to those files, or a notice indicating where to find the applicable terms.

Additional terms, permissive or non-permissive, may be stated in the form
of a separately written license, or stated as exceptions; the above requirements
apply either way.

   8. Termination.

You may not propagate or modify a covered work except as expressly provided
under this License. Any attempt otherwise to propagate or modify it is void,
and will automatically terminate your rights under this License (including
any patent licenses granted under the third paragraph of section 11).

However, if you cease all violation of this License, then your license from
a particular copyright holder is reinstated (a) provisionally, unless and
until the copyright holder explicitly and finally terminates your license,
and (b) permanently, if the copyright holder fails to notify you of the violation
by some reasonable means prior to 60 days after the cessation.

Moreover, your license from a particular copyright holder is reinstated permanently
if the copyright holder notifies you of the violation by some reasonable means,
this is the first time you have received notice of violation of this License
(for any work) from that copyright holder, and you cure the violation prior
to 30 days after your receipt of the notice.

Termination of your rights under this section does not terminate the licenses
of parties who have received copies or rights from you under this License.
If your rights have been terminated and not permanently reinstated, you do
not qualify to receive new licenses for the same material under section 10.

   9. Acceptance Not Required for Having Copies.

You are not required to accept this License in order to receive or run a copy
of the Program. Ancillary propagation of a covered work occurring solely as
a consequence of using peer-to-peer transmission to receive a copy likewise
does not require acceptance. However, nothing other than this License grants
you permission to propagate or modify any covered work. These actions infringe
copyright if you do not accept this License. Therefore, by modifying or propagating
a covered work, you indicate your acceptance of this License to do so.

   10. Automatic Licensing of Downstream Recipients.

Each time you convey a covered work, the recipient automatically receives
a license from the original licensors, to run, modify and propagate that work,
subject to this License. You are not responsible for enforcing compliance
by third parties with this License.

An "entity transaction" is a transaction transferring control of an organization,
or substantially all assets of one, or subdividing an organization, or merging
organizations. If propagation of a covered work results from an entity transaction,
each party to that transaction who receives a copy of the work also receives
whatever licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the Corresponding
Source of the work from the predecessor in interest, if the predecessor has
it or can get it with reasonable efforts.

You may not impose any further restrictions on the exercise of the rights
granted or affirmed under this License. For example, you may not impose a
license fee, royalty, or other charge for exercise of rights granted under
this License, and you may not initiate litigation (including a cross-claim
or counterclaim in a lawsuit) alleging that any patent claim is infringed
by making, using, selling, offering for sale, or importing the Program or
any portion of it.

   11. Patents.

A "contributor" is a copyright holder who authorizes use under this License
of the Program or a work on which the Program is based. The work thus licensed
is called the contributor's "contributor version".

A contributor's "essential patent claims" are all patent claims owned or controlled
by the contributor, whether already acquired or hereafter acquired, that would
be infringed by some manner, permitted by this License, of making, using,
or selling its contributor version, but do not include claims that would be
infringed only as a consequence of further modification of the contributor
version. For purposes of this definition, "control" includes the right to
grant patent sublicenses in a manner consistent with the requirements of this
License.

Each contributor grants you a non-exclusive, worldwide, royalty-free patent
license under the contributor's essential patent claims, to make, use, sell,
offer for sale, import and otherwise run, modify and propagate the contents
of its contributor version.

In the following three paragraphs, a "patent license" is any express agreement
or commitment, however denominated, not to enforce a patent (such as an express
permission to practice a patent or covenant not to sue for patent infringement).
To "grant" such a patent license to a party means to make such an agreement
or commitment not to enforce a patent against the party.

If you convey a covered work, knowingly relying on a patent license, and the
Corresponding Source of the work is not available for anyone to copy, free
of charge and under the terms of this License, through a publicly available
network server or other readily accessible means, then you must either (1)
cause the Corresponding Source to be so available, or (2) arrange to deprive
yourself of the benefit of the patent license for this particular work, or
(3) arrange, in a manner consistent with the requirements of this License,
to extend the patent license to downstream recipients. "Knowingly relying"
means you have actual knowledge that, but for the patent license, your conveying
the covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that country
that you have reason to believe are valid.

If, pursuant to or in connection with a single transaction or arrangement,
you convey, or propagate by procuring conveyance of, a covered work, and grant
a patent license to some of the parties receiving the covered work authorizing
them to use, propagate, modify or convey a specific copy of the covered work,
then the patent license you grant is automatically extended to all recipients
of the covered work and works based on it.

A patent license is "discriminatory" if it does not include within the scope
of its coverage, prohibits the exercise of, or is conditioned on the non-exercise
of one or more of the rights that are specifically granted under this License.
You may not convey a covered work if you are a party to an arrangement with
a third party that is in the business of distributing software, under which
you make payment to the third party based on the extent of your activity of
conveying the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory patent
license (a) in connection with copies of the covered work conveyed by you
(or copies made from those copies), or (b) primarily for and in connection
with specific products or compilations that contain the covered work, unless
you entered into that arrangement, or that patent license was granted, prior
to 28 March 2007.

Nothing in this License shall be construed as excluding or limiting any implied
license or other defenses to infringement that may otherwise be available
to you under applicable patent law.

   12. No Surrender of Others' Freedom.

If conditions are imposed on you (whether by court order, agreement or otherwise)
that contradict the conditions of this License, they do not excuse you from
the conditions of this License. If you cannot convey a covered work so as
to satisfy simultaneously your obligations under this License and any other
pertinent obligations, then as a consequence you may not convey it at all.
For example, if you agree to terms that obligate you to collect a royalty
for further conveying from those to whom you convey the Program, the only
way you could satisfy both those terms and this License would be to refrain
entirely from conveying the Program.

   13. Use with the GNU Affero General Public License.

Notwithstanding any other provision of this License, you have permission to
link or combine any covered work with a work licensed under version 3 of the
GNU Affero General Public License into a single combined work, and to convey
the resulting work. The terms of this License will continue to apply to the
part which is the covered work, but the special requirements of the GNU Affero
General Public License, section 13, concerning interaction through a network
will apply to the combination as such.

   14. Revised Versions of this License.

The Free Software Foundation may publish revised and/or new versions of the
GNU General Public License from time to time. Such new versions will be similar
in spirit to the present version, but may differ in detail to address new
problems or concerns.

Each version is given a distinguishing version number. If the Program specifies
that a certain numbered version of the GNU General Public License "or any
later version" applies to it, you have the option of following the terms and
conditions either of that numbered version or of any later version published
by the Free Software Foundation. If the Program does not specify a version
number of the GNU General Public License, you may choose any version ever
published by the Free Software Foundation.

If the Program specifies that a proxy can decide which future versions of
the GNU General Public License can be used, that proxy's public statement
of acceptance of a version permanently authorizes you to choose that version
for the Program.

Later license versions may give you additional or different permissions. However,
no additional obligations are imposed on any author or copyright holder as
a result of your choosing to follow a later version.

   15. Disclaimer of Warranty.

THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE
LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM
PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.

   16. Limitation of Liability.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL
ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM
AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO
USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE
PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

   17. Interpretation of Sections 15 and 16.

If the disclaimer of warranty and limitation of liability provided above cannot
be given local legal effect according to their terms, reviewing courts shall
apply local law that most closely approximates an absolute waiver of all civil
liability in connection with the Program, unless a warranty or assumption
of liability accompanies a copy of the Program in return for a fee. END OF
TERMS AND CONDITIONS

How to Apply These Terms to Your New Programs

If you develop a new program, and you want it to be of the greatest possible
use to the public, the best way to achieve this is to make it free software
which everyone can redistribute and change under these terms.

To do so, attach the following notices to the program. It is safest to attach
them to the start of each source file to most effectively state the exclusion
of warranty; and each file should have at least the "copyright" line and a
pointer to where the full notice is found.

<one line to give the program's name and a brief idea of what it does.>

Copyright (C) <year> <name of author>

This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.

This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with
this program. If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

If the program does terminal interaction, make it output a short notice like
this when it starts in an interactive mode:

<program> Copyright (C) <year> <name of author>

This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.

This is free software, and you are welcome to redistribute it under certain
conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands might
be different; for a GUI interface, you would use an "about box".

You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>.

The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General Public
License instead of this License. But first, please read <https://www.gnu.org/
licenses /why-not-lgpl.html>.
export SHELL:=/bin/bash
export SHELLOPTS:=$(if $(SHELLOPTS),$(SHELLOPTS):)pipefail:errexit

.ONESHELL:
.PHONY: coverage

build:
	go mod tidy
	cd cmd/ffcss && go build

tests:
# setup mocks
	$(MAKE) mocks-setup
# defer tearing down mocks (so that it runs even if the tests fail)
	trap "$(MAKE) mocks-teardown" EXIT
# run tests, with $HOME overriden to a mocked directory, and gopath re-set, otherwise the compiler freaks out.
	GOPATH=$$(go env GOPATH) HOME=testarea/home GIT_TERMINAL_PROMPT=0 go test -race -coverprofile=coverage.txt -covermode=atomic -parallel 10
# compute code coverage
	$(MAKE) coverage

coverage:
# get the binary to convert from go coverage file to a standard one accepted by codecov & co
	go get -u github.com/jandelgado/gcov2lcov
# run it
	gcov2lcov -infile=coverage.txt -outfile=coverage/lcov.info
# remove it from the go.mod file
	go mod tidy

install:
# make necessary directories
	mkdir -p ~/.config/ffcss/themes ~/.local/bin
# copy builtin themes
	cp themes/*.yaml ~/.config/ffcss/themes/
# copy binary to some standard place that's in $PATH most of the time
	cp cmd/ffcss/ffcss ~/.local/bin/ffcss

format:
	gofmt -s -w **.go
	cd cmd/ffcss; gofmt -s -w *.go

mocks-setup:
# configure git identity (for whatever reason it needs it now)
	git config --global --get user.name || git config --global user.name "Ewen Le Bihan"
	git config --global --get user.email || git config --global user.email hey@ewen.works
# init git repo for testdata/nomanifest
	cd testdata/nomanifest; git init --quiet; git add .; git commit --quiet -m "a"; cd ../..
# local webserver to mock http requests, save its PID to a file so that we can kill it during teardown
	python -m http.server 8080 --bind localhost --directory testdata/ >/dev/null 2>/dev/null & echo $$! > .mockswebserverpid
# create directories that are cleaned up after use
	mkdir -p testarea/{zip-dropoff,cache,home/{.mozilla/firefox/667ekipp.default-release,.{config,cache}/ffcss,.config/ffcss/themes}}
# copy themes into mock config directory
	cp themes/*.yaml testarea/home/.config/ffcss/themes/
# copy static mocks from testdata/ to testarea/
	cp -R testdata/{home,catalogs,manifests}/ testarea/
# create coverage directory
	mkdir -p coverage

mocks-teardown:
	rm -rf testdata/nomanifest/.git
# remove testing artifacts
	rm -rf testarea
# kill mocks webserver
	kill -9 $$(cat .mockswebserverpid)
# remove mocks webserver PID file
	rm .mockswebserverpid


release:
# remove artifacts from previous release
	rm -rf dist/
# build & install binary
	$(MAKE) build
	$(MAKE) install
# update changelog headings
	chachacha release $$(read -p bump=; echo $$REPLY)
# remove stupid bullet points in front of <hX> tags
	sd '^- #' '#' CHANGELOG.md
# extract release notes for the new version only,
# and bump the version in go code
	tools/make_release_notes.rb $$(read -p bump=; echo $$REPLY)
# recompile so that the binary shows the new version when doing ffcss version
	$(MAKE) build
	$(MAKE) install
# make tagged Release commit
	git add CHANGELOG.md ffcss.go
	git commit -m "🔖 Release $$(ffcss version)"
	git tag -am v$$(ffcss version) v$$(ffcss version)
# github push, tags push, github release w/ binaries in .tar.gz, milestone close, etc.
	GITHUB_TOKEN=$$(cat .github_token) goreleaser release --release-notes release_notes.md
# remove extracted release notes
	rm release_notes.md
# ffcss

![a demo of ffcss](./DEMO.gif)

[![wakatime](https://wakatime.com/badge/github/ewen-lbh/ffcss.svg)](https://wakatime.com/badge/github/ewen-lbh/ffcss)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/ewen-lbh/ffcss)
[![Go reference](https://img.shields.io/badge/-reference-grey?logo=go)](https://pkg.go.dev/github.com/ewen-lbh/ffcss)
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/ewen-lbh/ffcss/test)
![Codecov](https://img.shields.io/codecov/c/github/ewen-lbh/ffcss)

A CLI interface to apply and configure [Firefox CSS themes](https://reddit.com/r/FirefoxCSS) (also known as userChrome.css themes).

It uses a YAML file to declare files, variants, helper addons and others. This file is referred to as a "manifest".

## Installation

### AUR

Arch Linux users can install the package through the AUR:

```
yay -S ffcss # latest release
# COMING SOON yay -S ffcss-git # latest commit
# COMING SOON yay -S ffcss-bin # pre-compiled binary of latest release
```

### Pre-compiled binary 

#### Linux & MacOS

_Make sure there isn't a folder called “installation” in your working directory_

```sh
# Don’t pollute your current directory
mkdir installation; cd installation
# Install the latest release by downloading the binary on Github
wget https://github.com/ewen-lbh/ffcss/releases/latest/download/YOUR-OS-64bit.tar.gz -O ffcss.tar.gz # replace YOUR-OS with linux or macos. There's also a 32bit version (386 instead of amd64)
# Extract it
tar -xzvf ffcss.tar.gz
# Install built-in themes
mkdir -p ~/.config/ffcss/themes/
cp -v themes/*.yaml ~/.config/ffcss/themes/
# Make sure the file is marked as executable
chmod +x ffcss
# Move it to a folder that’s in your path (so you can type `ffcss` anywhere), eg.
mv ffcss ~/.local/bin/ffcss
# Remove the extracted contents (you don’t need them anymore)
cd ..; rm -I -rf installation
```

#### Windows

(Assuming your user folder is at `C:\Users\you`)

1. Download the zip file: https://github.com/ewen-lbh/ffcss/releases/latest/download/windows-64bit.zip
1. Extract it somewhere, let's say in `C:\Users\you\Desktop\ffcss`.
1. Open that folder in the Explorer
1. Right-click on empty space and select "Open Windows Terminal Here"
1. Type `.\ffcss.exe`
1. Copy `C:\Users\you\Desktop\ffcss\themes` to `C:\Users\you\.config\ffcss`

To use ffcss, you can run that `ffcss.exe` in the Windows Terminal

### Compile it yourself

_instructions for linux & MacOS, I haven't figured out how to run `make` on Windows, winget installs it but the command is still 'not found'..._

```sh
git clone https://github.com/ewen-lbh/ffcss
cd ffcss
make 
make tests # optional, to make sure everything works
make install
```

## Usage

```docopt
ffcss - Apply and configure FirefoxCSS themes

Usage:
	ffcss [options] use THEME_NAME [VARIANT]
	ffcss [options] get THEME_NAME
	ffcss [options] cache clear
	ffcss [options] init
	ffcss [options] reapply
	ffcss version [COMPONENT]

Where:
	THEME_NAME  a theme name or URL (see README.md)
	COMPONENT   is either major, minor or patch (to get a single digit)

Options:
	-a --all-profiles           Apply the theme to all profiles
	-p --profiles=PATHS      Select which profiles to apply the theme to.
	                         Can be absolute or relative to --profiles-dir.
							 Comma-separated.
	--profiles-dir=PATH      Directory that contains profile directories.
	                         Default value is platform-specific:
	                         - $HOME/.mozilla/firefox                                on Linux
	                         - $HOME/Library/Application Support/Firefox/Profiles    on MacOS
	                         - %appdata%/Roaming/Mozilla/Firefox/Profiles            on Windows
	--skip-manifest-source   Don't ask to show the manifest source
```

#### The `use` command

Synopsis: `ffcss use THEME_NAME [VARIANT_NAME]`

ffcss will first search for a folder at `~/.cache/ffcss/THEME_NAME/VARIANT_NAME`. If not found, it will try to download the theme:


If `THEME_NAME` is of the form `OWNER/REPO`:

- It'll use the repository `github.com/OWNER/REPO`
  
If `THEME_NAME` is of the form `NAME`:

- It'll search for `NAME` in the manifests included with ffcss (see [Built-in themes](#built-in-themes))

The `NAME` here is insensitive to case, whitespace, punctuation (`.`, ` `, `_` and `-`) and unicode ([NFD normalization](http://www.macchiato.com/unicode/nfc-faq) is applied before searching). 

That means that typing `ffcss use FrozenFox` will work even if the theme's manifest is stored in `frozenfox.yaml`, and not `FrozenFox.yaml`.

And if `THEME_NAME` is an URL:

- It'll download the zip file / clone the git repository at `THEME_NAME` (the `https://` part can be omitted)

_Technical note: when no variant is used, `VARIANT_NAME` is "\_"_

<!-- ### The `config` command

Synopsis: `ffcss config KEY [VALUE]`

Much simpler than the `use` command, this one just adds convenience to set `about:config` keys. If `VALUE` is not provided, ffcss will output the specified `KEY`'s current value. -->

### The `reapply` command

Synopsis: `ffcss reapply`

This is the same as doing `ffcss use` with the current theme, useful when firefox updates.

The current theme for each profile is stored in ffcss' configuration folder, in `currently.yaml`

### The `get` command

This is the same as running `use`, but does not actually apply the theme, it just downloads it to the cache.

### The `cache clear` command

Clears the ffcss cache, including all downloaded themes.

### The `init` command

Synopsis: `ffcss init`

Creates a [`ffcss` manifest file](#creating-a-firefoxcss-theme) in the current directory

## What if the theme I want to download has no manifest?

You can create one for yourself, by placing it in `<home folder>/.config/ffcss/<your theme's name>.yaml`, along with the other included themes, then running `ffcss use` with that name.

Consider opening a pull request if you can, so that others can benefit from your manifest without having to write it again.

The next section covers how to create that manifest file. It is written for theme makers, but the only difference is that the manifest you wrote isn't named `ffcss.yaml`.

## Creating a FirefoxCSS theme

So that your users can benefit from the simple installation process provided by ffcss, you can add a `ffcss.yaml` file in the root of your project and declare additional configuration you might need. 

_Note: example use cases from real FirefoxCSS themes are given for illustrative purposes, and I don't claim to speak in place of the theme's author(s), even if it may appear as so_.

You can start by using `ffcss init` in your theme's folder, some values will be filled automatically.


### Download

You can specify in your manifest file from where to download the theme.
This can be useful if

- you want to use that value in `variants`, to download a different zip file
- you want to [add your manifest to the registry](#add-to-the-registry), in which case the repository's URL is needed for ffcss to figure out where to download your theme from
- you want users to download a zip file instead of cloning your repo

### Branch, Tag & Commit

If you use a git repository URL (ffcss will know if any given URL is `git-clone`-able), you can also use these three to:

- change the branch used (`branch: <the branch's name>`)
- change the commit used (`commit: <the commit's SHA>`)
- change the tag used (`tag: <the git tag's name>`)

Here again, this is especially useful for variants (for example, Lepton's photon-style variant is on a separate branch `photon-style`)

### Config

An object mapping `about:config` configuration keys to their values:

```yaml
config:
    svg.context-properties.content.enabled: true
    security.insecure_connection_text.enabled: true
```

Note that `toolkit.legacyUserProfileCustomizations.stylesheets` is set to `true` automatically, no need to declare it.

This will write a generated `user.js` file in the selected profile directory (or directories).
If you provide your own `user.js` file, the generated content will be appended to yours and written on the same file.

### Files

You can use `userChrome`, `userContent` and `user.js` keys to specify where those files are in your repo. You can use [glob patterns][globster].

They will get copied to `<selected profile directory>/chrome/userChrome.css` (or `userContent.css`) and `<selected profile directory>/user.js>`, respectively.

Note that even if _your_ file is not called like this, if will get copied with that name, no manual renaming required.

For example, SimplerentFox uses three different `userContent.css` files for its variants, and the manifest has `{{ os }}/userContent__{{ variant }}.css`.

<!--If not specified, their default values are either `userChrome.css` (or `userContent.css`, or `user.js`) or `null` when the default file is not found.-->

You can also declare other files in `assets`, an array of [glob patterns][globster]. They take precedence over the others keys, since they get copied last.

You can use `{{ os }}`, which will get replaced with one of `windows`, `linux` or `macos`, and `{{ variant }}`, which will get replaced by the variant the user has chosen.


All files will get copied to `<user's profile folder>/chrome/`. You can change the from where they get copied with `copy from`.
This is pretty useful if you store your assets inside a folder (such as `chrome`), to avoid them being nested too much

```yaml
assets: 
    - my-assets/**
copy from: my-assets/ 
```

without the `copy from`, files would get copied to `<user's profile folder>/chrome/my-assets/...` instead of `<user's profile folder>/chrome/...`: `my-assets/` will be a part of the file names described by `my-assets/**`.

For example, MaterialFox stores everything under `chrome/`, and its manifest uses `copy from: chrome/` to tell ffcss to copy assets _from that directory_, not from the repository's root.

[globster]: https://globster.xyz

### Variants

Some themes allow users to choose between different variations. To declare them, add an object with key `variants`, that maps variant names to a configuration object, overriding `config`, `userContent`, etc. for that variation. <!--An additional `description` key is available and will be shown to users when selecting a variant.-->

Note that overriding `config` only overrides values set, it does not remove configuration keys that have been set globally: with the following manifest:

```yaml
ffcss: 1 # signals that the manifest works with ffcss versions 1.X.X

config:
    one.property: yes
    another.property: buckaroo

variants:
    blue:
        config:
            one.property: false
```

choosing the variant "blue" will apply the following config:

```yaml
one.property: false
another.property: buckaroo
```

Properties that can be overriden are:

- repository 
- branch
- tag
- commit
- config
- userChrome
- userContent
- user.js
- assets
- addons
- run
- description


For example, SimplerentFox proposes an addon to enhance the experience, and declares it as such:

```yaml
...

addons:
    - https://addons.mozilla.org/en-US/firefox/addon/simplerentfox/

...
```

### Running custom commands

You can run any shell command after and/or before the installation, with the manifest entries `run`.`before` and `run`.`after`:

```yaml
run:
  before: echo hello from {{ profile_path }}!
  after: wget https://example.com/my-custom-file?version={{ firefox_version }}
```

In both values, `{{ profile_path }}` and `{{ firefox_version }}` will respectively get replaced with the path of the profile to which the theme is being installed, and that profile's firefox version.

### Messages

You can specify a message to be printed at the end of the installation. Markdown syntax is supported.

Useful to provide additional instructions that can't be automated, to suggest optional tweaks, to do shameless promotion or even just to say thanks! 

For example, VerticalTabs uses messages to tell users about additional installation steps that can't be automated:

```yaml
message: |
  Go to about:addons, select Tab Center Reborn, go to Preferences and set:

  - *Animations*: on.
  - *Use current browser theme*: on, if you want to use dark mode.
  - *Compact Mode*: either “Dynamic” or “Enabled”. It works with “Disabled” too but looks nicer with only favicons.
  - *Favicon-only pinned tabs*: off.
  - *Activate Custom Stylesheet* and paste the contents of tabCenterReborn.css into the text area, and click “Save CSS”.
```

### Description & By

Those two fields can be set to be displayed before the installation begins. Something like


```
Installing <name> by <by>

| <description>
```

Will then be shown to the user when starting the installation

### Declaring ffcss' version

Putting a `ffcss: 0` in your manifest tells the user that this theme was made with ffcss version 0.X.X. 
If their version differs, they'll get a warning: major version changes (i.e. when the leftmost number changes) may include _breaking changes_ regarding the manifest file, which means that your theme might not work.

I'll try to not do that though, it sucks to break things. (I may support _different versions of manifest files_, at least)

### Declaring supported Firefox versions

The manifest entry `firefox` can be used to specify which versions of Firefox are compatible with your theme.

Users that have a non-compatible Firefox version will be shown a warning.

The following patterns can be used:

- `<version>` - compatible with that version only.

    For example:

    ```yaml
    firefox: 90
    ```
- `<A>-<B>` - compatible with every version from _A_ to _B_ (including both _A_ and _B_)

    For example:

    ```yaml
    firefox: 86-90
    ```

- `<A>+` - compatible with version _A_ or more recent 

    For example:

    ```yaml
    firefox: 90+
    ```

- `up to <A>` - compatible with every version, up to _A_ (including _A_)

    For example:

    ```yaml
    firefox: up to 88
    ```

Note that you can also be more precise and specify the minor part (the second digit after the dot), for example `firefox: 90.5+`.

### Examples

You can look at some themes' manifests in `themes/*.yaml` to see how they are written.

## Built-in themes

The following themes are provided with ffcss, you can install them with `ffcss use <theme's name>`.

- [alpenblue](https://github.com/Godiesc/AlpenBlue) by [Godiesc](https://github.com/Godiesc)
- [australis-tabs](https://github.com/sagars007/Australis-like-tabs-FF-ProtonUI-changes) by [sagars007](https://github.com/sagars007)
- [blurredfox](https://github.com/manilarome/blurredfox) by [manilarome](https://github.com/manilarome)
- [chameleons-beauty](https://github.com/Godiesc/Chameleons-Beauty) by [Godiesc](https://github.com/Godiesc)
- [compactmode](https://github.com/Godiesc/compactmodefirefoxcss) by [Godiesc](https://github.com/Godiesc)
- [diamondfucsia](https://github.com/Godiesc/DiamondFucsia) by [Godiesc](https://github.com/Godiesc)
- [frozenfox](https://github.com/tortious/FrozenFox) by [tortious](https://github.com/tortious)
- [fxcompact](https://github.com/dannycolin/fx-compact-mode) by [dannycolin](https://github.com/dannycolin)
- [halo](https://github.com/seirin-blu/Firefox-Halo) by [seirin-blu](https://github.com/seirin-blu)
- [lepton](https://github.com/black7375/Firefox-UI-Fix) by [black7375](https://github.com/black7375)
- [martinfox](https://github.com/arp242/MartinFox) by [arp242](https://github.com/arp242)
- [materialfox](https://github.com/muckSponge/MaterialFox) by [muckSponge](https://github.com/muckSponge)
- [modoki](https://github.com/soup-bowl/Modoki-FirefoxCSS) by [soup-bowl](https://github.com/soup-bowl)
- [montereyfox](https://github.com/FirefoxCSSThemers/Monterey-Fox) by [FirefoxCSSThemers](https://github.com/FirefoxCSSThemers)
- [pro-fox](https://github.com/xmansyx/Pro-Fox) by [xmansyx](https://github.com/xmansyx)
- [proton-connected-rounded-tabs](https://github.com/sagars007/Proton-UI-connected-rounded-tabs) by [sagars007](https://github.com/sagars007)
- [simplerentfox](https://github.com/MiguelRAvila/SimplerentFox) by [MiguelRAvila](https://github.com/MiguelRAvila)
- [technetium](https://github.com/edo0/Technetium) by [edo0](https://github.com/edo0)
- [verticaltabs](https://github.com/ranmaru22/firefox-vertical-tabs) by [ranmaru22](https://github.com/ranmaru22)
- [wavefox](https://github.com/QNetITQ/WaveFox) by [QNetITQ](https://github.com/QNetITQ)

### Add to registry

You can add your theme to this list, just fork the repository, copy your `ffcss.yaml` theme as `themes/<your theme's name>.yaml`, and submit a PR!
package ffcss

import (
	"fmt"
	"os/exec"
)

// InstallAddon install the addon at addonURL on this profile.
// Currently, it justs opens firefox with that URL so that the user can manually install it (hence the operatingSystem argument).
func (ffp FirefoxProfile) InstallAddon(operatingSystem string, addonURL string) error {
	LogStep(1, "Opening [blue][bold]%s", addonURL)
	LogStep(1, "[yellow]Waiting for you to close Firefox")
	var command *exec.Cmd
	switch operatingSystem {
	case "linux":
		command = exec.Command("firefox", "--new-tab", addonURL, "--profile", ffp.Path)
	case "macos":
		command = exec.Command("open", "-a", "firefox", addonURL, "--args", "--profile", ffp.Path)
	case "windows":
		command = exec.Command("start", "firefox", "-profile", ffp.Path, addonURL)
	default:
		LogWarning("unrecognized OS %s, cannot open firefox automatically. Open %s in firefox using profile %s", operatingSystem, addonURL, ffp)
		return nil
	}
	err := command.Run()
	if err != nil {
		return fmt.Errorf("couldn't open %q: while running %s: %w", addonURL, command.String(), err)
	}
	return nil
}
package ffcss

import (
	"bytes"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestInstallAddon(t *testing.T) {
	// Empty the buffer
	mockedStdout = bytes.Buffer{}
	BaseIndentLevel = 0
	mockedProfile.InstallAddon("linux", "https://example.com")
	// assert.NoError(t, err) FIXME firefox won't open with the given profile even if the mocked .mozilla/firefox dir is copied exactly from my ~
	assert.Equal(t,
		"  \x1b[36m•\x1b[0m Opening \x1b[34m\x1b[1mhttps://example.com\x1b[0m\n  \x1b[36m•\x1b[0m \x1b[33mWaiting for you to close Firefox\x1b[0m\n",
		mockedStdout.String(),
	)

	// Empty the buffer
	mockedStdout = bytes.Buffer{}
	BaseIndentLevel = 0
	err := mockedProfile.InstallAddon("goretijgoierjogirej", "https://example.com")
	assert.NoError(t, err)
	assert.Equal(t,
		"  \x1b[36m•\x1b[0m Opening \x1b[34m\x1b[1mhttps://example.com\x1b[0m\n  \x1b[36m•\x1b[0m \x1b[33mWaiting for you to close Firefox\x1b[0m\n\x1b[33m\x1b[1munrecognized OS goretijgoierjogirej, cannot open firefox automatically. Open https://example.com in firefox using profile default-release (667ekipp)\n\x1b[0m",
		mockedStdout.String(),
	)
}
package ffcss

import (
	"fmt"
	"path/filepath"
	"strings"

	"github.com/bmatcuk/doublestar"
	"github.com/hoisie/mustache"
)

func renderFileTemplate(f FileTemplate, operatingSystem string, variant Variant, osRenameMap map[string]string) string {
	if strings.Contains(strings.Trim(f, " "), "{{variant}}") && variant.Name == "" {
		LogWarning("%q uses {{variant}} which is empty\n", f)
	}
	var osName string
	if osRenameMap[operatingSystem] == "" {
		osName = operatingSystem
	} else {
		osName = osRenameMap[operatingSystem]
	}
	return mustache.Render(f, map[string]string{
		"os":      osName,
		"variant": variant.Name,
	})
}

// DestinationPathOfAsset computes the destination path of some asset from its path and the destination profile directory
// It is assumed that assetPath is absolute.
func (t Theme) DestinationPathOfAsset(assetPath string, profileDir string, operatingSystem string, variant Variant) (string, error) {
	if !strings.HasPrefix(assetPath, t.DownloadedTo) {
		return "", fmt.Errorf("asset %q is outside of the theme's root %q", assetPath, t.DownloadedTo)
	}

	relativeTo := filepath.Clean(filepath.Join(t.DownloadedTo, filepath.Clean(renderFileTemplate(t.CopyFrom, operatingSystem, variant, t.OSNames))))
	if !strings.HasPrefix(relativeTo, t.DownloadedTo) {
		return "", fmt.Errorf("copy from %q is outside of the theme's root %q", relativeTo, t.DownloadedTo)
	}

	relativised, err := filepath.Rel(relativeTo, assetPath)
	if err != nil {
		return "", fmt.Errorf("couldn't make %s relative to %s: %w", assetPath, filepath.Join(t.DownloadedTo, filepath.Clean(t.CopyFrom)), err)
	}

	return filepath.Join(profileDir, "chrome", relativised), nil
}

// AssetsPaths returns the individual file paths of all assets.
func (t Theme) AssetsPaths(os string, variant Variant) ([]string, error) {
	resolvedFiles := make([]string, 0)
	for _, template := range t.Assets {
		glob := renderFileTemplate(template, os, variant, t.OSNames)
		LogDebug("looking for assets: globbing %q", filepath.Join(t.DownloadedTo, glob))
		glob = filepath.Clean(filepath.Join(t.DownloadedTo, glob))
		files, err := doublestar.Glob(glob)
		if err != nil {
			return resolvedFiles, fmt.Errorf("while getting all matches of glob %s: %w", glob, err)
		}
		// If no matches
		if len(files) < 1 {
			// If it's _really_ a glob pattern
			if strings.Contains(glob, "*") {
				return resolvedFiles, fmt.Errorf("glob pattern %s matches no files", glob)
			}
			// If it's just a regular file (that was treated as a glob pattern)
			return resolvedFiles, fmt.Errorf("file %s not found", glob)
		}
		// For each match of the glob pattern
		resolvedFiles = append(resolvedFiles, files...)
	}
	return resolvedFiles, nil
}
package ffcss

import (
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestRenderFileTemplate(t *testing.T) {
	assert.Equal(t, "userChrome.css", renderFileTemplate(
		"userChrome.css",
		"linux",
		Variant{},
		map[string]string{},
	))

	assert.Equal(t, "linux.css", renderFileTemplate(
		"linux.css",
		"linux",
		Variant{},
		map[string]string{"linux": "Linux"},
	))

	assert.Equal(t, "GNU/Linux.css", renderFileTemplate(
		"{{os}}.css",
		"linux",
		Variant{},
		map[string]string{"linux": "GNU/Linux"},
	))

	assert.Equal(t, "linux.css", renderFileTemplate(
		"linux.css",
		"windows",
		Variant{},
		map[string]string{},
	))

	assert.Equal(t, "./macos/rainbow.css", renderFileTemplate(
		"./{{ os }}/{{variant}}.css",
		"macos",
		Variant{Name: "rainbow"},
		map[string]string{},
	))

}

func TestDestinationPathOfAsset(t *testing.T) {
	manifest := Theme{
		DownloadedTo: CacheDir("simplerentfox/_"),
		ExplicitName: "materialfox",
		Variants:     map[string]Variant{},
		Config:       Config{},
	}

	file, err := manifest.DestinationPathOfAsset("/home/ewen/lol.pdf", testarea, "linux", Variant{})
	if assert.Error(t, err) {
		assert.Regexp(t, `asset ".+" is outside of the theme's root ".+"`, err.Error())
		assert.Equal(t, "", file)
	}

	file, err = manifest.DestinationPathOfAsset("testarea/home/.cache/ffcss/simplerentfox/../../../lol.pdf", testarea, "linux", Variant{})
	if assert.Error(t, err) {
		assert.Regexp(t, `asset ".+" is outside of the theme's root ".+"`, err.Error())
		assert.Equal(t, "", file)
	}

	manifest = Theme{
		DownloadedTo: CacheDir("simplerentfox/_"),
		ExplicitName: "simplerentfox",
		Variants: map[string]Variant{
			"WithoutURLBar": {},
			"OneLine":       {},
		},
		CopyFrom: "{{ os }}/",
		OSNames: map[string]string{
			"linux": "Linux",
		},
	}

	file, err = manifest.DestinationPathOfAsset(CacheDir("simplerentfox/_/Linux/userChrome__OneLine.css"), testarea, "linux", Variant{Name: "OneLine"})
	assert.NoError(t, err)
	assert.Equal(t, filepath.Join(testarea, "chrome", "userChrome__OneLine.css"), file)
}

func TestAssetsPaths(t *testing.T) {
	simplerentfox := Theme{
		ExplicitName: "simplerentfox",
		DownloadedTo: CacheDir("simplerentfox/_"),
		Variants: map[string]Variant{
			"WithoutURLBar": {},
			"OneLine":       {},
		},
		Assets: []string{"./{{ os }}/userChrome__{{ variant }}.css"}, // for the purposes of testing
		OSNames: map[string]string{
			"linux":   "Linux",
			"macos":   "Linux",
			"windows": "Windows",
		},
	}

	files, err := simplerentfox.AssetsPaths("linux", Variant{Name: "blue"})
	assert.Regexp(t, "file .+ not found", err.Error())
	assert.Equal(t, []string{}, files)

	files, err = simplerentfox.AssetsPaths("linux", Variant{Name: "OneLine"})
	assert.NoError(t, err)
	assert.Equal(t, []string{CacheDir("simplerentfox/_/Linux/userChrome__OneLine.css")}, files)
}
package ffcss

import (
	"fmt"
	"os"
)

// CleanDownloadArea removes the temporary download area used to download themes before knowing their name from their manifest
func CleanDownloadArea() error {
	return os.RemoveAll(CacheDir(TempDownloadsDirName))
}

// ClearWholeCache destroys the cache directory
func ClearWholeCache() error {
	err := os.RemoveAll(CacheDir())
	if err != nil {
		return fmt.Errorf("while deleting directory tree %q: %w", CacheDir(), err)
	}

	err = os.Mkdir(CacheDir(), 0777)
	if err != nil {
		return fmt.Errorf("while re-creating empty cache directory at %q: %w", CacheDir(), err)
	}

	return nil
}
package ffcss

import (
	"io/fs"
	"os"
	"os/exec"
	"testing"

	"github.com/stretchr/testify/assert"
)

func fillCache() {
	exec.Command("cp", "-r", "testdata/home/.cache", "testarea/home")
	LogDebug("filled mock cache")
}

func TestCleanDownloadArea(t *testing.T) {
	fillCache()
	initialCacheDir, _ := os.ReadDir(CacheDir())
	expectedCacheDir := make([]fs.DirEntry, 0)
	for _, dirEntry := range initialCacheDir {
		if dirEntry.Name() != ".download" {
			expectedCacheDir = append(expectedCacheDir, dirEntry)
		}
	}
	err := CleanDownloadArea()
	assert.NoError(t, err)
	actualCacheDir, err := os.ReadDir(CacheDir())
	assert.NoError(t, err)
	assert.Equal(t, expectedCacheDir, actualCacheDir)
}

func TestClearWholeCache(t *testing.T) {
	fillCache()
	err := ClearWholeCache()
	assert.NoError(t, err)
	actual, err := os.ReadDir(CacheDir())
	assert.NoError(t, err)
	assert.Equal(t, []fs.DirEntry{}, actual)
}
package ffcss

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/hbollon/go-edlib"
	"golang.org/x/text/unicode/norm"
)

// Catalog represents a collection of themes
type Catalog map[string]Theme

// Lookup looks up a theme by its name in the theme store.
// It also returns an error starting with "did you mean:" when
// a theme name is not found but themes with similar names exist.
func (store Catalog) Lookup(query string) (Theme, error) {
	originalQuery := query
	query = lookupPreprocess(query)
	LogDebug("using query %q", query)
	processedThemeNames := make([]string, 0, len(store))
	for _, theme := range store {
		LogDebug("\tlooking up against %q (%q)", lookupPreprocess(theme.Name()), theme.Name())
		if lookupPreprocess(theme.Name()) == query {
			return theme, nil
		}
		processedThemeNames = append(processedThemeNames, lookupPreprocess(theme.Name()))
	}
	// Use fuzzy search for did-you-mean errors
	suggestion, _ := edlib.FuzzySearchThreshold(query, processedThemeNames, 0.75, edlib.Levenshtein)

	if suggestion != "" {
		return Theme{}, fmt.Errorf("theme %q not found. did you mean [blue][bold]%s[reset]?", originalQuery, suggestion)
	}
	return Theme{}, fmt.Errorf("theme %q not found", originalQuery)
}

// lookupPreprocess applies transformations to s so that it can be compared
// to search for something.
// For example, it is used by (ThemeStore).Lookup
func lookupPreprocess(s string) string {
	return strings.ToLower(regexp.MustCompile(`[-_ .]`).ReplaceAllString(norm.NFKD.String(s), ""))
}

// LoadCatalog loads a directory of theme manifests.
// Keys are theme names (files' basenames with the .yaml removed).
func LoadCatalog(storeDirectory string) (themes Catalog, err error) {
	themeNamePattern := regexp.MustCompile(`^(.+)\.ya?ml$`)
	themes = make(Catalog)
	manifests, err := os.ReadDir(storeDirectory)
	if err != nil {
		return
	}
	LogDebug("loading potential themes %v into catalog", func() []string {
		dirNames := make([]string, 0, len(manifests))
		for _, dir := range manifests {
			dirNames = append(dirNames, dir.Name())
		}
		return dirNames
	}())
	for _, manifest := range manifests {
		if !themeNamePattern.MatchString(manifest.Name()) {
			continue
		}
		themeName := themeNamePattern.FindStringSubmatch(manifest.Name())[1]
		theme, err := LoadManifest(filepath.Join(storeDirectory, manifest.Name()))
		if err != nil {
			return nil, fmt.Errorf("while loading theme %q: %w", themeName, err)
		}
		LogDebug("\tadding theme from manifest %q", manifest.Name())
		themes[themeName] = theme
	}
	return
}
package ffcss

import (
	"path/filepath"
	"sort"
	"testing"

	"github.com/stretchr/testify/assert"
)

func makeTheme(name string) Theme {
	t := NewTheme()
	t.ExplicitName = name
	return t
}

func TestCatalogLookup(t *testing.T) {
	catalog := Catalog{
		"lorem": makeTheme("lorem"),
		"ipsum": makeTheme("ipsum"),
		"dolor": makeTheme("a_completely_different_name"),
		"bacon": makeTheme("bacon"),
	}

	actual, err := catalog.Lookup("lorem")
	assert.NoError(t, err)
	assert.Equal(t, makeTheme("lorem"), actual)

	actual, err = catalog.Lookup("a.completely different-name")
	assert.NoError(t, err)
	assert.Equal(t, makeTheme("a_completely_different_name"), actual)

	actual, err = catalog.Lookup("dolor")
	assert.Error(t, err, `theme "dolor" not found`)
	assert.Equal(t, Theme{}, actual)

	actual, err = catalog.Lookup("baaaa con")
	assert.Error(t, err, `theme "baaaa con" not found. did you mean [blue][bold]bacon[reset]?`)
	assert.Equal(t, Theme{}, actual)
}

func TestLookupPreprocess(t *testing.T) {
	cases := []struct{ in, out string }{
		{"same", "same"},
		{"B  O   N    K", "bonk"},
		{"you. shall. not. pass.", "youshallnotpass"},
		{"wHaTevER", "whatever"},
		{"all-thE.puncT_uAtio    n", "allthepunctuation"},
		{"with great power\u037e comes great responsibility.", "withgreatpower;comesgreatresponsibility"},
		{"𝔽𝔸ℕℂ𝕐", "fancy"},
	}

	for _, caze := range cases {
		assert.Equal(t, caze.out, lookupPreprocess(caze.in))
	}
}

func TestLoadCatalog(t *testing.T) {
	actual, err := LoadCatalog(filepath.Join(testarea, "catalogs/empty"))
	assert.NoError(t, err)
	assert.Equal(t, make(Catalog), actual)

	actual, err = LoadCatalog(filepath.Join(testarea, "catalogs/various"))
	assert.NoError(t, err)
	actualNames := make([]string, 0, len(actual))
	for name := range actual {
		actualNames = append(actualNames, name)
	}
	sort.Strings(actualNames)
	assert.Equal(t, []string{"alpenblue", "flyingfox", "montereyfox", "sometheme"}, actualNames)
}
ffcss - Apply and configure FirefoxCSS themes

Usage:
	ffcss [options] use THEME_NAME [VARIANT]
	ffcss [options] get THEME_NAME
	ffcss [options] cache clear
	ffcss [options] init
	ffcss [options] reapply
	ffcss [options] reset
	ffcss [options] version [COMPONENT]

Where:
	THEME_NAME  a theme name or URL (see README.md)
	COMPONENT   is either major, minor or patch (to get a single digit)

Options:
	-a --all-profiles        Apply the theme to all profiles
	-p --profiles=PATHS      Select which profiles to apply the theme to.
	                         Can be absolute or relative to --profiles-dir.
							 Comma-separated.
	--profiles-dir=PATH      Directory that contains profile directories.
	                         Default value is platform-specific:
	                         - $HOME/.mozilla/firefox                                on Linux
	                         - $HOME/Library/Application Support/Firefox/Profiles    on MacOS
	                         - %appdata%/Roaming/Mozilla/Firefox/Profiles            on Windows
	-d --default-profile     Apply the themes to the default profile (ending with default-release)
	--skip-manifest-source   Don't ask to show the manifest source
package main

import (
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/docopt/docopt-go"
	"github.com/ewen-lbh/ffcss"
	"gopkg.in/yaml.v2"
)

func runCommandUse(args flagsAndArgs) error {
	err := ffcss.CreateDataDirectories()
	if err != nil {
		return err
	}

	ffcss.LogStep(0, "Resolving the theme's name")
	uri, typ, err := ffcss.ResolveURL(args.string("THEME_NAME"))
	if err != nil {
		return fmt.Errorf("while resolving name %s: %w", args.string("THEME_NAME"), err)
	}

	ffcss.LogStep(0, "Downloading the theme")
	manifest, err := ffcss.Download(uri, typ)
	if err != nil {
		return err
	}

	ffcss.DescribeTheme(manifest, ffcss.BaseIndentLevel)

	manifest.AskToSeeManifestSource(args.bool("--skip-manifest-source"))

	// Detect OS
	operatingSystem := ffcss.GOOStoOS(runtime.GOOS)

	selectedProfiles, err := ffcss.SelectProfiles(
		args.strings("--profiles"),
		args.string("--profiles-dir"),
		args.bool("--default-profile"),
		args.bool("--all-profiles"),
	)
	if err != nil {
		return err
	}

	if len(selectedProfiles) == 0 {
		return nil
	}
	singleProfile := len(selectedProfiles) == 1

	incompatibleProfiles, err := manifest.IncompatibleProfiles(selectedProfiles)
	if err != nil {
		return fmt.Errorf("while checking for incompatible profiles: %w", err)
	}

	if len(incompatibleProfiles) != 0 {
		ffcss.LogStep(1, "[yellow]This theme ensures compatibility with firefox [bold]%s[reset][yellow]. The following themes could be incompatible:", manifest.FirefoxVersionConstraint.Sentence)
		for _, profile := range incompatibleProfiles {
			ffcss.LogStep(2, "%s [dim]([reset]version [blue][bold]%s[reset][dim])", profile.Profile, profile.Version)
		}
	}

	// Choose variant
	variant := ffcss.Variant{}
	if len(manifest.AvailableVariants()) > 0 {
		variantName, _ := args.String("VARIANT")
		if variantName == "" {
			var cancel bool
			variant, cancel = manifest.ChooseVariant()
			if cancel {
				return nil
			}
		} else {
			var found bool
			variant, found = manifest.Variants[variantName]
			if !found {
				return fmt.Errorf("variant %q does not exist on this theme. Available variants are %s", variantName, strings.Join(manifest.AvailableVariants(), ", "))
			}
		}
		manifest, actionsNeeded := manifest.WithVariant(variant)
		manifest.ReDownloadIfNeeded(actionsNeeded)
	}

	// Check for OS compatibility
	manifest.WarnIfIncompatibleWithOS(operatingSystem)

	// For each profile directory...
	if singleProfile {
		ffcss.BaseIndentLevel--
	}
	for _, profile := range selectedProfiles {
		if !singleProfile {
			ffcss.LogStep(0, "With profile "+filepath.Base(profile.Path))
		}

		ffcss.LogStep(1, "Backing up the current theme")
		err = profile.BackupChrome()
		if err != nil {
			return fmt.Errorf("while backing up chrome directory: %w", err)
		}
		err = profile.BackupUserJS()
		if err != nil {
			return fmt.Errorf("while backing up user.js: %w", err)
		}

		// Run pre-install script
		if manifest.Run.Before != "" {
			ffcss.LogStep(1, "Running pre-install script")
			// TODO for this to be useful, print commandline _with mustaches replaced_:  Step(baseIndent+2, "[dim]$ bash -c [reset][bold]%s", manifest.Run.Before)
			output, err := manifest.RunPreInstallHook(profile)
			if err != nil {
				return fmt.Errorf("while running pre-install script: %w", err)
			}
			ffcss.ShowHookOutput(output)
		}

		err := os.Mkdir(filepath.Join(profile.Path, "chrome"), 0700)
		if err != nil {
			return err
		}

		// Install stuff
		ffcss.LogStep(1, "Installing the theme")
		err = manifest.InstallUserChrome(operatingSystem, variant, profile.Path)
		if err != nil {
			return fmt.Errorf("couldn't install userChrome.css: %w", err)
		}

		err = manifest.InstallUserContent(operatingSystem, variant, profile.Path)
		if err != nil {
			return fmt.Errorf("couldn't install userContent.css: %w", err)
		}

		err = manifest.InstallUserJS(operatingSystem, variant, profile.Path)
		if err != nil {
			return fmt.Errorf("couldn't install user.js: %w", err)
		}

		err = manifest.InstallAssets(operatingSystem, variant, profile.Path)
		if err != nil {
			return fmt.Errorf("couldn't install assets: %w", err)
		}

		// Run post-install script
		if manifest.Run.After != "" {
			ffcss.LogStep(1, "Running post-install script")
			// TODO for this to be useful, print commandline _with mustaches replaced_:  Step(baseIndent+2, "[dim]$ bash -c [reset][bold]%s", manifest.Run.After)
			output, err := manifest.RunPostInstallHook(profile)
			if err != nil {
				return fmt.Errorf("while running post-install script: %w", err)
			}
			ffcss.ShowHookOutput(output)
		}

		err = profile.RegisterCurrentTheme(args.string("THEME_NAME"))
		if err != nil {
			return fmt.Errorf("while registering current theme for profile %q: %w", profile.FullName(), err)
		}

	}
	if singleProfile {
		ffcss.BaseIndentLevel++
	}

	// Ask to open extensions' pages
	if len(manifest.Addons) > 0 {
		if ffcss.ConfirmInstallAddons(manifest.Addons) {
			for _, profile := range selectedProfiles {
				ffcss.LogStep(0, "With profile "+filepath.Base(profile.Path))
				for _, addonURL := range manifest.Addons {
					profile.InstallAddon(operatingSystem, addonURL)
				}
			}
		}
	}

	// Show message
	err = manifest.ShowMessage()
	if err != nil {
		return fmt.Errorf("couldn't display the message: %w", err)
	}
	return nil
}

func runCommandGet(args flagsAndArgs) error {
	themeName, _ := args.String("THEME_NAME")
	// variant, _ := args.String("VARIANT")

	err := ffcss.CreateDataDirectories()
	if err != nil {
		return err
	}

	ffcss.LogStep(0, "Resolving the theme's name")
	uri, typ, err := ffcss.ResolveURL(themeName)
	if err != nil {
		return fmt.Errorf("while resolving name %s: %w", themeName, err)
	}

	ffcss.LogStep(0, "Downloading the theme")
	manifest, err := ffcss.Download(uri, typ)
	if err != nil {
		return err
	}

	ffcss.LogStepC("✓", 0, "Downloaded [blue][bold]%s[reset] [dim](to %s)", manifest.Name(), manifest.DownloadedTo)
	return nil
}

func runCommandReapply(args flagsAndArgs) error {
	operatingSystem := ffcss.GOOStoOS(runtime.GOOS)
	profilesDir, _ := args.String("--profiles-dir")
	var profilesPaths []string
	var err error
	if profilesDir != "" {
		profilesPaths, err = ffcss.ProfilePaths(operatingSystem, profilesDir)
	} else {
		profilesPaths, err = ffcss.ProfilePaths(operatingSystem)
	}
	if err != nil {
		return fmt.Errorf("while getting profiles: %w", err)
	}

	currentThemesFileContents, err := os.ReadFile(ffcss.ConfigDir("currently.yaml"))
	if err != nil {
		return fmt.Errorf("while reading current themes list: %w", err)
	}

	currentThemes := make(map[string]string)
	yaml.Unmarshal(currentThemesFileContents, &currentThemes)

	for _, profilePath := range profilesPaths {
		themeName, exists := currentThemes[filepath.Base(profilePath)]
		if !exists {
			ffcss.LogStep(0, "[yellow]Profile %s[reset][yellow] has no ffcss theme applied, skipping.", ffcss.NewFirefoxProfileFromPath(profilePath).Display())
			continue
		}
		ffcss.LogStep(0, "Apply theme [blue][bold]%s[reset] to profile %s", themeName, ffcss.NewFirefoxProfileFromPath(profilePath).Display())

		useArgs, _ := docopt.ParseArgs(usage, []string{"use", string(themeName), "--profiles", profilePath, "--skip-manifest-source"}, ffcss.VersionString)
		ffcss.BaseIndentLevel++
		err = runCommandUse(flagsAndArgs{useArgs})
		if err != nil {
			return err
		}
	}
	return nil
}

func runCommandReset(args flagsAndArgs) error {
	profiles, err := ffcss.SelectProfiles(args.strings("--profiles"), args.string("--profiles-dir"), args.bool("--default-profile"), args.bool("--all-profiles"))
	if err != nil {
		return err
	}
	for _, profile := range profiles {
		ffcss.LogStep(0, "With profile %s", profile.Display())
		ffcss.LogStep(1, "Removing the current theme")
		ffcss.LogStep(2, "Moving chrome/ to chrome.bak/")
		err = profile.BackupChrome()
		if err != nil {
			return fmt.Errorf("couldn't back up the chrome folder: %w", err)
		}
		ffcss.LogStep(2, "Moving user.js to user.js.bak")
		err = profile.BackupUserJS()
		if err != nil {
			return fmt.Errorf("couldn't back up user.js: %w", err)
		}
	}
	return nil
}

func runCommandInit(args flagsAndArgs) error {
	// TODO: set user{Chrome,Content,.js} by finding their path
	// TODO: only set assets if chrome/ actually exists
	workingDir, err := os.Getwd()
	if err != nil {
		return fmt.Errorf("could not get working directory: %w", err)
	}

	theme, err := ffcss.InitializeTheme(workingDir)
	if err != nil {
		return fmt.Errorf("while initializing theme: %w", err)
	}

	return theme.WriteManifest(workingDir)
}
// Apply and configure FirefoxCSS themes
package main

import (
	"fmt"
	"io"
	"os"
	_ "embed"

	"github.com/docopt/docopt-go"
	"github.com/ewen-lbh/ffcss"
)

//go:embed USAGE
var usage string

var out io.Writer = os.Stdout

func main() {
	args, _ := docopt.ParseDoc(usage)

	err := os.MkdirAll(ffcss.CacheDir(), 0700)
	if err != nil {
		panic(err)
	}

	err = os.MkdirAll(ffcss.ConfigDir(), 0700)
	if err != nil {
		panic(err)
	}

	if err := dispatchCommand(flagsAndArgs{args}); err != nil {
		fmt.Fprintln(out)
		ffcss.LogError("Woops! An error occurred:")
		fmt.Fprintln(out)
		ffcss.DisplayErrorMessage(err)
	}
}

func dispatchCommand(args flagsAndArgs) error {
	ffcss.LogDebug("dispatching %#v", args)
	if val, _ := args.Bool("configure"); val {
		return fmt.Errorf("not implemented")
	}
	if val, _ := args.Bool("use"); val {
		err := runCommandUse(args)
		return err
	}
	if val, _ := args.Bool("get"); val {
		err := runCommandGet(args)
		return err
	}
	if val, _ := args.Bool("reapply"); val {
		err := runCommandReapply(args)
		return err
	}
	if val, _ := args.Bool("init"); val {
		err := runCommandInit(args)
		return err
	}
	if val, _ := args.Bool("cache"); val {
		if val, _ := args.Bool("clear"); val {
			return ffcss.ClearWholeCache()
		}
	}
	if val, _ := args.Bool("reset"); val {
		return runCommandReset(args)
	}
	if val, _ := args.Bool("version"); val {
		component, _ := args.String("COMPONENT")
		switch component {
		case "major":
			fmt.Fprintln(out, ffcss.VersionMajor)
		case "minor":
			fmt.Fprintln(out, ffcss.VersionMinor)
		case "patch":
			fmt.Fprintln(out, ffcss.VersionPatch)
		default:
			fmt.Fprintln(out, ffcss.VersionString)
		}
	}
	return nil
}
package main

import (
	"strings"

	"github.com/docopt/docopt-go"
	"github.com/ewen-lbh/ffcss"
)

type flagsAndArgs struct {
	docopt.Opts
}

func (o flagsAndArgs) string(name string) string {
	val, err := o.String(name)
	if err != nil {
		ffcss.LogDebug("while getting value of %s: %s: using %#v", name, err.Error(), val)
	}
	return val
}

func (o flagsAndArgs) bool(name string) bool {
	val, err := o.Bool(name)
	if err != nil {
		ffcss.LogDebug("while getting value of %s: %s: using %#v", name, err.Error(), val)
	}
	return val
}

func (o flagsAndArgs) strings(name string) []string {
	val, err := o.String(name)
	if err != nil {
		ffcss.LogDebug("while getting value of %s: %s: using an empty arary", name, err.Error())
		return []string{}
	}
	return strings.Split(val, ",")
}
package ffcss

import (
	"fmt"
	"os"

	"gopkg.in/yaml.v2"
)

// CurrentThemeByProfile returns a map mapping a profile path to its current theme's name.
func CurrentThemeByProfile() (map[string]string, error) {
	currentThemesRaw, err := os.ReadFile(ConfigDir("currently.yaml"))
	if os.IsNotExist(err) {
		err = os.WriteFile(ConfigDir("currently.yaml"), []byte(""), 0777)
		if err != nil {
			return nil, fmt.Errorf("while creating current themes list file: %w", err)
		}
	} else if err != nil {
		return nil, fmt.Errorf("while reading current themes list: %w", err)
	}

	currentThemes := make(map[string]string)
	yaml.Unmarshal(currentThemesRaw, &currentThemes)
	return currentThemes, nil
}

// RegisterCurrentTheme writes the currently.yaml file in ffcss' configuration to update
// what ffcss considers to be the current theme for that profile.
func (ffp FirefoxProfile) RegisterCurrentTheme(themeName string) error {
	currentThemes, err := CurrentThemeByProfile()
	if err != nil {
		return err
	}
	currentThemes[ffp.FullName()] = themeName
	currentThemesNewContents, err := yaml.Marshal(currentThemes)
	if err != nil {
		return fmt.Errorf("while marshaling into YAML: %w", err)
	}

	err = os.WriteFile(ConfigDir("currently.yaml"), currentThemesNewContents, 0777)
	if err != nil {
		return fmt.Errorf("while writing new contents: %w", err)
	}

	return nil
}
package ffcss

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestCurrentThemeByProfile(t *testing.T) {
	actual, err := CurrentThemeByProfile()

	assert.NoError(t, err)
	assert.Equal(t, map[string]string{
		"thingie": "hmmmm",
		"stuff":   "yesees",
	}, actual)
}
package ffcss

import (
	"fmt"
	"os"
	"path/filepath"
)

// CreateDataDirectories makes the required directories for ffcss to function properly.
func CreateDataDirectories() error {
	err := os.MkdirAll(filepath.Join(getConfigDir(), "themes"), 0777)
	if err != nil {
		return fmt.Errorf("couldn't create data directories: %w", err)
	}
	return nil
}
package ffcss

import (
	"os"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestCreateDataDirectories(t *testing.T) {
	err := CreateDataDirectories()
	assert.NoError(t, err)
	actual, err := os.ReadDir(getConfigDir())
	assert.NoError(t, err)
	dirnames := make([]string, 0, len(actual))
	for _, n := range actual {
		dirnames = append(dirnames, n.Name())
	}
	assert.Contains(t, dirnames, "themes")
}
package ffcss

import (
	"errors"
	"fmt"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"

	"github.com/evilsocket/islazy/zip"
)

// RootVariantName is the name of the default variant, when none were applied.
// This is used when computing the location of the cached theme: a variant-less theme is downloaded to <theme name>/_.
const RootVariantName = "_"

// TempDownloadsDirName is the name of the directory used to download themes when the theme's name is not known.
// See the source code of Download for more insights.
// A theme cannot have that name.
const TempDownloadsDirName = ".download"

// ResolveURL resolves the THEME_NAME given to ffcss use to either:
// - a URL to download
// - a git repo URL to clone
func ResolveURL(themeName string) (URL string, typ string, err error) {
	protocolLessURL := regexp.MustCompile(`^[\w_-]+\.[\w_-]+/.*$`)
	userSlashRepo := regexp.MustCompile(`^[\w_-]+/[\w_-]+$`)
	var completeURL string

	// Try OWNER/REPO
	if userSlashRepo.MatchString(themeName) {
		completeURL = "https://github.com/" + themeName
		if !isURLClonable(completeURL) {
			return "", "", fmt.Errorf("%s is not clonable. Make sure it exists", completeURL)
		}
		// Try DOMAIN.TLD/PATH
	} else if protocolLessURL.MatchString(themeName) {
		completeURL = "https://" + themeName
		if !isValidURL(completeURL) {
			return "", "", fmt.Errorf("%q is not a valid URL", completeURL)
		}
		// Try URL
	} else if isValidURL(themeName) {
		completeURL = themeName
	} else {
		return themeName, "bare", nil
	}

	if isURLClonable(completeURL) {
		return completeURL, "git", nil
	}
	return completeURL, "website", nil
}

// Download downloads the theme at URL.
// If typ is website, then it downloads the zip file and extracts it.
// If typ is git, then it clones the repository
// If typ is bare, then it tries to find the URL in ~/.config/ffcss/themes/{{URL}}.yaml
// In all cases, the theme is downloaded to ~/.cache/ffcss/{{themeName}}.
// If themeName is not provided, the theme will first be downloaded to a temporary location to get the name from the manifest.
func Download(URL string, typ string, themeManifest ...Theme) (manifest Theme, err error) {
	LogDebug("typ is %s", typ)
	if len(themeManifest) >= 1 {
		manifest = themeManifest[0]
		LogDebug("manifest is provided")
		// Don't re-download if it already exists
		LogDebug("checking if theme is in cache @ %s", manifest.DownloadedTo)
		stat, err := os.Stat(manifest.DownloadedTo)
		if err == nil && stat.IsDir() {
			LogDebug("skipped downloading of %s [%s#%s]", URL, manifest.Name(), manifest.currentVariantName)
			return manifest, nil
		}
	}
	err = os.MkdirAll(CacheDir(TempDownloadsDirName), 0777)
	if err != nil {
		return manifest, fmt.Errorf("couldn't create %s: %w", CacheDir(TempDownloadsDirName), err)
	}
	tempDir, err := os.MkdirTemp(CacheDir(TempDownloadsDirName), "*")
	if err != nil {
		return manifest, fmt.Errorf("couldn't create a temporary directory at %s: %w", CacheDir(TempDownloadsDirName), err)
	}
	switch typ {
	case "website":
		manifest, err = DownloadFromZip(URL, tempDir, CacheDir(), themeManifest...)
		if err != nil {
			return manifest, fmt.Errorf("couldn't use the zip file at %s: %w", URL, err)
		}
	case "git":
		manifest, err = DownloadRepository(URL, tempDir, CacheDir(), themeManifest...)
		if err != nil {
			return manifest, fmt.Errorf("couldn't use the repository %s: %w", URL, err)
		}
	case "bare":
		themes, err := LoadCatalog(ConfigDir("themes"))
		if err != nil {
			return manifest, fmt.Errorf("while loading catalog of themes: %w", err)
		}
		theme, err := themes.Lookup(URL)
		if err != nil {
			return theme, err
		}

		manifest, err = Download(theme.DownloadAt, "git", theme)
		if err != nil {
			return manifest, fmt.Errorf("from catalog: %w", err)
		}
	default:
		panic("unexpected URL type")
	}
	manifest.DownloadedTo = CacheDir(manifest.Name(), manifest.currentVariantName)
	return
}

// DownloadRepository downloads the repository at URL to {{cloneTo}}/{{ffcss.yaml:name}}/{{current variant's name}}
// It first clones the repo to tempCloneTo, then loads the manifest to determine where to move it.
// the manifest can be provided in case the repository does not contain it.
func DownloadRepository(URL string, tempCloneTo string, cloneTo string, themeManifest ...Theme) (manifest Theme, err error) {
	hasManifest := len(themeManifest) >= 1
	if hasManifest {
		manifest = themeManifest[0]
	}

	err = os.MkdirAll(cloneTo, 0777)
	if err != nil {
		return manifest, fmt.Errorf("could not create directory to download to: %w", err)
	}

	err = os.MkdirAll(tempCloneTo, 0777)
	if err != nil {
		return manifest, fmt.Errorf("could not create directory to clone to: %w", err)
	}

	cloneArgs := []string{"clone", URL, tempCloneTo}
	if hasManifest && manifest.Branch != "" {
		cloneArgs = append(cloneArgs, "--branch", manifest.Branch)
	}
	LogDebug("Cloning repo...")
	process := exec.Command("git", cloneArgs...)
	//TODO print this in verbose mode: fmt.Fprintf(out, "DEBUG $ %s\n", process.String())
	output, err := process.CombinedOutput()
	if err != nil {
		return manifest, fmt.Errorf("%w: %s", err, output)
	}

	if !hasManifest {
		manifest, err = LoadManifest(filepath.Join(tempCloneTo, "ffcss.yaml"))
		if _, err := os.Stat(filepath.Join(tempCloneTo, "ffcss.yaml")); os.IsNotExist(err) {
			return manifest, fmt.Errorf("no manifest found: %w", err)
		}
		if err != nil {
			return manifest, fmt.Errorf("could not load manifest: %w", err)
		}
		if manifest.Branch != "" {
			LogDebug("switching to branch %q", manifest.Branch)
			err = switchGitBranch(manifest.Branch, tempCloneTo)
			if err != nil {
				return manifest, fmt.Errorf("while switching to branch %q: %w", manifest.Branch, err)
			}
		}
	}

	if manifest.Commit != "" {
		LogDebug("switching to commit %q", manifest.Commit)
		err = switchGitCommit(manifest.Commit, tempCloneTo)
		if err != nil {
			return manifest, fmt.Errorf("while checking out commit %q: %w", manifest.Commit, err)
		}
	}
	if manifest.Tag != "" {
		LogDebug("switching to tag %q", manifest.Tag)
		err = switchGitTag(manifest.Tag, tempCloneTo)
		if err != nil {
			return manifest, fmt.Errorf("while checking out tag %q: %w", manifest.Tag, err)
		}
	}

	if manifest.Name() == "" {
		return manifest, errors.New("manifest has no name")
	}

	err = os.MkdirAll(filepath.Dir(manifest.DownloadedTo), 0700)
	if err != nil {
		return manifest, fmt.Errorf("while creating final cache location: %w", err)
	}

	err = os.Rename(tempCloneTo, manifest.DownloadedTo)
	if err != nil {
		return manifest, fmt.Errorf("while moving from temporary downloads %q to final cache location %q: %w", tempCloneTo, manifest.DownloadedTo, err)
	}

	return
}

// DownloadFromZip downloads a ffcss manifest files along with its resources from the given URL.
// The URL must point to a zip file that contains a ffcss.yaml in its root.
// The zip file will be downloaded and extracted to {{tempDownloadTo}}, then, after loading the manifest,
// the folder will then be moved to {{finalDownloadTo}}/{{ffcss.yaml:name}}.
// the manifest can be provided in case the zip does not contain it.
func DownloadFromZip(URL string, tempDownloadTo string, finalDownloadTo string, themeManifest ...Theme) (manifest Theme, err error) {
	tempDownloadTo = tempDownloadTo + "/theme.zip"
	hasManifest := len(themeManifest) >= 1
	if hasManifest {
		manifest = themeManifest[0]
	}

	// Check if file exists, has the right Content-Type, etc.
	head, err := http.Head(URL)
	if err != nil {
		return manifest, fmt.Errorf("couldn't check remote file: %w", err)
	}
	if head.StatusCode >= 400 {
		return manifest, fmt.Errorf("couldn't check remote file: server returned %s", head.Status)
	}
	if head.Header.Get("Content-Type") != "application/zip" {
		return manifest, fmt.Errorf("expected a zip file (application/zip), got a %s", head.Header.Get("Content-Type"))
	}

	// Download it
	process := exec.Command("wget", URL, "-O", tempDownloadTo)
	LogDebug("Running %s", process.String())
	output, err := process.CombinedOutput()
	if err != nil {
		return manifest, fmt.Errorf("couldn't download zip file: %w: %s", err, output)
	}

	// Unzip it, check contents
	LogDebug("Unzipping %s to %s", tempDownloadTo, filepath.Dir(tempDownloadTo))
	unzipped, err := zip.Unzip(tempDownloadTo, filepath.Dir(tempDownloadTo))
	if err != nil {
		return manifest, fmt.Errorf("while unzipping %s: %w", tempDownloadTo, err)
	}

	if !hasManifest {
		for _, file := range unzipped {
			if filepath.Base(file) == "ffcss.yaml" {
				hasManifest = true
				manifest, err = LoadManifest(file)
				if err != nil {
					return manifest, fmt.Errorf("couldn't load the manifest file: %w", err)
				}
				break
			}
		}
		if !hasManifest {
			os.RemoveAll(filepath.Dir(tempDownloadTo))
			return manifest, errors.New("downloaded zip file has no manifest file (ffcss.yaml)")
		}
	}
	if manifest.Name() == "" {
		return manifest, errors.New("manifest has no name")
	}
	err = os.Rename(filepath.Dir(tempDownloadTo), filepath.Join(finalDownloadTo, manifest.Name()))
	if err != nil {
		return manifest, fmt.Errorf("could not move %s to %s: %w", filepath.Dir(tempDownloadTo), filepath.Join(finalDownloadTo, manifest.Name()), err)
	}
	return
}

// isURLClonable determines if the given URL points to a git repository
func isURLClonable(URL string) bool {
	output, err := exec.Command("git", "ls-remote", URL).CombinedOutput()
	if err == nil {
		return true
	}
	switch err.(type) {
	case *exec.ExitError:
		if err.(*exec.ExitError).ExitCode() == 128 {
			return false
		}
	}
	LogWarning("could not determine clonability of %s: while running git-ls-remote: %w: %s\n", URL, err, output)
	return false
}

// ReDownloadIfNeeded re-downloads the repo, actionNeeded.reDownload
// is true
func (t Theme) ReDownloadIfNeeded(actionsNeeded struct {
	switchBranch bool
	reDownload   bool
}) error {
	// FIXME for now switching branches just re-downloads the entire thing to a new dir with the new branch
	// ideal thing would be to copy from the root variant to the new variant, cd into it then `git switch` there.
	if actionsNeeded.reDownload || actionsNeeded.switchBranch {
		LogStep(0, "Downloading the variant")
		LogDebug("re-downloading: new repo is %s", t.DownloadAt)
		uri, typ, err := ResolveURL(t.DownloadAt)
		if err != nil {
			return fmt.Errorf("while resolving URL %s: %w", t.DownloadAt, err)
		}

		_, err = Download(uri, typ, t)
		if err != nil {
			return fmt.Errorf("couldn't download the variant at %s: %w", uri, err)
		}
	}
	return nil
}

// WarnIfIncompatibleWithOS shows a warning to the user if operatingSystem is marked as incompatible by the theme ("os" entry in the manifest).
func (t Theme) WarnIfIncompatibleWithOS(operatingSystem string) {
	for k, v := range t.OSNames {
		if k == operatingSystem && v == "" {
			LogWarning("This theme is marked as incompatible with %s. Things might not work.", operatingSystem)
		}
	}
}
package ffcss

import (
	"fmt"
	"os"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestResolveURL(t *testing.T) {
	name, typ, err := ResolveURL("ewen-lbh/ffcss")
	assert.NoError(t, err)
	assert.Equal(t, []string{"https://github.com/ewen-lbh/ffcss", "git"}, []string{name, typ})

	name, typ, err = ResolveURL("bitbucket.io/guaca/mole")
	assert.NoError(t, err)
	assert.Equal(t, []string{"https://bitbucket.io/guaca/mole", "website"}, []string{name, typ})

	name, typ, err = ResolveURL("http://localhost:8080/")
	assert.NoError(t, err)
	assert.Equal(t, []string{"http://localhost:8080/", "website"}, []string{name, typ})

	name, typ, err = ResolveURL("materialfox")
	assert.NoError(t, err)
	assert.Equal(t, []string{"materialfox", "bare"}, []string{name, typ})

	name, typ, err = ResolveURL("unknownone")
	assert.NoError(t, err)
	assert.Equal(t, []string{"unknownone", "bare"}, []string{name, typ})

	name, typ, err = ResolveURL("unvalid~github~username/somerepo")
	assert.NoError(t, err)
	assert.Equal(t, []string{"unvalid~github~username/somerepo", "bare"}, []string{name, typ})
}

func TestDownload(t *testing.T) {
	_, err := Download("testdata/nomanifest", "git")
	assert.Contains(t, err.Error(), "no manifest found")

	_, err = Download("http://localhost:8080/notfound", "website")
	assert.Contains(t, err.Error(), "server returned 404 File not found")

	_, err = Download("http://localhost:8080/htmlfile.html", "website")
	assert.Contains(t, err.Error(), "expected a zip file (application/zip), got")

	_, err = Download("materialfox", "bare")
	assert.NoError(t, err)

	_, err = Download("unknownone", "bare")
	assert.Contains(t, err.Error(), `theme "unknownone" not found`)
}

func TestDownloadFromZip(t *testing.T) {
	i := 0
	dl := func(s string) error {
		os.MkdirAll(filepath.Join(cwd(), fmt.Sprintf("testarea/zip-dropoff/%d", i)), 0777)
		_, err := DownloadFromZip(s, filepath.Join(cwd(), fmt.Sprintf("testarea/zip-dropoff/%d", i)), filepath.Join(cwd(), "testarea/cache"))
		i++
		return err
	}

	assert.Contains(t, dl("http://localhost:8080/notfound").Error(), "couldn't check remote file: server returned 404 File not found")
	assert.Contains(t, dl("http://localhost:8080/htmlfile.html").Error(), "expected a zip file (application/zip), got a text/html")
	// TODO: check for absence of unzipped folder
	assert.Contains(t, dl("http://localhost:8080/../themeWithNoManifest.zip").Error(), "downloaded zip file has no manifest file (ffcss.yaml)")
	// TODO: check for presence of unzipped folder
	assert.Nil(t, dl("http://localhost:8080/../materialfox.zip"))
	// TODO: check for absence of zip file, in both cases
}
package ffcss

import (
	"fmt"
	"io"
	"os"
)

const (
	// VersionMajor is the major semver component of the current version, X._._.
	// It is incremented on breaking changes. Incrementing sets others to zero.
	VersionMajor = 0
	// VersionMinor is the semver minor component of the current version, _.Y._.
	// It is incremented on feature changes. Incrementing sets VersionPatch to zero.
	VersionMinor = 2
	// VersionPatch is the semver patch component of the current version, _._.Z.
	// It is incremented on bug fixes
	VersionPatch = 0
)

var (
	// VersionString is the full version string, constructed from the component constants Version{Major,Minor,Patch}.
	VersionString = fmt.Sprintf("%d.%d.%d", VersionMajor, VersionMinor, VersionPatch)
	// out is an IO writer used to capture stdout instead of printing in unit tests
	out io.Writer = os.Stdout
	// BaseIndentLevel is the base level of indentation for LogStep and others.
	// Can be incremented when a whole section of code is run in the context of another
	// (For an example usage, see reapply in the cmd/ffcss package, that effectively runs the use command on each profile).
	BaseIndentLevel uint
)
package ffcss

import (
	"bytes"
	"fmt"
	"os"
	"os/user"
	"path/filepath"
)

var currentUser user.User
var realHomedir string
var mockedHomedir string
var mockedProfile FirefoxProfile
var mockedStdout bytes.Buffer
var testarea = filepath.Join(cwd(), "testarea")

func init() {
	usr, _ := user.Current()
	currentUser = *usr
	realHomedir, _ = os.UserHomeDir()
	mockedHomedir = os.Getenv("HOME")
	mockedProfile = NewFirefoxProfileFromPath(filepath.Join(mockedHomedir, ".mozilla", "firefox", "667ekipp.default-release"))
	out = &mockedStdout
}

// withUser Replaces %s with currentUser.Username in s
func withuser(s string) string {
	return fmt.Sprintf(s, currentUser.Username)
}
package ffcss

import (
	"fmt"
	"math"
	"os"
	"path/filepath"
	"strconv"
	"strings"
)

// FirefoxVersion represents a firefox version of the form "major.minor".
type FirefoxVersion struct {
	Major int
	Minor int // -1 means "unspecified". Can be obtained by using "x" for the minor part. Useful for constraints.
}

// FirefoxVersionConstraint represents a constraint to test on a firefox version.
type FirefoxVersionConstraint struct {
	Min FirefoxVersion
	Max FirefoxVersion
	// To be included in a Sentence like "this theme ensures compatibility with firefox <Sentence>"
	Sentence string
}

// NewFirefoxVersionConstraint creates a firefox version constraint from its string representation.
//
// Leaving out the minor version (e.g. 90 instead of 90.something) implies a trailing ".x", which means "any minor version".
//
// The following formats are supported, where X, Z are integers and Y, W are integers or the character 'x'
//
//    Format     Meaning                                    Interval
//    X.Y+       X.Y or higher                              [X.Y,  +∞]   (where +∞ = math.MaxInt32)
//    up to X.Y  X.Y but not higher                         [0.0, X.Y]
//    X.Y-Z.W    between X.Y and Z.W (including both ends)  [X.Y, Z.W]
//    X.Y        exactly X.Y                                [X.Y, X.Y]
func NewFirefoxVersionConstraint(constraint string) (FirefoxVersionConstraint, error) {
	min := FirefoxVersion{0, 0}
	max := FirefoxVersion{math.MaxInt32, math.MaxInt32}
	var sentence string
	var err error
	if strings.HasSuffix(constraint, "+") {
		LogDebug("constraint type is minimum")
		min, err = NewFirefoxVersion(strings.TrimSuffix(constraint, "+"))
		if err != nil {
			return FirefoxVersionConstraint{}, fmt.Errorf("while parsing minimum constraint %q: %w", constraint, err)
		}
		sentence = fmt.Sprintf("version %s or higher", min)
	} else if strings.Count(constraint, "-") == 1 {
		LogDebug("constraint type is range")
		minmaxStrings := strings.SplitN(constraint, "-", 2)
		min, err = NewFirefoxVersion(minmaxStrings[0])
		if err != nil {
			return FirefoxVersionConstraint{}, fmt.Errorf("while parsing lower bound of range constraint %q: %w", constraint, err)
		}
		max, err = NewFirefoxVersion(minmaxStrings[1])
		if err != nil {
			return FirefoxVersionConstraint{}, fmt.Errorf("while parsing upper bound of range constraint %q: %w", constraint, err)
		}
		if min.GreaterOrEqual(max) && !min.Equal(max) {
			return FirefoxVersionConstraint{}, fmt.Errorf("lower bound (%s) is higher than upper bound (%s)", min, max)
		}
		sentence = min.String() + "–" + max.String()
	} else if strings.HasPrefix(constraint, "up to ") {
		LogDebug("constraint type is upto")
		max, err = NewFirefoxVersion(strings.TrimPrefix(constraint, "up to "))
		if err != nil {
			return FirefoxVersionConstraint{}, fmt.Errorf("while parsing maximum constraint %q: %w", constraint, err)
		}
		sentence = fmt.Sprintf("version %s or lower", max)
	} else {
		LogDebug("constraint type is exact match")
		exact, err := NewFirefoxVersion(constraint)
		if err != nil {
			return FirefoxVersionConstraint{}, fmt.Errorf("while parsing exact match constraint %q: %w", constraint, err)
		}
		min = exact
		max = exact
		sentence = fmt.Sprintf("%s only", exact)
	}
	return FirefoxVersionConstraint{min, max, sentence}, nil
}

// GreaterOrEqual checks if the version is greater or equal to other.
// If one of the two (or both) has the minor part unspecified (".x", stored as -1),
// it only compares major parts. Otherwise, it uses a standard lexical sort.
func (ffv FirefoxVersion) GreaterOrEqual(other FirefoxVersion) bool {
	if other.Minor == -1 || ffv.Minor == -1 {
		return ffv.Major >= other.Major
	}
	return ffv.Major > other.Major || (ffv.Major == other.Major && ffv.Minor >= other.Minor)
}

// LessOrEqual checks if the version is less than or equal to other.
// If one of the two (or both) has the minor part unspecified (".x", stored as -1),
// it only compares major parts. Otherwise, it uses a standard lexical sort.
func (ffv FirefoxVersion) LessOrEqual(other FirefoxVersion) bool {
	if other.Minor == -1 || ffv.Minor == -1 {
		return ffv.Major <= other.Major
	}
	LogDebug("check that %d <= %d or %d = %d and %d <= %d", ffv.Major, other.Major, ffv.Major, other.Major, ffv.Minor, other.Minor)
	return ffv.Major < other.Major || (ffv.Major == other.Major && ffv.Minor <= other.Minor)
}

// Equal checks if the two versions are equal.
func (ffv FirefoxVersion) Equal(other FirefoxVersion) bool {
	return ffv.Major == other.Major && ffv.Minor == other.Minor
}

// String returns a string representation of the version
// If the minor part is -1, it is rendered as a 'x' character.
func (ffv FirefoxVersion) String() string {
	if ffv.Minor == -1 {
		return fmt.Sprintf("%d.x", ffv.Major)
	}
	return fmt.Sprintf("%d.%d", ffv.Major, ffv.Minor)
}

// FulfilledBy checks if version ∈ [constraint.min, constraint.max]
func (constraint FirefoxVersionConstraint) FulfilledBy(version FirefoxVersion) bool {
	LogDebug("checking if %s ∈ [%s, %s]", version, constraint.Min, constraint.Max)
	return version.GreaterOrEqual(constraint.Min) && version.LessOrEqual(constraint.Max)
}

// NewFirefoxVersion turns a version string (90 or 90.0 for example) into a FirefoxVersion.
// defaultMinor is used when parsing a dot-less version string. It defaults to "x" (meaning unspecified).
func NewFirefoxVersion(stringRepr string, defaultMinor ...string) (FirefoxVersion, error) {
	fragments := strings.Split(stringRepr, ".")
	if len(defaultMinor) == 0 {
		defaultMinor = []string{"x"}
	}
	if len(fragments) == 1 {
		fragments = append(fragments, defaultMinor...)
	}
	LogDebug("parsing version %s: fragments is %#v", stringRepr, fragments)
	major, err := strconv.ParseInt(fragments[0], 10, 64)
	if major < 0 {
		return FirefoxVersion{}, fmt.Errorf("version number cannot be negative")
	}
	if err != nil {
		return FirefoxVersion{}, fmt.Errorf("while converting major segment: %w", err)
	}
	var minor int64
	if fragments[1] == "x" {
		minor = -1
	} else {
		var err error
		minor, err = strconv.ParseInt(fragments[1], 10, 64)
		if err != nil {
			return FirefoxVersion{}, fmt.Errorf("while converting minor segment: %w", err)
		}
		if minor < 0 {
			return FirefoxVersion{}, fmt.Errorf("version number cannot be negative")
		}
	}
	LogDebug("parsed as major=%d minor=%d", major, minor)
	return FirefoxVersion{
		Major: int(major),
		Minor: int(minor),
	}, nil
}

// FirefoxVersion returns the firefox version of the profile by reading the value of
// the browser.startup.homepage_override.mstone configuration entry in the profile's prefs.js.
// This method does not work for profiles that have never been opened by the user.
func (profile FirefoxProfile) FirefoxVersion() (FirefoxVersion, error) {
	prefs, err := os.ReadFile(filepath.Join(profile.Path, "prefs.js"))
	if err != nil {
		return FirefoxVersion{}, fmt.Errorf("while reading file: %w", err)
	}

	versionString, err := ValueOfUserPrefCall(prefs, "browser.startup.homepage_override.mstone")
	if err != nil {
		return FirefoxVersion{}, fmt.Errorf("while getting value in prefs.js: %w", err)
	}

	version, err := NewFirefoxVersion(versionString)
	if err != nil {
		return FirefoxVersion{}, fmt.Errorf("while parsing version string %q: %w", versionString, err)
	}

	return version, nil
}
package ffcss

import (
	"fmt"
	"math"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestFirefoxVersionOfProfile(t *testing.T) {
	version, err := NewFirefoxProfileFromPath(filepath.Join(mockedHomedir, ".mozilla", "firefox", "667ekipp.default-release")).FirefoxVersion()
	assert.NoError(t, err)
	assert.Equal(t, FirefoxVersion{90, 0}, version)
}

func TestFirefoxVersionConstraint(t *testing.T) {
	parsingFailsWith := func(constraint string, errorPart string) {
		_, err := NewFirefoxVersionConstraint(constraint)
		assert.Contains(t, err.Error(), errorPart)
	}
	fulfillementIs := func(fulfilled bool, version FirefoxVersion, constraint string) {
		parsedConstraint, err := NewFirefoxVersionConstraint(constraint)
		assert.NoError(t, err)
		assert.Equal(t, fulfilled, parsedConstraint.FulfilledBy(version), fmt.Sprintf("testing if %s statisfies %v", version.String(), parsedConstraint))

	}

	fulfillementIs(true, FirefoxVersion{90, 0}, "90+")
	fulfillementIs(true, FirefoxVersion{90, 0}, "88-90")
	fulfillementIs(true, FirefoxVersion{90, 0}, "90")
	fulfillementIs(true, FirefoxVersion{90, 0}, "up to 90")
	fulfillementIs(false, FirefoxVersion{90, 0}, "88-89")
	fulfillementIs(true, FirefoxVersion{90, 0}, "70+")
	fulfillementIs(false, FirefoxVersion{90, 0}, "100")
	fulfillementIs(true, FirefoxVersion{90, 1}, "up to 90")
	fulfillementIs(false, FirefoxVersion{90, 1}, "up to 90.0")

	parsingFailsWith("10o", "while parsing exact match constraint")
	parsingFailsWith("-10", "while parsing lower bound of range constraint")
	parsingFailsWith("88-", "while parsing upper bound of range constraint")
	parsingFailsWith("up to me", "while parsing maximum constraint")
}

func TestNewFirefoxVersionConstraint(t *testing.T) {
	type v = FirefoxVersion
	cases := []struct {
		in       string
		min, max FirefoxVersion
	}{
		{"90+", v{90, -1}, v{math.MaxInt32, math.MaxInt32}},
		{"up to 88", v{0, 0}, v{88, -1}},
		{"88-90", v{88, -1}, v{90, -1}},
		{"up to 76.43", v{0, 0}, v{76, 43}},
		{"45", v{45, -1}, v{45, -1}},
	}

	for _, caze := range cases {
		actual, err := NewFirefoxVersionConstraint(caze.in)
		assert.NoError(t, err)
		assert.Equal(t, caze.min, actual.Min)
		assert.Equal(t, caze.max, actual.Max)
	}

	errorCases := []struct{ in, inErr string }{
		{"gouirhigerjig", "while converting major segment"},
		{"98-30", "lower bound (98.x) is higher than upper bound (30.x)"},
		{"10o", "while parsing exact match constraint"},
		{"-10", "while parsing lower bound of range constraint"},
		{"88-", "while parsing upper bound of range constraint"},
		{"up to me", "while parsing maximum constraint"},
	}

	for _, caze := range errorCases {
		_, err := NewFirefoxVersionConstraint(caze.in)
		assert.Error(t, err)
		if err != nil {
			assert.Contains(t, err.Error(), caze.inErr)
		}
	}
}

func TestNewFirefoxVersion(t *testing.T) {
	type v = FirefoxVersion
	cases := []struct {
		in, defaultMinor string
		out              FirefoxVersion
	}{
		{"1.12", "", v{1, 12}},
		{"465", "5", v{465, 5}},
		{"465.7", "5", v{465, 7}},
		{"4", "", v{4, -1}},
	}

	for _, caze := range cases {
		var actual FirefoxVersion
		var err error
		if caze.defaultMinor == "" {
			actual, err = NewFirefoxVersion(caze.in)
		} else {
			actual, err = NewFirefoxVersion(caze.in, caze.defaultMinor)
		}
		assert.NoError(t, err)
		assert.Equal(t, caze.out, actual)
	}

	errorCases := []struct{ in, inErr string }{
		{"gouirhigerjig", "while converting major segment"},
		{"98-30", "while converting major segment"},
		{"10o", ""},
		{"-10", "cannot be negative"},
	}

	for _, caze := range errorCases {
		_, err := NewFirefoxVersion(caze.in)
		assert.Error(t, err)
		if err != nil {
			assert.Contains(t, err.Error(), caze.inErr)
		}
	}
}
package ffcss

import (
	"bytes"
	"fmt"
	"os/exec"
)

// currentRepoRemote returns the git repo's origin remote URL
// if any error occurred while getting the URL, the empty string is returned.
func currentRepoRemote() string {
	var out bytes.Buffer
	command := exec.Command("git", "config", "--get", "remote.origin.url")
	command.Stdout = &out

	err := command.Run()
	if err != nil {
		LogWarning("Could not get the current git remote origin's URL. Leaving repository entry blank.\n")
		return ""
	}
	return out.String()
}

func switchGitBranch(newBranch, clonedTo string) error {
	process := exec.Command("git", "switch", newBranch)
	process.Dir = clonedTo
	output, err := process.CombinedOutput()
	if err != nil {
		return fmt.Errorf("%w: %s", err, output)
	}
	return nil
}

func switchGitCommit(commitSHA, clonedTo string) error {
	process := exec.Command("git", "checkout", commitSHA)
	process.Dir = clonedTo
	output, err := process.CombinedOutput()
	if err != nil {
		return fmt.Errorf("%w: %s", err, output)
	}
	return nil
}

func switchGitTag(tagName, clonedTo string) error {
	process := exec.Command("git", "fetch", "--all", "--tags")
	process.Dir = clonedTo
	output, err := process.CombinedOutput()
	if err != nil {
		return fmt.Errorf("%w: %s", err, output)
	}

	process = exec.Command("git", "checkout", "tags/"+tagName)
	process.Dir = clonedTo
	output, err = process.CombinedOutput()
	if err != nil {
		return fmt.Errorf("%w: %s", err, output)
	}
	return nil
}
package ffcss
module github.com/ewen-lbh/ffcss

go 1.16

require (
	github.com/AlecAivazis/survey/v2 v2.3.2
	github.com/alecthomas/chroma v0.10.0
	github.com/bmatcuk/doublestar v1.3.4
	github.com/charmbracelet/glamour v0.5.0
	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
	github.com/evilsocket/islazy v1.10.6
	github.com/hbollon/go-edlib v1.6.0
	github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69
	github.com/mattn/go-colorable v0.1.12 // indirect
	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
	github.com/microcosm-cc/bluemonday v1.0.18 // indirect
	github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db
	github.com/muesli/termenv v0.11.0 // indirect
	github.com/stretchr/testify v1.7.0
	github.com/yuin/goldmark v1.4.10 // indirect
	golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
	golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 // indirect
	golang.org/x/text v0.3.7
	gopkg.in/yaml.v2 v2.4.0
)
github.com/AlecAivazis/survey/v2 v2.3.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO4gCnU8=
github.com/AlecAivazis/survey/v2 v2.3.2/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g=
github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/evilsocket/islazy v1.10.6 h1:MFq000a1ByoumoJWlytqg0qon0KlBeUfPsDjY0hK0bo=
github.com/evilsocket/islazy v1.10.6/go.mod h1:OrwQGYg3DuZvXUfmH+KIZDjwTCbrjy48T24TUpGqVVw=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/hbollon/go-edlib v1.6.0 h1:ga7AwwVIvP8mHm9GsPueC0d71cfRU/52hmPJ7Tprv4E=
github.com/hbollon/go-edlib v1.6.0/go.mod h1:wnt6o6EIVEzUfgbUZY7BerzQ2uvzp354qmS2xaLkrhM=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69 h1:umaj0TCQ9lWUUKy2DxAhEzPbwd0jnxiw1EI2z3FiILM=
github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69/go.mod h1:zdLK9ilQRSMjSeLKoZ4BqUfBT7jswTGF8zRlKEsiRXA=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/microcosm-cc/bluemonday v1.0.18 h1:6HcxvXDAi3ARt3slx6nTesbvorIc3QeTzBNRvWktHBo=
github.com/microcosm-cc/bluemonday v1.0.18/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
github.com/muesli/termenv v0.11.0 h1:fwNUbu2mfWlgicwG7qYzs06aOI8Z/zKPAv8J4uKbT+o=
github.com/muesli/termenv v0.11.0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
github.com/yuin/goldmark v1.4.10 h1:+WgKGo8CQrlMTRJpGCFCyNddOhW801TKC2QijVV9QVg=
github.com/yuin/goldmark v1.4.10/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc=
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
package ffcss

import (
	"fmt"
	"os/exec"

	"github.com/hoisie/mustache"
)

// runHook runs a provided command for a specific profile. See any of the (Manifest).Run*Hook methods
// for a list of available {{mustache}} placeholders.
func (t Theme) runHook(commandline string, profile FirefoxProfile) (output string, err error) {
	ffversion, err := profile.FirefoxVersion()
	if err != nil {
		return "", fmt.Errorf("while getting firefox version for current profile: %w", err)
	}

	command := exec.Command("bash", "-c", mustache.Render(commandline, map[string]interface{}{
		"profile_path":    profile.Path,
		"firefox_version": ffversion.String(),
	}))

	outputBytes, err := command.CombinedOutput()
	output = string(outputBytes)
	if err != nil {
		return "", fmt.Errorf("while running %q: %s: %w", command.String(), output, err)
	}

	return
}

// RunPreInstallHook passes the pre-install hook specified in the manifest's run.before entry to bash.
// Several {{mustache}} placeholders are available:
//
//	profile_path        The current profile's path
//	firefox_version     The current profile's Firefox version
func (t Theme) RunPreInstallHook(profile FirefoxProfile) (output string, err error) {
	return t.runHook(t.Run.Before, profile)
}

// RunPostInstallHook does the same as RunPreInstallHook but for the manifest's run.after entry.
func (t Theme) RunPostInstallHook(profile FirefoxProfile) (output string, err error) {
	return t.runHook(t.Run.After, profile)
}
package ffcss
package ffcss

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
)

// InstallAssets installs the assets in the specified profile directory
func (t Theme) InstallAssets(operatingSystem string, variant Variant, profileDir string) (err error) {
	files, err := t.AssetsPaths(operatingSystem, variant)
	if err != nil {
		return fmt.Errorf("while gathering assets: %w", err)
	}
	LogDebug("gathered %d asset(s)", len(files))

	for _, file := range files {
		stat, err := os.Stat(file)
		if err != nil {
			return fmt.Errorf("couldn't check file %s: %w", file, err)
		}

		if stat.IsDir() {
			continue
		}

		content, err := ioutil.ReadFile(file)
		if err != nil {
			return fmt.Errorf("while reading %s: %w", file, err)
		}

		destPath, err := t.DestinationPathOfAsset(file, profileDir, operatingSystem, variant)
		if err != nil {
			println(err.Error())
			continue
		}

		err = os.MkdirAll(filepath.Dir(destPath), 0700)
		if err != nil {
			return fmt.Errorf("couldn't create parent directories for %s: %w", destPath, err)
		}

		err = ioutil.WriteFile(destPath, content, 0700)
		if err != nil {
			return fmt.Errorf("while writing to %s: %w", destPath, err)
		}
		LogDebug("wrote %s", destPath)

	}
	return nil
}

// InstallUserJS installs the content of user.js and the config entries to {{profileDir}}/user.js
func (t Theme) InstallUserJS(operatingSystem string, variant Variant, profileDir string) error {
	var content []byte
	var err error

	if t.UserJS != "" {
		file := filepath.Join(t.DownloadedTo, renderFileTemplate(t.UserJS, operatingSystem, variant, t.OSNames))
		content, err = ioutil.ReadFile(file)
		if err != nil {
			return fmt.Errorf("while reading %s: %w", file, err)
		}

	} else {
		content = []byte{}
	}

	additionalContent, err := t.UserJSFileContent()
	if err != nil {
		return fmt.Errorf("while translating config entries to javascript: %w", err)
	}

	if additionalContent != "" {
		content = []byte(string(content) + "\n" + additionalContent)
		LogDebug("generated additional user.js content from config entries: %q", additionalContent)
	}

	if string(content) == "" {
		return nil
	}

	err = ioutil.WriteFile(filepath.Join(profileDir, "user.js"), content, 0700)
	if err != nil {
		return fmt.Errorf("while writing: %w", err)
	}

	LogDebug("installed user.js @ %s", filepath.Join(profileDir, "user.js"))

	return nil
}

// InstallUserChrome writes the content of userChrome to {{profileDir}}/chrome/userChrome.css
func (t Theme) InstallUserChrome(os string, variant Variant, profileDir string) error {
	if t.UserChrome == "" {
		return nil
	}
	file := filepath.Join(t.DownloadedTo, renderFileTemplate(t.UserChrome, os, variant, t.OSNames))
	content, err := ioutil.ReadFile(file)
	if err != nil {
		return fmt.Errorf("while reading %s: %w", file, err)
	}

	err = ioutil.WriteFile(filepath.Join(profileDir, "chrome", "userChrome.css"), content, 0700)
	if err != nil {
		return fmt.Errorf("while writing: %w", err)
	}

	LogDebug("installed userChrome.css @ %s", filepath.Join(profileDir, "chrome", "userChrome.css"))

	return nil
}

// InstallUserContent writes the content of userContent to {{profileDir}}/chrome/userContent.css
func (t Theme) InstallUserContent(os string, variant Variant, profileDir string) error {
	if t.UserContent == "" {
		return nil
	}
	file := filepath.Join(t.DownloadedTo, renderFileTemplate(t.UserContent, os, variant, t.OSNames))
	content, err := ioutil.ReadFile(file)
	if err != nil {
		return fmt.Errorf("while reading %s: %w", file, err)
	}

	err = ioutil.WriteFile(filepath.Join(profileDir, "chrome", "userContent.css"), content, 0700)
	if err != nil {
		return fmt.Errorf("while writing: %w", err)
	}

	LogDebug("installed userContent.css @ %s", filepath.Join(profileDir, "chrome", "userContent.css"))

	return nil
}
package ffcss
package ffcss

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	"github.com/charmbracelet/glamour"
	"gopkg.in/yaml.v2"
)

// ThemeCompatWarningShown controls whether the ffcss incompatibility between the installed version and the theme's declared version (ffcss entry in the manifest)
// is warned to the user.
// Set to VersionMajor > 0 to comply with semver: versions with the major component (X._._) set to 0 can have breaking changes at any version change.
var ThemeCompatWarningShown = VersionMajor > 0

// Config represents a configuration map in a manifest, representing a set of values for the about:config page in Firefox
type Config map[string]interface{}

// Equal returns true if the config c has all of its values equal to the other Config.
func (c Config) Equal(other Config) bool {
	// XXX: this is not efficient AT ALL. It sucks. Change this.
	for k, v := range c {
		if v != other[k] {
			return false
		}
	}
	for k, v := range other {
		if other[k] != v {
			return false
		}
	}
	return true
}

// FileTemplate represents a string with placeholders (e.g. "{{os}}") to be replaced by {{mustache}}.
type FileTemplate = string

// Variant represents a theme's variant. Most of the properties are identical to Theme's, because they overwrite the default values.
type Variant struct {
	// Properties exclusive to variants
	Name string

	// Properties that modify the "default variant"
	DownloadAt  string `yaml:"download"`
	Branch      string
	Commit      string
	Tag         string
	Config      Config
	UserChrome  FileTemplate `yaml:"userChrome"`
	UserContent FileTemplate `yaml:"userContent"`
	UserJS      FileTemplate `yaml:"user.js"`
	Assets      []FileTemplate
	Description string
	Addons      []string
	Run         struct {
		Before string
		After  string
	}
	Message string
}

// Theme represents a FirefoxCSS theme, read from a manifest YAML file. (See LoadManifest).
type Theme struct {
	// Internal, cannot be set in the YAML file
	currentVariantName string `yaml:"-"` // Used to construct the directory where the theme will be cached
	raw                string `yaml:"-"` // Contains the raw yaml file contents
	DownloadedTo       string `yaml:"-"` // Stores the path to the directory where the theme is cached. Set by .Download().

	// Top-level (non-variant-modifiable)
	FfcssVersion             int                      `yaml:"ffcss"`
	FirefoxVersion           string                   `yaml:"firefox,omitempty"`
	FirefoxVersionConstraint FirefoxVersionConstraint `yaml:"-"`
	ExplicitName             string                   `yaml:"name"`
	Author                   string                   `yaml:"by"`
	Description              string
	Variants                 map[string]Variant
	OSNames                  map[string]string `yaml:"os,omitempty"`

	// Override-able by variants
	DownloadAt  string `yaml:"download"`
	Branch      string
	Commit      string `yaml:",omitempty"`
	Tag         string `yaml:",omitempty"`
	Config      Config
	UserChrome  FileTemplate `yaml:"userChrome"`
	UserContent FileTemplate `yaml:"userContent"`
	UserJS      FileTemplate `yaml:"user.js"`
	Assets      []FileTemplate
	CopyFrom    string `yaml:"copy from,omitempty"`
	Addons      []string
	Run         struct {
		Before string
		After  string
	}
	Message string
}

// ManifestKeyGroupsStarts specifies at which keys a group of related keys starts.
// This assumes that the YAML keys are displayed/written in the order they are defined in (see Theme).
//
// It is used to add blank lines in generated manifests: for example, above the 'variant' key, a blank line should be added to add grouping.
var ManifestKeyGroupsStarts = [...]string{"name", "variants", "os", "download", "config", "run", "message"}

// NewTheme creates a new Theme with vital defaults (namely the config entry to enable CSS customization of Firefox).
func NewTheme() Theme {
	return Theme{
		Config: Config{
			"toolkit.legacyUserProfileCustomizations.stylesheets": true,
		},
		Variants: map[string]Variant{},
		Assets:   []FileTemplate{},
	}
}

// LoadManifest loads a ffcss.yaml file into a Theme object.
func LoadManifest(manifestPath string) (manifest Theme, err error) {
	raw, err := os.ReadFile(manifestPath)
	if err != nil {
		err = fmt.Errorf("while reading manifest %s: %w", manifestPath, err)
		return
	}
	manifest = NewTheme()
	manifest.raw = string(raw)
	err = yaml.Unmarshal(raw, &manifest)

	if manifest.FfcssVersion < 0 {
		err = fmt.Errorf("ffcss version cannot be negative but is set to %d", manifest.FfcssVersion)
		return
	}

	if manifest.FfcssVersion != VersionMajor && !ThemeCompatWarningShown && manifest.FfcssVersion != 0 {
		LogWarning("ffcss %s is installed, but you are using a theme made for ffcss %d.X.X. Some things may not work.\n", VersionString, manifest.FfcssVersion)
		ThemeCompatWarningShown = true
	}

	if manifest.Name() == TempDownloadsDirName {
		err = fmt.Errorf("invalid theme name %q", TempDownloadsDirName)
		return
	}

	if manifest.Name() == "" {
		err = fmt.Errorf("theme has no name")
		return
	}

	for key := range manifest.OSNames {
		if key != "linux" && key != "macos" && key != "windows" {
			return Theme{}, fmt.Errorf("%s is not a valid os replacement target. Targets are macos, windows and linux", key)
		}
	}

	for name, variant := range manifest.Variants {
		if name == RootVariantName {
			err = fmt.Errorf("invalid variant name %q", name)
			return
		}
		variantWithName := variant
		variantWithName.Name = name
		manifest.Variants[name] = variantWithName
	}
	manifest.currentVariantName = RootVariantName // ensure the current variant's name wasn't manipulated by the YAML unmarshaling
	if err != nil {
		err = fmt.Errorf("while parsing manifest %s: %w", manifestPath, err)
		return
	}
	manifest.DownloadedTo = CacheDir(manifest.Name(), manifest.currentVariantName)
	if manifest.FirefoxVersion != "" {
		manifest.FirefoxVersionConstraint, err = NewFirefoxVersionConstraint(manifest.FirefoxVersion)
		if err != nil {
			err = fmt.Errorf("invalid Firefox version constraint %q: %w", manifest.FirefoxVersion, err)
			return
		}

	}
	return
}

// WithVariant returns a Manifest representing the theme if the selected variant
// was used as the "root values".
// i.e. the values of UserJS, UserContent, UserChrome, Assets are replaced with their variant's, if set,
// and the value of Config is combined with the variant's.
// Some variants change the git branch, the entire repository or other settings that require external actions.
// Those are returned in actionsNeeded as a struct of booleans with descriptive field names.
func (t Theme) WithVariant(variant Variant) (newTheme Theme, actionsNeeded struct{ switchBranch, reDownload bool }) {
	// TODO might clean this up with reflection, selecting fields that are both in Manifest & Variant
	newTheme = t
	newTheme.currentVariantName = variant.Name
	if variant.UserChrome != "" {
		newTheme.UserChrome = variant.UserChrome
	}
	if variant.UserContent != "" {
		newTheme.UserContent = variant.UserContent
	}
	if variant.UserJS != "" {
		newTheme.UserJS = variant.UserJS
	}
	if variant.Message != "" {
		newTheme.Message = variant.Message
	}
	if len(variant.Assets) > 0 {
		newTheme.Assets = variant.Assets
	}
	if variant.DownloadAt != "" {
		actionsNeeded.reDownload = true
		newTheme.DownloadAt = variant.DownloadAt
	}
	if variant.Branch != "" {
		actionsNeeded.switchBranch = true
		newTheme.Branch = variant.Branch
	}
	if variant.Commit != "" {
		newTheme.Commit = variant.Commit
	}
	if variant.Tag != "" {
		newTheme.Tag = variant.Tag
	}
	if variant.Run.Before != "" {
		newTheme.Run.Before = variant.Run.Before
	}
	if variant.Run.After != "" {
		newTheme.Run.After = variant.Run.After
	}
	for key, val := range variant.Config {
		newTheme.Config[key] = val
	}
	if actionsNeeded.reDownload || actionsNeeded.switchBranch {
		newTheme.DownloadedTo = CacheDir(newTheme.Name(), newTheme.currentVariantName)
	}
	return newTheme, actionsNeeded
}

// Name returns a theme's name. If the name was explicitly set in the manifest (i.e. if t.ExplicitName is not empty), it is returned.
// Otherwise, the name is guessed.
// Currently, it is only guessed if a github repository is set for t.DownloadAt.
// If guessing is not possible, it returns the empty string.
func (t Theme) Name() string {
	if t.ExplicitName != "" {
		return strings.ToLower(t.ExplicitName)
	}
	if strings.HasPrefix(t.DownloadAt, "https://github.com") {
		fragments := strings.Split(t.DownloadAt, "/")
		return strings.ToLower(fragments[len(fragments)-1])
	}
	return ""
}

// AvailableVariants lists the possible variant names to choose from
func (t Theme) AvailableVariants() []string {
	names := make([]string, 0, len(t.Variants))
	for name := range t.Variants {
		names = append(names, name)
	}
	return names
}

// ShowMessage renders the message and prints it to the user
func (t Theme) ShowMessage() error {
	scheme := os.Getenv("COLORSCHEME")
	if scheme != "light" && scheme != "dark" {
		// TODO: detect with the terminal's current background color as a fallback
		scheme = "dark"
	}
	rendered, err := glamour.Render(t.Message, scheme)
	if err != nil {
		return fmt.Errorf("while rendering message: %w", err)
	}

	if strings.TrimSpace(rendered) != "" {
		fmt.Fprintln(out, rendered)
	}
	return nil
}

// ManifestPath returns the path of a theme's manifest file
func ManifestPath(themeRoot string) string {
	return filepath.Join(themeRoot, "ffcss.yaml")
}

// GenerateManifest returns the YAML contents of the manifest corresponding to the given theme.
// If t.Raw is set, it'll return it.
// Otherwise, it serializes the values into YAML, following the Theme struct.
func (t Theme) GenerateManifest() (string, error) {
	if t.raw != "" {
		return t.raw, nil
	}
	t.ExplicitName = t.Name()
	// Remove redundant keys
	if t.Config.Equal(NewTheme().Config) {
		t.Config = Config{}
	}
	contentBytes, err := yaml.Marshal(t)
	if err != nil {
		return "", err
	}

	content := string(contentBytes)

	for _, key := range ManifestKeyGroupsStarts {
		content = strings.Replace(content, key+": ", "\n"+key+": ", 1)
		content = strings.Replace(content, key+":\n", "\n"+key+":\n", 1)
	}

	return content, nil
}

// WriteManifest writes the contents of t as a YAML file named ffcss.yaml
// inside inDirectory.
// It adds a comment mentioning the documentation at the top of the file.
// See GenerateManifest to see how the contents of the file are generated.
func (t Theme) WriteManifest(inDirectory string) error {
	content, err := t.GenerateManifest()
	if err != nil {
		return fmt.Errorf("while generating manifest contents for %s: %w", t.Name(), err)
	}

	content = "# This is a manifest for a FirefoxCSS theme. \n# See https://github.com/ewen-lbh/ffcss for more information.\n" + content

	err = ioutil.WriteFile(filepath.Join(inDirectory, "ffcss.yaml"), []byte(content), 0700)
	if err != nil {
		return fmt.Errorf("while writing the manifest: %w", err)
	}

	return nil
}

// InitializeTheme returns a new, blank theme, but with some values guessed from the current context.
// Meant to be used by "ffcss init".
func InitializeTheme(workingDir string) (Theme, error) {
	theme := NewTheme()

	theme.DownloadAt = strings.TrimSuffix(currentRepoRemote(), ".git")
	if theme.DownloadAt == "" {
		theme.DownloadAt = "TODO"
	}

	if !strings.Contains(theme.DownloadAt, "https://github.com") {
		theme.ExplicitName = filepath.Base(workingDir)
	}

	return theme, nil
}
package ffcss

import (
	"math"
	"os"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestNewTheme(t *testing.T) {
	assert.Equal(t, Theme{
		Config: Config{
			"toolkit.legacyUserProfileCustomizations.stylesheets": true,
		},
		Variants: map[string]Variant{},
		Assets:   []string{},
	}, NewTheme())
}

func TestLoadManifest(t *testing.T) {
	errorCases := []struct{ manifestName, errorPart string }{
		{"ffcss_version_negative", "ffcss version cannot be negative"},
		{"invalid_firefox_constraint", "invalid Firefox version constraint"},
		{"no_name", "no name"},
		{"temp_download_dir_as_name", "invalid theme name \"" + TempDownloadsDirName + "\""},
		{"temp_download_dir_as_name_via_github_remote", "invalid theme name \"" + TempDownloadsDirName + "\""},
		{"root_variant_name", "invalid variant name \"" + RootVariantName + "\""},
		{"unknown_os_key", "hannah montana is not a valid os replacement target. Targets are macos, windows and linux"},
		{"wrong_casing_os_key", "MacOS is not a valid os replacement target. Targets are macos, windows and linux"},
	}

	// TODO when out of 0.x.x, test for warning appearing when ffcss version incompatible (and appearing only _once_)
	// {"ffcss_version_99999"

	for _, caze := range errorCases {
		_, err := LoadManifest(filepath.Join(testarea, "manifests", caze.manifestName+".yaml"))
		assert.Error(t, err, "case %s needs to contain %q in error message", caze.manifestName, caze.errorPart)
		if err != nil {
			assert.Contains(t, err.Error(), caze.errorPart)
		}
	}

	actual, err := LoadManifest(filepath.Join(testarea, "manifests", "fine.yaml"))
	fileContents, _ := os.ReadFile(filepath.Join(testarea, "manifests", "fine.yaml"))
	assert.NoError(t, err)
	assert.Equal(t, Theme{
		currentVariantName:       RootVariantName,
		raw:                      string(fileContents),
		DownloadedTo:             filepath.Join(mockedHomedir, ".cache", "ffcss", "a fine theme", RootVariantName),
		FfcssVersion:             0,
		FirefoxVersion:           "89+",
		FirefoxVersionConstraint: FirefoxVersionConstraint{Min: FirefoxVersion{89, -1}, Max: FirefoxVersion{math.MaxInt32, math.MaxInt32}, Sentence: "version 89.x or higher"},
		ExplicitName:             "a fine theme",
		Author:                   "some nice person",
		Description:              "Lorem ipsum _dolor_ sit am**et**\n",
		Variants: map[string]Variant{
			"default": {
				Name: "default",
			},
			"new moon": {
				Name:       "new moon",
				DownloadAt: "https://example.com/new-moon",
			},
		},
		OSNames: map[string]string{
			"linux": "GNU+Linux",
		},
		DownloadAt: "https://example.com/.git",
		Branch:     "sun",
		Commit:     "85dfe1ac",
		Tag:        "v0.184.668",
		Config: Config{
			"legacy.some-config-entry":                            "yeees",
			"toolkit.legacyUserProfileCustomizations.stylesheets": true,
			"zincoxide": true,
		},
		UserChrome:  "userChrome.sass",
		UserContent: "./{{os}}/userContent--{{variant}}.css",
		UserJS:      "user.ls",
		Assets: []string{
			"chrome/**",
			"logos/*.svg",
		},
		CopyFrom: "chromeee/",
		Addons: []string{
			"https://example.com/extensions/a",
			"https://example.com/extensions/b",
		},
		Run: struct {
			Before string
			After  string
		}{
			Before: "cd /; tree; echo you have been hacked",
			After:  "echo hacking complete 😎",
		},
		Message: "Here's a choccy milk :) <https://i.redd.it/sh9re7861t851.png>\n",
	}, actual)
}
package ffcss

import (
	"fmt"
	"os"
	"os/user"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
)

// FirefoxProfile represents a Firefox Profile stored at Path. Each Firefox Profile is a separate instance of Firefox settings and other user data.
// See https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data for more information.
type FirefoxProfile struct {
	ID   string
	Name string
	Path string
}

type firefoxProfileWithVersion = struct {
	Profile FirefoxProfile
	Version FirefoxVersion
}

// FullName returns the basename of ffp.Path
func (ffp FirefoxProfile) FullName() string {
	return filepath.Base(ffp.Path)
}

// String returns a string representation of the profile.
func (ffp FirefoxProfile) String() string {
	return fmt.Sprintf("%s (%s)", ffp.Name, ffp.ID)
}

// NewFirefoxProfileFromPath returns a FirefoxProfile by parsing the path into and ID and a Name.
func NewFirefoxProfileFromPath(path string) FirefoxProfile {
	base := filepath.Base(path)
	parts := strings.Split(base, ".")
	return FirefoxProfile{
		Path: path,
		ID:   parts[0],
		Name: parts[1],
	}
}

// NewFirefoxProfileFromDisplay returns a FirefoxProfile by matching the given displayString against profiles'.
// See (FirefoxProfile).Display for the definition of a display string.
func NewFirefoxProfileFromDisplay(displayString string, profiles []FirefoxProfile) FirefoxProfile {
	for _, profile := range profiles {
		ffp := NewFirefoxProfileFromPath(profile.Path)
		if ffp.Display() == displayString {
			return ffp
		}
	}
	LogDebug("while searching for %s in %v", displayString, profiles)
	panic("internal error: can't get profile from display string")
}

// ProfilePaths returns an array of profile directories from the profile folder.
// 1 arguments: the profiles folder is assumed to be the current OS's default.
// 2 argument: use the given profiles folder
// more arguments: panic.
func ProfilePaths(operatingSystem string, optionalProfilesDir ...string) ([]string, error) {
	var profilesFolder string
	if len(optionalProfilesDir) == 0 {
		// XXX: Weird golang thing, if I assign to profilesFolder directly, it tells me the variable is unused
		_profilesFolder, err := DefaultProfilesDir(operatingSystem)
		profilesFolder = _profilesFolder
		if err != nil {
			return []string{}, fmt.Errorf("couldn't get the profiles folder: %w. Try to use --profiles-dir", err)
		}
	} else if len(optionalProfilesDir) == 1 {
		profilesFolder = optionalProfilesDir[0]
	} else {
		panic(fmt.Sprintf("received %d arguments, expected 1 or 2", len(optionalProfilesDir)+1))
	}
	directories, err := os.ReadDir(profilesFolder)
	releasesPaths := make([]string, 0)
	patternReleaseID := regexp.MustCompile(`[a-z0-9]{8}\.\w+`)
	if err != nil {
		return []string{}, fmt.Errorf("couldn't read %s: %w", profilesFolder, err)
	}
	for _, releasePath := range directories {
		if patternReleaseID.MatchString(releasePath.Name()) {
			stat, err := os.Stat(filepath.Join(profilesFolder, releasePath.Name()))
			if err != nil {
				continue
			}
			if stat.IsDir() {
				releasesPaths = append(releasesPaths, filepath.Join(profilesFolder, releasePath.Name()))
			}
		}
	}
	return releasesPaths, nil
}

// Profiles returns a list of FirefoxProfiles. If optionalProfilesDir is "",
// DefaultProfilesDir() is used to get the profiles' directory.
func Profiles(optionalProfilesDir string) ([]FirefoxProfile, error) {
	var profiles []FirefoxProfile
	var err error
	var profilePaths []string

	if optionalProfilesDir != "" {
		profilePaths, err = ProfilePaths(GOOStoOS(runtime.GOOS), optionalProfilesDir)
	} else {
		profilePaths, err = ProfilePaths(GOOStoOS(runtime.GOOS))
	}

	if err != nil {
		return profiles, fmt.Errorf("while getting profile paths: %w", err)
	}

	for _, profilePath := range profilePaths {
		profiles = append(profiles, NewFirefoxProfileFromPath(profilePath))
	}

	return profiles, nil
}

// DefaultProfilesDir returns the operating-system-dependent default location for the Firefox profiles' directories.
func DefaultProfilesDir(operatingSystem string) (string, error) {
	switch operatingSystem {
	case "linux":
		homedir, err := os.UserHomeDir()
		if err != nil {
			return "", err
		}
		return filepath.Join(homedir, ".mozilla", "firefox"), nil
	case "macos":
		user, err := user.Current()
		if err != nil {
			return "", fmt.Errorf("couldn't get the current user: %w", err)
		}

		return filepath.Join("/Users", user.Username, "Library", "Application Support", "Firefox", "Profiles"), nil
	case "windows":
		homedir, err := os.UserHomeDir()
		if err != nil {
			return "", err
		}

		return filepath.Join(homedir, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles"), nil
	}
	return "", fmt.Errorf("unknown operating system %s", operatingSystem)
}

// IncompatibleProfiles returns the list of profiles that don't meet the Firefox version constraint specified by the theme, along with the detect Firefox version of each profile.
func (t Theme) IncompatibleProfiles(profiles []FirefoxProfile) ([]firefoxProfileWithVersion, error) {
	if t.FirefoxVersion != "" {
		incompatibleProfileDirs := make([]firefoxProfileWithVersion, 0)
		for _, profile := range profiles {
			profileVersion, err := profile.FirefoxVersion()
			if err != nil {
				LogWarning("Couldn't get firefox version for profile %s", profile)
			}
			fulfillsConstraint := t.FirefoxVersionConstraint.FulfilledBy(profileVersion)
			if !fulfillsConstraint {
				incompatibleProfileDirs = append(incompatibleProfileDirs, firefoxProfileWithVersion{profile, profileVersion})
			}
		}
		return incompatibleProfileDirs, nil
	}
	return []firefoxProfileWithVersion{}, nil
}

// BackupChrome moves the chrome/ folder to chrome.bak/
func (ffp FirefoxProfile) BackupChrome() error {
	return renameIfExists(filepath.Join(ffp.Path, "chrome"), filepath.Join(ffp.Path, "chrome.bak"))
}

// BackupUserJS moves user.js to user.js.bak if user.js exists
func (ffp FirefoxProfile) BackupUserJS() error {
	return renameIfExists(filepath.Join(ffp.Path, "user.js"), filepath.Join(ffp.Path, "user.js.bak"))
}
package ffcss
ffcss: 0

name: compactmode
download: https://github.com/Godiesc/compactmodefirefoxcss

user.js: user.js
userChrome: chrome/userChrome.css

addons:
  - https://addons.mozilla.org/en-US/firefox/addon/beautifultotaldark/

variants:
  default: {}
  photon-tabs:
    user.js: OtherCss/user.js
    userChrome: OtherCss/chrome/userChrome.css
ffcss: 0

name: lepton
download: https://github.com/black7375/Firefox-UI-Fix

user.js: user.js
userChrome: userChrome.css
userContent: userContent.css

asssets:
  - icons/**

variants:
  default: {}
  photon-style:
    branch: photon-style
../../../themes/alpenblue.yaml
../../../themes/flyingfox.yaml
../../../themes/montereyfox.yaml
ffcss: 0
name: no extension
ffcss: 0
name: ugly 8.3 filename, like we're still on windows 2.1 or whatever
---
name: Bug report
about: Create a report to help us improve the theme
title: "[Feature/element] broken when [doing something]"
labels: bug
assignees: ''

---

**Sanity checks (must complete)**
- [ ] I have read and followed the installation instructions in the README
- [ ] I have not modified the userChrome.css file
- [ ] I have tested the latest release for my Firefox version, or commit on master/beta branch (beta is for Firefox Beta only)

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Enable '...'
2. Go to '....'
3. Click on '....'
4. See issue

**Screenshots**
Add screenshots to help explain your problem.

**System info**
 - OS: [e.g. Windows 10]
 - Firefox version: [e.g. 75]

**Additional context**
Add any other context about the problem here.
# macOS
# General
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon


# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

# Windows
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db

# Dump file
*.stackdump

# Folder config file
[Dd]esktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp

# Windows shortcuts
*.lnk

# Linux
*~

# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*

# KDE directory preferences
.directory

# Linux trash folder which might appear on any partition or disk
.Trash-*

# .nfs files are created when an open file is removed but is still being accessed
.nfs*

# JetBrains
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn.  Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client
.idea/httpRequests

# VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
MIT License

Copyright (c) 2018 muckSponge

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# MaterialFox
*A Material Design-inspired userChrome.css theme for Firefox*

![Preview](https://user-images.githubusercontent.com/5405629/45172944-21d91900-b24a-11e8-8bc5-03814121b0de.png)
This theme is powered by blood, sweat, and coffee. If you like it, please consider helping out to support its continued development.

[![Buy me a coffee](https://svgshare.com/i/8Yd.svg)](https://www.buymeacoffee.com/n4ho5QX2l)

## What this does
Inspired by Google's Material Design and their latest Google Chrome UI, this theme turns your Firefox into a Material-styled web browser. The aim was to style the browser as closely as possible to Google Chrome, where practical.

This is a userChrome.css theme, which means you must manually add it to your Firefox profile. The theme overrides certain browser styles. Currently, only the main UI is affected (settings pages, etc. are not). More elements of the UI may be styled in the future but a broader scope becomes harder to maintain as Mozilla updates their browser code so some UI styles may be culled or redone if they become unmaintainable.

## What version do I use?
Check the [releases](https://github.com/muckSponge/MaterialFox/releases) section. Each release version will match the compatible Firefox version. For example, if you're using Firefox 88, try a v88.x release. If there's no matching release and you're not using an old version of Firefox, go for the latest one. If you're using a beta version of Firefox, you might want to try the master branch, which will have the latest bug fixes.

## Installation
1. Copy the chrome folder and user.js file into your Firefox profile directory. To find your profile directory, go to about:support or about:profiles.
2. See [Recommended instructions](#recommended-instructions) if you'd prefer a more Chrome-like experience.
3. Restart Firefox.

### Recommended instructions
Add space above tab bar:
* Right click on toolbar -> Customize.
* Check Drag Space checkbox.

Replicate Chrome behaviour for clipped tabs:
* [about:config] Set ```browser.tabs.tabClipWidth``` to ```83``` (default is ```140```).

Replicate Chrome's "Not Secure" text on HTTP:
* [about:config] Set ```security.insecure_connection_text.enabled``` to ```true```.

## Please note
* Linux is no longer officially supported but you can give it a try – if you'd like to work on it feel free to make a PR.
* Some customisation settings may no longer work (such as compact/touch density).
* Some custom themes may clash with the address bar.
* Some themes using transparency might not work.
.findbar-textbox
{
	background-color: transparent !important;
	background-image: url(../icons/search.svg) !important;
	background-repeat: no-repeat !important;
	background-position: 5px center !important;
	fill-opacity: 1 !important;
	border: 0 !important;
	box-shadow: none !important;
	margin-inline-start: 2px !important;
	padding-inline-start: 28px !important;
}

hbox[anonid="findbar-textbox-wrapper"]
{
	background-color: var(--toolbar-field-background-color) !important;
	border-radius: 16px !important;
	position: relative !important;
	display: flex !important;
	padding: 2px !important;
	height: 28px !important;
	transition: background-color .1s var(--ease-basic) !important;
}

hbox[anonid="findbar-textbox-wrapper"]:hover
{
	background-color: var(--toolbar-field-hover-background-color) !important;
}

hbox[anonid="findbar-textbox-wrapper"]:focus-within
{
	background-color: var(--toolbar-field-focus-background-color) !important;
	/* won't work in a var... */
	border: 2px solid -moz-accent-color !important;
	margin: -1px !important;
	padding: 1px !important;
	height: 30px !important;
}

hbox[anonid="findbar-textbox-wrapper"] toolbarbutton
{
	border-radius: 99px !important;
	border: 0 !important;
	width: 24px !important;
	height: 24px !important;
	padding: 0 !important;
	display: flex !important;
	background: 0 !important;
	transition: background-color .2s var(--ease-basic) !important;
	margin: 0 !important;
	box-shadow: none !important;
}

hbox[anonid="findbar-textbox-wrapper"] toolbarbutton[disabled]
{
	display: none !important;
}

hbox[anonid="findbar-textbox-wrapper"] toolbarbutton .toolbarbutton-text
{
	visibility: collapse !important;
}

hbox[anonid="findbar-textbox-wrapper"] toolbarbutton:not([disabled]):hover
{
	background-color: var(--toolbarbutton-hover-background) !important;
}

hbox[anonid="findbar-textbox-wrapper"] toolbarbutton:not([disabled]):hover:active
{
	background-color: var(--toolbarbutton-active-background) !important;
	transition-duration: 0 !important;
}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<circle fill="context-fill" fill-opacity="context-fill-opacity" cx="8" cy="8" r="8"/>
</svg>
@media (-moz-windows-compositor)
{
	@media not (-moz-os-version: windows-win7)
	{
		@media not (-moz-os-version: windows-win8)
		{
			@media (-moz-windows-default-theme)
			{
				:root[tabsintitlebar] .titlebar-button
				{
					transform: translateY(-8px) !important;
				}
				
				@media (-moz-windows-accent-color-in-titlebar: 0)
				{					
					:root[tabsintitlebar]:not(:-moz-lwtheme):not([privatebrowsingmode=temporary])
					{
						background-color: #dee1e6 !important;
						color: #3c4043 !important;
        		  	}
					
					:root[tabsintitlebar]:not(:-moz-lwtheme):not([privatebrowsingmode=temporary]):-moz-window-inactive
					{
						background-color: #e7eaed !important;
						color: #666a6d !important;
        		  	}
        		}
      		}
		}
	}
}

#navigator-toolbox :-moz-any(toolbar, #nav-bar-customization-target):not(#toolbar-menubar)
{
	-moz-box-align: center !important;
}

#navigator-toolbox :-moz-any(.toolbarbutton-1, toolbarbutton.bookmark-item:not(.subviewbutton)),
/* no ::part workaround - may have side effects */
#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton),
#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton)
{
	border-radius: 99px !important;
	transition-property:
		background-color,
		background-size,
		fill-opacity !important;
	transition-duration: .3s !important;
	transition-timing-function:
		var(--ease-basic),
		var(--ease-out),
		var(--ease-basic) !important;

	fill: currentColor !important;
	fill-opacity: 0 !important;

	background-image:
		url(circle.svg),
		url(circle.svg) !important;
	background-size: 25% !important;
	background-repeat: no-repeat !important;
	background-position: center !important;
	background-color: transparent !important;
}

#navigator-toolbox :-moz-any(.toolbarbutton-1, toolbarbutton.bookmark-item:not(.subviewbutton)),
/* no ::part workaround - may have side effects */
#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton),
#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton)
{
	font-size: 12px !important;
	padding: 0 !important;
	margin: 0 !important;
	border: 2px solid transparent !important;
	background-clip: padding-box !important;
}

#navigator-toolbox :-moz-any(.toolbarbutton-1, .scrollbutton-up, .scrollbutton-down),
/* no ::part workaround - may have side effects */
.#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton),
.#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton)
{
	max-height: 32px !important;
	height: 32px !important;
	min-height: 32px !important;
}

#navigator-toolbox :-moz-any(.toolbarbutton-1, toolbarbutton.bookmark-item:not(.subviewbutton))[disabled],
/* no ::part workaround - may have side effects */
#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton)[disabled],
#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton)[disabled]
{
	opacity: .42 !important;
}

#navigator-toolbox :-moz-any(.toolbarbutton-1, toolbarbutton.bookmark-item:not(.subviewbutton)):hover:not([disabled]),
#navigator-toolbox :-moz-any(.toolbarbutton-1, toolbarbutton.bookmark-item:not(.subviewbutton)):-moz-any(:hover:active, [checked], [open]):not([disabled]),
/* no ::part workaround - may have side effects */
#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton):hover:not([disabled]),
#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton):hover:not([disabled])
{
	background-color: var(--toolbarbutton-hover-background) !important;
}

#navigator-toolbox :-moz-any(.toolbarbutton-1, toolbarbutton.bookmark-item:not(.subviewbutton)):-moz-any(:hover:active, [checked], [open]):not([disabled]),
/* no ::part workaround - may have side effects */
#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton):hover:active:not([disabled]),
#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton):hover:active:not([disabled])
{
	fill-opacity: .04 !important;
	background-size: 100% !important;
}

#navigator-toolbox :-moz-any(.toolbarbutton-icon, .toolbarbutton-badge-stack),
/* no ::part workaround - may have side effects */
#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton) > .toolbarbutton-icon,
#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton) > .toolbarbutton-icon
{
	fill-opacity: 1 !important;
	border: 0 !important;
	box-shadow: none !important;
}

#navigator-toolbox .toolbarbutton-1 > :-moz-any(.toolbarbutton-icon, .toolbarbutton-badge-stack)
{
	width: calc(2 * var(--toolbarbutton-inner-padding) + 16px) !important;
	height: calc(2 * var(--toolbarbutton-inner-padding) + 16px) !important;
}

#navigator-toolbox > #PersonalToolbar .toolbarbutton-1 > .toolbarbutton-icon,
#navigator-toolbox .toolbarbutton-1 > :-moz-any(.toolbarbutton-icon, .toolbarbutton-badge-stack),
/* no ::part workaround - may have side effects */
#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton) > .toolbarbutton-icon,
#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton) > .toolbarbutton-icon
{
	padding: 6px !important;
	background: 0 !important;
	width: auto !important;
	height: auto !important;
}

/*
	hack: fix customization screen popping bug when changing ui density
	icon size is enforced and it doesn't like it when the normal density
	nav-bar height is less than 38px (it's 36px due to smaller back icon)
*/
:root:not([uidensity=compact]) #customization-content-container
{
	padding-top: 1px !important;
}

:root:not([uidensity=compact]) :-moz-any(#customization-palette-container, #customization-panel-container)
{
	margin-top: -1px !important;
}

.menu-iconic,
.menuitem-iconic
{
	fill: currentColor !important;
}

/* legacy */
#navigator-toolbox
{
	border-bottom: 1px solid var(--chrome-content-separator-color, var(--toolbox-border-bottom-color)) !important;
}

/* legacy */
#navigator-toolbox::after
{
	display: none !important;
}

.pointerlockfswarning
{
	display: flex !important;
	border-radius: 4px !important;
	padding: 6px !important;
	background: #333 !important;
	border: 0 !important;
	font-size: 14px !important;
	box-shadow:
		0 3px 5px -1px rgba(0,0,0,.2),
		0 6px 10px 0 rgba(0,0,0,.14),
		0 1px 18px 0 rgba(0,0,0,.12) !important;
	color: hsla(0,0%,100%,.87) !important;
	transition-timing-function: var(--ease-out), linear !important;
	transition-property: transform, top !important;
}

#fullscreen-warning
{
	flex-direction: column !important;
	min-width: 344px !important;
}

.pointerlockfswarning[hidden]
{
	visibility: hidden !important;
}

/* hack to keep it in same place as [ontop] */
/* probably doesn't work for single-line */
.pointerlockfswarning:not([hidden]):not([ontop])
{
	top: -32px !important;
}

.pointerlockfswarning[ontop]
{
	top: 28px !important;
}

.pointerlockfswarning::before
{
	display: none !important;
}

.pointerlockfswarning-domain-text
{
	margin-block: 8px 18px !important;
	margin-inline: 10px !important;
	font-size: unset !important;
	font-weight: unset !important;
	align-self: start !important;
}

.pointerlockfswarning-domain
{
	font-weight: unset !important;
}

#fullscreen-exit-button
{
	-moz-appearance: none !important;
	border: 0 !important;
	height: 32px !important;
	display: flex !important;
	align-items: center !important;
	margin-inline: 8px 0 !important;
	text-transform: uppercase !important;
	font-weight: 500 !important;
	letter-spacing: .0892857143em !important;
	background: 0 !important;
	position: relative !important;
	color: var(--toolbarbutton-icon-fill-attention) !important;
	align-self: end !important;
	font-family: Roboto, inherit !important;
}

#fullscreen-exit-button::before
{
	content: "" !important;
	display: block !important;
	z-index: -1 !important;
	position: absolute !important;
	left: 0 !important;
	right: 0 !important;
	top: 0 !important;
	bottom: 0 !important;
	border-radius: 4px !important;
	background: currentColor !important;
	opacity: 0 !important;
	transition: opacity .3s var(--ease-basic) !important;
}

#fullscreen-exit-button:hover::before
{
	opacity: .12 !important;
}

#fullscreen-exit-button:hover:active::before
{
	opacity: .24 !important;
	transition-duration: .1s !important;
}

/* note: use Firefox account button instead */
#TabsToolbar .private-browsing-indicator
{
	display: none !important;
}

/* high DPI adjustments */
@media (min--moz-device-pixel-ratio: 2)
{
	#navigator-toolbox
	{
		border-bottom-width: .5px !important;
		padding-bottom: .5px !important;
		box-shadow: inset 0 -.5px var(--toolbar-bgcolor) !important;
	}
}
/* light mode */
:root:not([style]),
:root[style*="--lwt-accent-color:rgb(227, 228, 230);"],
:root[style*="--lwt-accent-color:white;"]
{
	/* accentcolor */
	--lwt-accent-color: #dee1e6 !important;
	
	/* textcolor */
	--lwt-text-color: #606368 !important;
	
	/* toolbar */
	--toolbar-bgcolor: #fff !important;
	
	/* toolbar_text */
	--toolbar-color: #606368 !important;
	
	/* toolbar_bottom_separator */
	--toolbox-border-bottom-color: #b3b1b3 !important; /* legacy */
	--chrome-content-separator-color: #b3b1b3 !important;
	
	/* icons */
	--lwt-toolbarbutton-icon-fill: #606368 !important;
	
	/* icons_attention */
	--lwt-toolbarbutton-icon-fill-attention: #5086ec !important;
	
	/* button_background_hover */
	--lwt-toolbarbutton-hover-background: rgba(0, 0, 0, .07) !important;
	
	/* button_background_active */
	--lwt-toolbarbutton-active-background: rgba(0, 0, 0, .11) !important;
	
	--lwt-toolbar-field-color: #202124 !important;
	
	/* popup_highlight */
	--autocomplete-popup-highlight-background: #f2f2f2 !important;
	
	/* popup_highlight_text */
	--autocomplete-popup-highlight-color: #202124 !important;
}

/* don't bother with sidebar in light mode */

/* dark mode */
:root[style*="--lwt-accent-color:rgb(12, 12, 13);"],
:root[privatebrowsingmode=temporary]
{
	/* accentcolor */
	--lwt-accent-color: #202124 !important;
	
	/* textcolor */
	--lwt-text-color: #9ba0a5 !important;
	
	/* toolbar */
	--toolbar-bgcolor: #333639 !important;
	
	/* toolbar_text */
	--toolbar-color: #fff !important;
	
	/* toolbar_bottom_separator */
	--toolbox-border-bottom-color: #282828 !important; /* legacy */
	--chrome-content-separator-color: #282828 !important;
	
	/* icons */
	--lwt-toolbarbutton-icon-fill: #fff !important;
	
	/* icons_attention */
	--lwt-toolbarbutton-icon-fill-attention: #5086ec !important;
	
	/* button_background_hover */
	--lwt-toolbarbutton-hover-background: #434649 !important;
	
	/* button_background_active */
	--lwt-toolbarbutton-active-background: #4e5153 !important;
	
	--lwt-toolbar-field-color: #fff !important;
	
	/* popup */
	--arrowpanel-background: #373737 !important;
	
	/* popup_text */
	--arrowpanel-color: #fff !important;
	--autocomplete-popup-color: #fff !important;
	--panel-disabled-color: hsla(0, 0%, 100%, .5) !important;
	
	/* popup_border */
	--arrowpanel-border-color: #555 !important;
	--autocomplete-popup-border-color: #555 !important;
	
	/* popup_highlight */
	--autocomplete-popup-highlight-background: #4c4f52 !important;
	
	/* popup_highlight_text */
	--autocomplete-popup-highlight-color: #fff !important;
}

:-moz-any(#sidebar-box, #sidebar)[style*="--sidebar-background-color:rgb(56, 56, 61);"]
{
	/* sidebar */
	--sidebar-background-color: #333639 !important;
	
	/* sidebar_text */
	--sidebar-text-color: #f1f3ee !important;
}

:-moz-any(.sidebar-panel, body)[style*="--lwt-sidebar-background-color:rgb(56, 56, 61);"]
{
	/* sidebar */
	--lwt-sidebar-background-color: #333639 !important;
	
	/* sidebar_text */
	--lwt-sidebar-text-color: #f1f3ee !important;
}

:root
{
    --ease-in: cubic-bezier(.4, 0, 1, 1);
	--ease-out: cubic-bezier(0, 0, .2, 1);
	--ease-basic: linear;

	--button-size: 32px; /* is this used? */
	--icon-size: 24px; /* is this used? */
	
	--downloads-item-height: 48px !important;
	
	--toolbar-non-lwt-bgcolor: var(--toolbar-bgcolor) !important;
	--toolbar-non-lwt-textcolor: var(--toolbar-color) !important;
	
	--toolbar-field-fontsize: 14px !important;
}

#titlebar,
#tabbrowser-tabs
{
 	--tab-min-height: 34px !important;
}

@supports -moz-bool-pref("materialFox.reduceTabOverflow")
{
	#tabbrowser-tabs
	{
		--tab-min-width: 32px !important;
	}
}

:root
{	
	--tab-separator-opacity: .35 !important;
	
	--toolbar-field-background-color: hsl(200, 12%, 95%) !important;
	--toolbar-field-hover-background-color: hsl(216, 12%, 92%) !important;
	--toolbar-field-focus-background-color: hsl(0, 0%, 100%) !important;
}

:root:-moz-any(:-moz-lwtheme-brighttext, [privatebrowsingmode=temporary])
{
	--toolbar-field-background-color: #202124 !important;
	--toolbar-field-hover-background-color: #292a2d !important;
	--toolbar-field-focus-background-color: #202124 !important;
}

:root:not(:-moz-lwtheme):not([privatebrowsingmode=temporary])
{
	--toolbar-bgcolor: hsl(0, 0%, 100%) !important;
	--toolbar-color: hsl(213, 5%, 39%) !important;
	--toolbox-border-bottom-color: hsl(0, 0%, 70%) !important;
}

@media (-moz-mac-yosemite-theme)
{
	:root[extradragspace]
	{
		--space-above-tabbar: 8px !important;
	}
}

/* Ubuntu */
@media (-moz-gtk-csd-available)
{
	:root:not(:-moz-lwtheme):not([privatebrowsingmode=temporary])
	{
		--toolbar-bgcolor: -moz-dialog !important;
		--toolbar-color: -moz-dialogtext !important;
	}
}

:root:-moz-lwtheme-darktext,
:root toolbar:not([brighttext]),
.tabbrowser-tab[visuallyselected]
{
	--toolbarbutton-hover-background: var(--lwt-toolbarbutton-hover-background, hsla(0, 0%, 0%, .07)) !important;
    --toolbarbutton-active-background: var(--lwt-toolbarbutton-active-background, hsla(0, 0%, 0%, .11)) !important;
}

:root:-moz-lwtheme-brighttext,
:root toolbar[brighttext],
.tabbrowser-tab[visuallyselected]:-moz-lwtheme-brighttext
{
    --toolbarbutton-hover-background: var(--lwt-toolbarbutton-hover-background, hsla(0, 0%, 100%, .1)) !important;
    --toolbarbutton-active-background: var(--lwt-toolbarbutton-active-background, hsla(0, 0%, 100%, .17)) !important;
}

/* special case for urlbar buttons */
:-moz-any(#urlbar, #searchbar)
{
	--toolbarbutton-hover-background: hsl(210, 5%, 85%) !important;
	--toolbarbutton-active-background: hsl(210, 4%, 80%) !important;
}

:root:-moz-any([privatebrowsingmode=temporary], :-moz-lwtheme-brighttext) :-moz-any(#urlbar, #searchbar)
{
	--toolbarbutton-hover-background: hsl(206, 5%, 26%) !important;
	--toolbarbutton-active-background: hsl(210, 4%, 31%) !important;
}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 22 22">
    <path fill="context-fill" fill-opacity="context-fill-opacity" transform="translate(-1 -1)" 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>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" stroke-opacity="context-fill-opacity" d="M8,1A1,1,0,1,1,7,2,1,1,0,0,1,8,1ZM2,7A1,1,0,1,1,1,8,1,1,0,0,1,2,7ZM14,7a1,1,0,1,1-1,1A1,1,0,0,1,14,7ZM8,13a1,1,0,1,1-1,1A1,1,0,0,1,8,13Zm0.723,0.309L7.309,14.723l-6-6L2.723,7.309Zm-6-4.586L1.309,7.309l6-6L8.723,2.723Zm-0.707.293v-2h12v2h-12Z"/>
</svg>
<svg
	version="1.1" xmlns="http://www.w3.org/2000/svg"
	width="16" height="16" viewBox="0 0 16 16" 
	fill="none" stroke-opacity="context-fill-opacity" stroke="context-fill" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
>
	<path d="M2.25,7.75h11.5"/>
	<path d="M7.75,13.25l-5.5-5.5,5.5-5.5"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M11,9.143V5.5h1v4.639ZM9,7.149V6.5h1V8.146ZM6.672,4.828L8,3.5V6.153Zm-3.96-2.11,9.531,9.5-0.509.494L8,8.985V12.5l-3-3H3v-3H5l0.255-.255L2.218,3.212l0.5-.5h0Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M11,10.5v-5h1v5H11Zm-2-4h1v3H9v-3Zm-6,3v-3H5l3-3v9l-3-3H3Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M13,12.091V4.5l-0.056-.056a3,3,0,0,0-2.388-2.388L10.5,2H5A2,2,0,0,1,7,0h6a2,2,0,0,1,2,2V13ZM12,5V16L7,13.5,2,16V5A2,2,0,0,1,4,3h6A2,2,0,0,1,12,5Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M15,13.47H12.32a5,5,0,0,1-8.66,0H1v-2H3c0-.16,0-.33,0-.49V10H1V8H3V7a5,5,0,0,1,0-.51H1v-2H3.68A5,5,0,0,1,5,2.94l-1.5-1.5L5,0l2,2a5,5,0,0,1,2,0l2-2,1.41,1.41L10.95,2.95a5,5,0,0,1,1.36,1.52H15v2H13A5,5,0,0,1,13,7V8h2v2H13v1c0,.17,0,.33,0,.49h2Zm-5.51-7h-3V8h3Zm0,3.49h-3v1.5h3Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
    <path fill="context-fill" fill-opacity="context-fill-opacity" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M13.5,16h-8A1.5,1.5,0,0,1,4,14.5V4.5A1.5,1.5,0,0,1,5.5,3h8A1.5,1.5,0,0,1,15,4.5v10A1.5,1.5,0,0,1,13.5,16Zm0-11.5h-8v10h8Zm-11-3v10H1V1.5A1.5,1.5,0,0,1,2.5,0H11V1.5Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M6.11,4.51A3.12,3.12,0,0,0,6.4,3.2,3.2,3.2,0,1,0,3.2,6.4a3.12,3.12,0,0,0,1.31-.29L6.4,8,4.51,9.89A3.12,3.12,0,0,0,3.2,9.6a3.2,3.2,0,1,0,3.2,3.2,3.12,3.12,0,0,0-.29-1.31L8,9.6,13.4,15H16v-.6Zm-3,.24A1.62,1.62,0,1,1,4.75,3.13,1.62,1.62,0,0,1,3.13,4.75Zm0,9.74a1.62,1.62,0,1,1,1.62-1.62A1.62,1.62,0,0,1,3.13,14.49ZM8,8.49A.49.49,0,1,1,8.49,8,.49.49,0,0,1,8,8.49ZM13.41,1,8.8,5.6l1.6,1.6L16,1.6V1Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 20 20">
    <path fill="context-fill" fill-opacity="context-fill-opacity" transform="translate(-2 -2)" d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.5,14.5H1.5A1.5,1.5,0,0,1,0,13V3A1.5,1.5,0,0,1,1.5,1.5h13A1.5,1.5,0,0,1,16,3V13A1.5,1.5,0,0,1,14.5,14.5Zm0-11.25L8,7.25l-6.5-4V5L8,9l6.5-4Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
    <path fill="context-fill" fill-opacity="context-fill-opacity" d="M1.5,2H6L8,4h6.5A1.5,1.5,0,0,1,16,5.5v8A1.5,1.5,0,0,1,14.5,15H1.5A1.5,1.5,0,0,1,0,13.5V3.5A1.5,1.5,0,0,1,1.5,2Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
    <path fill="context-fill" fill-opacity="context-fill-opacity" d="M13.4,9.92A4.915,4.915,0,0,0,13.8,8,5.058,5.058,0,0,0,8.735,2.946a4.958,4.958,0,0,0-1.913.4L5.741,2.261A6.357,6.357,0,0,1,8.735,1.5,6.508,6.508,0,0,1,15.251,8a6.309,6.309,0,0,1-.77,3Zm2.476,4.1-0.884.884L12.98,12.893a6.469,6.469,0,0,1-8.851-.3l1.028-1.025a5.018,5.018,0,0,0,6.8.3l-7.1-7.1A5,5,0,0,0,3.666,8H5.838L2.913,10.91l-0.051-.1L0.046,8H2.218A6.433,6.433,0,0,1,3.84,3.752L1.214,1.126,2.1,0.242Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
    <path fill="context-fill" fill-opacity="context-fill-opacity" d="M12.72,12.31A1.61,1.61,0,0,0,11.2,11.2h-.71V8.8a.8.8,0,0,0-.8-.8H5V6.49H6.21a.8.8,0,0,0,.8-.8V4H8.8a1.6,1.6,0,0,0,1.6-1.6V2.07a6.39,6.39,0,0,1,2.32,10.24m-5.52,2A6.39,6.39,0,0,1,1.6,8a6,6,0,0,1,.17-1.43L5.6,10.4v.8a1.6,1.6,0,0,0,1.6,1.6M8,0a8,8,0,1,0,8,8A8,8,0,0,0,8,0Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 22 22">
    <path fill="context-fill" fill-opacity="context-fill-opacity" transform="translate(-1 -1)" d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="context-fill" stroke-opacity="context-fill-opacity" d="M15,8L13,6v9H3V6L1,8,8,1l7,7"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.5,8.25a0.75,0.75,0,0,1-1.28.53h0L13,8.57V13.5a1,1,0,0,1-1,1H4a1,1,0,0,1-1-1V8.57l-0.219.211h0A0.75,0.75,0,1,1,1.72,7.72h0l5.75-5.844h0a0.749,0.749,0,0,1,1.058,0h0l5.75,5.844h0A0.747,0.747,0,0,1,14.5,8.25ZM4.5,13h7V7.124L8,3.75,4.5,7.124V13Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<g fill="none" stroke="context-fill" stroke-opacity="context-fill-opacity" stroke-miterlimit="10" stroke-width="2">
		<path d="M9,11h3a3,3,0,0,0,0-6H9"/>
		<path d="M7,11H4A3,3,0,0,1,4,5H7"/>
		<line x1="5" y1="8" x2="11" y2="8"/>
	</g>
</svg>
.toolbarbutton-1:-moz-any(#back-button, #forward-button),
#context-back,
#context-forward,
.subviewbutton-back,
.identity-popup-expander
{
	list-style-image: url(arrow-back-1x.svg) !important;
}

#PanelUI-menu-button
{
	list-style-image: url(menu.svg) !important;
}

#PanelUI-menu-button[badge-status|="update"]
{
	list-style-image: url(menu-update.svg) !important;
}

#reload-button,
#context-reload
{
	list-style-image: url(refresh-1x.svg) !important;
}

#stop-button
{
	list-style-image: url(stop-1x.svg) !important;
}

.toolbarbutton-1#forward-button
{
	transform: scaleX(-1) !important;
}

#home-button
{
	list-style-image: url(home-1x.svg) !important;
}

#star-button,
#context-bookmarkpage,
#pageAction-panel-bookmark,
#panelMenuBookmarkThisPage
{
	list-style-image: url(star-1x.svg) !important;
	fill-opacity: 0 !important;
}

#star-button[starred],
#tracking-protection-icon-box[active]
{
	fill: var(--toolbarbutton-icon-fill-attention) !important;
}

#star-button[starred],
#context-bookmarkpage[starred],
#panelMenuBookmarkThisPage
{
	fill-opacity: 1 !important;
}

#bookmarks-menu-button,
#appMenu-library-bookmarks-button,
#panelMenu_toggleBookmarksMenu
{
	list-style-image: url(bookmark.svg) !important;
}

#library-button,
#appMenu-library-button
{
	list-style-image: url(library-1x.svg) !important;
}

#preferences-button,
#appMenu-preferences-button,
.identity-popup-preferences-button,
#urlbar-anon-search-settings-compact .button-icon
{
	list-style-image: url(settings.svg) !important;
}

#history-panelmenu,
#appMenu-library-history-button
{
	list-style-image: url(history.svg) !important;
}

#appMenu-logins-button,
richlistitem[originaltype="loginWithOrigin"] .ac-site-icon,
richlistitem[originaltype="loginsFooter"] .ac-site-icon
{
	list-style-image: url(key.svg) !important;
}

.subviewbutton[type="highlight-history"]::after
{
	content: url(history.svg) !important;
}

#panic-button
{
	list-style-image: url(forget.svg) !important;
}

#developer-button
{
	list-style-image: url(developer.svg) !important;
}

#add-ons-button,
#appMenu-addons-button
{
	list-style-image: url(addons.svg) !important;
}

#email-link-button,
#pageAction-panel-emailLink
{
	list-style-image: url(email.svg) !important;
}

#pageAction-panel-shareURL
{
	list-style-image: url(share.svg) !important;
}

#pageAction-panel-copyURL
{
	list-style-image: url(hyperlink.svg) !important;
}

#pageAction-panel-webcompat-reporter_mozilla_org
{
	list-style-image: url(bug.svg) !important;
}

#pageAction-panel-webcompat-reporter_mozilla_org > .toolbarbutton-icon
{
	list-style-image: inherit !important;
}

#screenshots_mozilla_org-menuitem-_create-screenshot .menu-iconic-left
{
	background-image: url(screenshot.svg) !important;
	background-position: 4px center !important;
	background-repeat: no-repeat !important;
}

#screenshots_mozilla_org-menuitem-_create-screenshot .menu-iconic-icon
{
	visibility: hidden !important;
}

#tabs-newtab-button,
#new-tab-button
{
	list-style-image: url(new-tab.svg) !important;
}

#fxa-avatar-image
{
	/* icon should be 20px, but we don't want to affect button size */
	transform: scale(1.25) !important;
	border-radius: 50% !important;
}

:root:not([fxastatus=signedin]) #fxa-avatar-image
{
	list-style-image: url(profile-inactive.svg) !important;
}

:root[fxastatus=signedin]:not([style*="--avatar-image-url"]) #fxa-avatar-image
{
	list-style-image: url(profile.svg) !important;
}

:root[privatebrowsingmode] #fxa-avatar-image
{
	list-style-image: url(incognito.svg) !important;
}

#tracking-protection-icon
{
	list-style-image: url(tracking-protection.svg) !important;
}

#pageActionButton
{
	list-style-image: url(menu.svg) !important;
}

#urlbar[pageproxystate="invalid"] #identity-icon,
.searchbar-search-icon,
#PopupAutoCompleteRichResult .ac-type-icon[type="keyword"],
#PopupAutoCompleteRichResult .ac-site-icon[type="searchengine"],
#appMenu-find-button,
#panelMenu_searchBookmarks
{
	list-style-image: url(search.svg) !important;
}

#appMenu-cut-button
{
	list-style-image: url(cut.svg) !important;
}

#appMenu-copy-button
{
	list-style-image: url(copy.svg) !important;
}

#appMenu-paste-button
{
	list-style-image: url(paste.svg) !important;
}

#appMenu-fxa-label,
#PanelUI-fxa-menu-syncnow-button,
#appMenu-fxa-icon,
#PanelUI-remotetabs-syncnow
{
	list-style-image: url(sync.svg) !important;
}

.tab-icon-image:not([src])
{
    list-style-image: url(globe.svg) !important;
}

.tab-icon-image[src="chrome://browser/skin/settings.svg"]
{
	display: none !important;
}

.tab-icon-image[src="chrome://browser/skin/settings.svg"] ~ .tab-icon-overlay
{
	display: -moz-box !important;
	margin: 0 !important;
	list-style-image: url(settings.svg) !important;
	-moz-context-properties: fill, fill-opacity !important;
	fill: currentColor !important;
}

.tab-icon-overlay[soundplaying],
.tab-icon-sound
{
    list-style-image: url(audio.svg) !important;
}

.tab-icon-overlay[muted],
.tab-icon-sound[muted]
{
    list-style-image: url(audio-muted.svg) !important;
}

.tab-icon-overlay[activemedia-blocked],
.tab-icon-sound[activemedia-blocked]
{
	list-style-image: url(media-blocked.svg) !important;
}

.downloadIconShow .button-icon
{
	list-style-image: url(folder.svg) !important;
}

.panel-banner-item[notificationid^="update"]
{
	list-style-image: url(menu-update.svg) !important;
	-moz-context-properties: fill, fill-opacity !important;
	fill: currentColor !important;
}

/* high DPI adjustments */
@media (min--moz-device-pixel-ratio: 2)
{
	.toolbarbutton-1:-moz-any(#back-button, #forward-button),
	#context-back,
	#context-forward,
	.subviewbutton-back,
	.identity-popup-expander
	{
		list-style-image: url(arrow-back-2x.svg) !important;
	}
	
	#PanelUI-menu-button:not([badge-status|="update"]) .toolbarbutton-icon
	{
		transform: translate(-.5px, -.5px) !important;
	}
	
	#stop-button
	{
		list-style-image: url(stop-2x.svg) !important;
	}
	
	#reload-button,
	#context-reload
	{
		list-style-image: url(refresh-2x.svg) !important;
	}
	
	#home-button
	{
		list-style-image: url(home-2x.svg) !important;
	}
	
	#star-button,
	#context-bookmarkpage,
	#pageAction-panel-bookmark,
	#panelMenuBookmarkThisPage
	{
		list-style-image: url(star-2x.svg) !important;
	}
	
	#bookmarks-menu-button .toolbarbutton-icon
	{
		transform: translateX(.5px) !important;
	}
	
	#library-button
	{
		list-style-image: url(library-2x.svg) !important;
	}
}
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
	<defs>
		<mask id="cutout">
			<rect fill="#fff" width="100%" height="100%"/>
			<path transform="translate(2 2)" fill="#000" d="M1,8L4.25,6.25h7.5L15,8H1ZM4.75,4.75l1-3L8,2.75l2.25-1,1,3H4.75ZM4.875,9.5a2.367,2.367,0,0,1,2.249,1.645A3.755,3.755,0,0,1,8,11a3.755,3.755,0,0,1,.876.145,2.374,2.374,0,1,1-.1,1.04c-0.286-.045-0.622-0.1-0.78-0.141C8.106,12.018,8.131,12,8,12s-0.106.018,0,.044c-0.158.039-.493,0.1-0.78,0.141A2.37,2.37,0,1,1,4.875,9.5Zm6.25,3.75A1.375,1.375,0,1,0,9.75,11.875,1.375,1.375,0,0,0,11.125,13.25Zm-6.25,0A1.375,1.375,0,1,0,3.5,11.875,1.375,1.375,0,0,0,4.875,13.25ZM8,12.044H8Z"/>
		</mask>
	</defs>
	<circle mask="url(#cutout)" fill="context-fill" cx="10" cy="10" r="10"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M8.47,6.48a4.36,4.36,0,1,0,0,2.91l3,0v3h3v-3H16v-3ZM4.36,9.39A1.45,1.45,0,1,1,5.82,7.93,1.46,1.46,0,0,1,4.36,9.39Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.5,12h-9A1.5,1.5,0,0,1,4,10.5v-9A1.5,1.5,0,0,1,5.5,0h9A1.5,1.5,0,0,1,16,1.5v9A1.5,1.5,0,0,1,14.5,12ZM14,2H10V8l2-1,2,1V2ZM13,16H1.5A1.5,1.5,0,0,1,0,14.5V3H2V14H13v2Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14,12.5H5A1.5,1.5,0,0,1,3.5,11V2A1.5,1.5,0,0,1,5,.5h9A1.5,1.5,0,0,1,15.5,2v9A1.5,1.5,0,0,1,14,12.5ZM14,2H10.25V8l1.871-1L14,8V2ZM12.5,15.5H2A1.5,1.5,0,0,1,.5,14V3.5H2V14H12.5v1.5Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M5.886,4.045L12,7.484,10.363,8.508ZM2.711,2.718l9.531,9.5-0.509.494L8.616,9.6,4,12.484V4.991L2.218,3.212l0.5-.5h0Zm9.537,9.495h0Z"/>
</svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill-rule="evenodd" fill="context-fill" stroke-opacity="context-fill-opacity" d="M8,0A8,8,0,1,1,0,8,8,8,0,0,1,8,0ZM6,9h4v3H6V9ZM8,4l4,5H4Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
    <path fill="context-fill" fill-opacity="context-fill-opacity" d="M8.5,2A1.5,1.5,0,1,1,7,3.5,1.5,1.5,0,0,1,8.5,2Zm0,5A1.5,1.5,0,1,1,7,8.5,1.5,1.5,0,0,1,8.5,7Zm0,5A1.5,1.5,0,1,1,7,13.5,1.5,1.5,0,0,1,8.5,12Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M8,2A1,1,0,0,1,9,3V13a1,1,0,0,1-2,0V3A1,1,0,0,1,8,2ZM3,7H13a1,1,0,0,1,0,2H3A1,1,0,0,1,3,7Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M13,1.5H10.29a2.5,2.5,0,0,0-4.58,0H3A1.5,1.5,0,0,0,1.5,3V14.5A1.5,1.5,0,0,0,3,16H13a1.5,1.5,0,0,0,1.5-1.5V3A1.5,1.5,0,0,0,13,1.5Zm-5,0a1,1,0,1,1-1,1A1,1,0,0,1,8,1.5Zm5,13H3V3H4.5V5h7V3H13Z"/>
</svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
    <rect fill="#e1e1e1" width="100%" height="100%"/>
    <path fill="#a3a3a3" d="M16,13.833a7.113,7.113,0,0,1-12,0V13.75c0-1.5,3.5-3,6-3s6,1.5,6,3v0.082ZM13,6a3,3,0,1,1-3-3A3,3,0,0,1,13,6Z"/>
</svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
	<defs>
        <mask id="cutout">
            <rect fill="#fff" width="100%" height="100%"/>
            <path fill="#000" d="M16,13.833a7.113,7.113,0,0,1-12,0V13.75c0-1.5,3.5-3,6-3s6,1.5,6,3v0.082ZM13,6a3,3,0,1,1-3-3A3,3,0,0,1,13,6Z"/>
        </mask>
    </defs>
    <rect mask="url(#cutout)" fill="context-fill" width="100%" height="100%"/>
</svg>
<svg
	xmlns="http://www.w3.org/2000/svg"
	width="16" height="16" viewBox="0 0 16 16"
	fill-rule="evenodd" 
>
	<path
		fill="none"
		stroke-opacity="context-fill-opacity" stroke="context-fill" stroke-linecap="round" stroke-width="2"
		d="M13.719,10.25A5.881,5.881,0,0,1,8,14,6,6,0,1,1,12.243,3.757"
	/>
	<path
		fill-opacity="context-fill-opacity"
		fill="context-fill"
		d="M15,1V7H9Z"
	/>
</svg>
<svg
	xmlns="http://www.w3.org/2000/svg"
	width="16" height="16" viewBox="0 0 16 16"
	fill-rule="evenodd" 
>
	<path
		fill="none"
		stroke-opacity="context-fill-opacity" stroke="context-fill" stroke-linecap="round" stroke-width="1.5"
		d="M13.292,10.25a5.752,5.752,0,1,1-.261-5.031"
	/>
	<path
		fill-opacity="context-fill-opacity"
		fill="context-fill"
		d="M15,1.5V7H9.5Z"
	/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.5,15H1.5A1.5,1.5,0,0,1,0,13.5V3.5A1.5,1.5,0,0,1,1.5,2h13A1.5,1.5,0,0,1,16,3.5v10A1.5,1.5,0,0,1,14.5,15Zm0-11.5H1.5v10h13ZM9,10.5h1.5V12H9Zm0-3A1.5,1.5,0,0,1,10.5,9H9Zm-3,0H7.5V9H6Zm-3,0H4.5V9H3Z"/>
</svg>
<svg
    xmlns="http://www.w3.org/2000/svg"
    width="16" height="16" viewBox="0 0 16 16"
>
    <path
        fill-opacity="context-fill-opacity"
        fill="context-fill"
        d="M10,9.5l0.75,0.25L14,13l-1,1L9.75,10.75,9.5,10,9.408,9.908a4.549,4.549,0,1,1,.5-0.5ZM6.5,3.25A3.25,3.25,0,1,0,9.75,6.5,3.25,3.25,0,0,0,6.5,3.25Z"
    />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
    <path fill="context-fill" fill-opacity="context-fill-opacity" transform="translate(-2 -2)" d="M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 20 20">
    <path fill="context-fill" fill-opacity="context-fill-opacity" transform="translate(-2 -2)" d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">	
	<path stroke-width="1.5" fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" stroke="context-fill" d="M6,5.547L7.5,1.688,9,5.547l4.25,0.531L10.094,8.75,11,13,7.5,10.75,4,13,4.906,8.75,1.75,6.094Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">	
	<path stroke-width="1.5" fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" stroke="context-fill" d="M6,5.547L7.5,1.688,9,5.547l4.25,0.531L10.094,8.75,11,13,7.5,10.75,4,13,4.906,8.75,1.75,6.094Z" transform="translate(.5 .5)"/>
</svg>
<svg
	xmlns="http://www.w3.org/2000/svg"
	width="16" height="16" viewBox="0 0 16 16"
	fill="none" stroke="context-fill" stroke-opacity="context-fill-opacity" stroke-linecap="round" stroke-width="2"
>
  <path d="M3,13L13,3"/>
  <path d="M13,13L3,3"/>
</svg>
<svg
	xmlns="http://www.w3.org/2000/svg"
	width="16" height="16" viewBox="0 0 16 16"
	fill="none" stroke="context-fill" stroke-opacity="context-fill-opacity" stroke-linecap="round" stroke-width="1.5"
>
  <path d="M3,13L13,3"/>
  <path d="M13,13L3,3"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M13.66,2.34,15.78.22H10.12V5.88l2.12-2.12a6,6,0,0,1,0,8.49,5.87,5.87,0,0,1-2.47,1.48v2.06A8,8,0,0,0,13.66,2.34Zm-9.9,9.9a6,6,0,0,1,0-8.49A5.87,5.87,0,0,1,6.23,2.27V.21A8,8,0,0,0,2.34,13.66L.22,15.78H5.88V10.12Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" transform="scale(.666667)" d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M11,13H5a1,1,0,0,1-1-1V7A1,1,0,0,1,5,6V5a3,3,0,0,1,6,0V6a1,1,0,0,1,1,1v5A1,1,0,0,1,11,13ZM8,3A2,2,0,0,0,6,5V6h4V5A2,2,0,0,0,8,3Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M11,13.5H5a1,1,0,0,1-1-1v-5a1,1,0,0,1,1-1H5.5v-1a2.5,2.5,0,0,1,5,0v1H11a1,1,0,0,1,1,1v5A1,1,0,0,1,11,13.5ZM8,4A1.5,1.5,0,0,0,6.5,5.5v1h3v-1A1.5,1.5,0,0,0,8,4Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" d="M8,1l8,13H0ZM7,6H9V9H7V6Zm0,4H9v2H7V10Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" d="M7.75,2.5L14,13.5H1.5ZM7,6.5H8.5v3H7v-3Zm0,4H8.5V12H7V10.5Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M31.4 14.627c-.044-.289-.088-.46-.088-.46s-.113.131-.3.379a10.8 10.8 0 0 0-.375-2.04 13.883 13.883 0 0 0-.94-2.418 10.078 10.078 0 0 0-.858-1.473q-.177-.264-.36-.512c-.571-.934-1.227-1.5-1.986-2.583a7.826 7.826 0 0 1-.993-2.692 10.88 10.88 0 0 0-.477 1.761c-.779-.786-1.458-1.345-1.866-1.726C21.133.978 21.367 0 21.367 0s-3.773 4.209-2.137 8.6a8.43 8.43 0 0 0 2.81 3.744c1.581 1.3 3.283 2.33 4.18 4.952a8.4 8.4 0 0 0-3.154-3.327 7.593 7.593 0 0 1 .6 3.006 7.145 7.145 0 0 1-8.736 6.96 6.576 6.576 0 0 1-1.77-.6 7.2 7.2 0 0 1-2.121-1.96l-.01-.017.126.046a6.517 6.517 0 0 0 .9.242 5.644 5.644 0 0 0 3.594-.424c1.129-.626 1.813-1.091 2.367-.908h.01c.542.172.969-.353.581-.9a2.949 2.949 0 0 0-2.846-1.114c-1.131.165-2.167.968-3.648.19a3.13 3.13 0 0 1-.278-.163c-.1-.058.317.088.221.022a7.351 7.351 0 0 1-.931-.555c-.022-.018.224.07.2.052a3.591 3.591 0 0 1-.971-.982 1.746 1.746 0 0 1-.066-1.559 1.376 1.376 0 0 1 .6-.566c.192.095.31.166.31.166s-.087-.16-.135-.244c.017-.006.032 0 .049-.011.168.073.539.261.734.376a1.018 1.018 0 0 1 .335.3s.067-.033.017-.173a.907.907 0 0 0-.347-.425h.016A2.952 2.952 0 0 1 12.3 15a2.085 2.085 0 0 0 .171-.906 1.181 1.181 0 0 0-.069-.5c-.054-.1.03-.14.123-.035a.977.977 0 0 0-.079-.239v-.008s.053-.069.078-.095a1.434 1.434 0 0 1 .216-.176 10 10 0 0 1 1.469-.749c.416-.181.759-.32.83-.36a2.287 2.287 0 0 0 .294-.226 1.973 1.973 0 0 0 .661-1.143 1.593 1.593 0 0 0 .017-.178v-.105c-.06-.226-.449-.395-2.483-.586a1.778 1.778 0 0 1-1.454-1.364v.009c-.029.075-.055.15-.081.225.026-.075.052-.151.081-.225v-.016a5.153 5.153 0 0 1 1.992-2.474c.052-.043-.208.011-.156-.032a5.18 5.18 0 0 1 .532-.225c.091-.039-.391-.222-.818-.178a2.2 2.2 0 0 0-.758.178c.1-.086.4-.2.33-.2a4.877 4.877 0 0 0-1.547.585.315.315 0 0 1 .03-.14 2.407 2.407 0 0 0-.966.746 1.286 1.286 0 0 0 .01-.174 2.883 2.883 0 0 0-.475.446l-.009.007a6.3 6.3 0 0 0-3.527-.3l-.01-.009h.012a2.953 2.953 0 0 1-.627-.7l-.008-.012-.014-.021a5.48 5.48 0 0 1-.237-.388 5.527 5.527 0 0 1-.176-.339c0-.008-.009-.011-.013-.012-.024 0-.041.111-.061.082v-.006a4.322 4.322 0 0 1-.309-1.697l-.016.008a1.89 1.89 0 0 0-.716.937c-.062.137-.1.213-.14.288v-.035c.009-.069.039-.212.032-.2s-.013.019-.02.029a1.741 1.741 0 0 0-.252.373 2.366 2.366 0 0 0-.15.383c-.006.021 0-.018 0-.064s.009-.128 0-.111l-.022.043a9.526 9.526 0 0 0-.806 3.044 3.038 3.038 0 0 0-.023.54v.016a6.641 6.641 0 0 0-.819 1.1 15.65 15.65 0 0 0-1.731 4.246 10.379 10.379 0 0 1 .928-1.626 15.041 15.041 0 0 0-1.049 5.514 14.264 14.264 0 0 1 .451-1.634 13.848 13.848 0 0 0 1.25 6.489 15.08 15.08 0 0 0 5.968 6.768 13.057 13.057 0 0 0 3.478 1.667c.163.059.327.117.5.173-.053-.021-.1-.044-.153-.067a15.752 15.752 0 0 0 4.506.659c5.41 0 7.2-2.06 7.36-2.266a2.739 2.739 0 0 0 .639-.858q.156-.064.316-.137l.067-.03.122-.057a11.349 11.349 0 0 0 2.284-1.43 5.511 5.511 0 0 0 2.129-3.11 1.944 1.944 0 0 0 .029-1.433q.083-.132.171-.28a12.743 12.743 0 0 0 1.913-6.2v-.184a7.76 7.76 0 0 0-.115-1.388z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" d="M8,1A7,7,0,1,1,1,8,7,7,0,0,1,8,1ZM8,3A5,5,0,1,1,3,8,5,5,0,0,1,8,3ZM7,4H9V6H7V4ZM7,7H9v4H7V7Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" d="M7.75,1A6.75,6.75,0,1,1,1,7.75,6.75,6.75,0,0,1,7.75,1Zm0,1.5A5.25,5.25,0,1,1,2.5,7.75,5.25,5.25,0,0,1,7.75,2.5ZM7,4.5H8.5V6H7V4.5ZM7,7H8.5v4H7V7Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M12.7,7h-0.92A28.263,28.263,0,0,0,12,3.5l-4.5-2V7.153A0.986,0.986,0,0,0,7,8v5.794A7.942,7.942,0,0,1,3.554,11C1.994,8.647,2,2.5,2,2.5L7.531,0,13,2.5A34.541,34.541,0,0,1,12.7,7Z"/>
	<path fill="#cc4f40" fill-opacity="context-fill-opacity" d="M14,15H9a1,1,0,0,1-1-1V9A1,1,0,0,1,9,8h5a1,1,0,0,1,1,1v5A1,1,0,0,1,14,15Zm-1.139-2.846-0.646-.646,0.646-.646-0.008-.008A0.5,0.5,0,0,0,12.5,10a0.491,0.491,0,0,0-.386.194l-0.606.606-0.646-.646,0,0a0.5,0.5,0,1,0-.7.7l0,0L10.8,11.508l-0.606.606A0.491,0.491,0,0,0,10,12.5a0.5,0.5,0,0,0,.854.354l0.008,0.008,0.646-.646,0.646,0.646,0,0a0.5,0.5,0,0,0,.7-0.7Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M13.152,7.5H12.22a25.187,25.187,0,0,0,.28-3.75L8.025,2,8.012,7.645A0.987,0.987,0,0,0,7.5,8.5v5.309A8,8,0,0,1,4,11C2.431,8.647,2.5,3,2.5,3L8,1l5.5,2A29.211,29.211,0,0,1,13.152,7.5Z"/>
	<path fill="#cc4f40" fill-opacity="context-fill-opacity" d="M14.5,15.5h-5a1,1,0,0,1-1-1v-5a1,1,0,0,1,1-1h5a1,1,0,0,1,1,1v5A1,1,0,0,1,14.5,15.5Zm-1.139-2.846-0.646-.646,0.646-.646-0.008-.008A0.5,0.5,0,0,0,13,10.5a0.491,0.491,0,0,0-.386.194l-0.606.606-0.646-.646,0,0a0.5,0.5,0,1,0-.7.7l0,0L11.3,12.008l-0.606.606A0.491,0.491,0,0,0,10.5,13a0.5,0.5,0,0,0,.854.354l0.008,0.008,0.646-.646,0.646,0.646,0,0a0.5,0.5,0,0,0,.7-0.7Z"/>
</svg>
#nav-bar
{
	min-height: 36px !important;
	box-shadow: none !important;
	padding-inline: 5px 6px !important;
	background-color: var(--toolbar-bgcolor) !important;
}

#reload-button[disabled]:not(:-moz-window-inactive) > .toolbarbutton-icon
{
	opacity: 1 !important;
}

/* Windows */
#PanelUI-button
{
	border: none !important;
	margin: 0 !important;
	padding: 0 !important;
}

#PanelUI-menu-button[badge-status|="update"] .toolbarbutton-badge
{
	display: none !important;
}

:root[privatebrowsingmode] #fxa-toolbar-menu-button
{
	pointer-events: none !important;
}

/* show "Private" label for en languages only */
:root[titlemodifier="(Private Browsing)"] #fxa-toolbar-menu-button::before
{
	content: "Private" !important;
	display: -moz-box !important;
	margin-inline-start: 12px !important;
	margin-inline-end: 6px !important;
}

#nav-bar-customization-target
{
	overflow: visible !important;
}
#navigator-toolbox toolbarbutton.bookmark-item:not(.subviewbutton)
{
	padding: 6px !important;
}
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
	<polyline points="2 10.5 6 14.5 16.5 4" fill="none" stroke="context-fill" stroke-opacity="context-fill-opacity" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M5,2l6,6L5,14V2Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" d="M5.5,2.5l5.25,5.25L5.5,13V2.5Z"/>
</svg>
menupopup
{
	appearance: none !important;
	background-color: var(--arrowpanel-background) !important;
	color: var(--arrowpanel-color) !important;
	-moz-context-properties: fill, fill-opacity !important;
	fill: currentColor !important;
}

menuitem,
menu > menu,
menupopup > menu
{
	appearance: none !important;
}

menuitem[disabled],
menu[disabled]
{
	pointer-events: none !important;
}

.menu-right
{
	appearance: none !important;
	list-style-image: url(menu-right-1x.svg) !important;
}

menuitem[type=radio][checked=true]
{
	list-style-image: url(checkmark-18dp.svg) !important;
}

menuitem[type=radio] .menu-iconic-icon
{
	width: 16px !important;
	height: 16px !important;
	margin-inline-start: 0 !important;
}
.ac-type-icon
{
	order: 9 !important;
	margin-inline-start: 16px !important;
	margin-inline-end: 0 !important;
}

.ac-site-icon
{
	margin-inline-end: 16px !important;
}

.ac-separator
{
	display: none !important;
}

.ac-title,
.ac-url
{
	flex: 1 !important;
}

.search-panel-one-offs
{
	border-radius: 0 0 8px 8px !important;
	margin-bottom: 18px !important;
}

.search-one-offs
{
	display: block !important;
}

#urlbarView-results
{
	padding: 0 !important;
}

.urlbarView-row
{
	padding: 0 16px !important;
	height: 32px !important;
	border-radius: 0 !important;
}

.urlbarView-row-inner
{
	height: 100% !important;
	align-items: center !important;
}

.urlbarView-favicon
{
	margin-inline-end: 16px !important;
}

.urlbarView-title-separator
{
	flex: 1 !important;
}

.urlbarView-title-separator::before
{
	display: none !important;
}

.urlbarView-type-icon
{
	order: 1 !important;
	margin-inline-start: 16px !important;
	margin-inline-end: 0 !important;
}

/* .search-one-offs
{
	padding: 4px !important;
	display: flex !important;
	flex-direction: column !important;
}

.search-panel-one-offs
{
	display: flex !important;
	flex-direction: row !important;
	padding: 0 !important;
}

.searchbar-engine-one-off-item
{
	height: 32px !important;
	border-radius: 16px !important;
	padding: 0 8px !important;
	margin: 4px !important;
	background-image: none !important;
	color: inherit !important;
	border: 0 !important;
	display: flex !important;
	justify-content: center !important;
	align-items: center !important;
	align-content: center !important;
}

.searchbar-engine-one-off-item > .button-box
{
	width: 16px !important;
	max-height: 16px !important;
	transform: translateY(50%) !important;
}

.searchbar-engine-one-off-item::after
{
	margin-inline-start: 8px !important;
	content: attr(tooltiptext) !important;
	line-height: 16px !important;
	transform: translateY(calc(50% - 1px)) !important;
}


.searchbar-engine-one-off-item.dummy
{
	display: none !important;
} */

/* 70+ */

#urlbar-container
{
	max-height: 37px !important;
}

#urlbar,
#searchbar
{
	font-size: 14px !important;
	border-radius: 99px !important;
	background-color: var(--toolbar-field-background-color) !important;
	border: 0 !important;
	box-shadow: none !important;
	padding: 2px !important;
	background-clip: padding-box !important;
	min-height: 28px !important;
	transition: background-color .1s var(--ease-basic) !important;
	-moz-box-align: center !important;
	
	display: block !important;
	margin: 0 !important;
}

#urlbar:not(:-moz-lwtheme),
#searchbar:not(:-moz-lwtheme)
{
	color: inherit !important;
}

#urlbar:hover,
#searchbar:hover
{
	background-color: var(--toolbar-field-hover-background-color) !important;
}

#urlbar[focused],
#urlbar[open],
#searchbar[focused],
#searchbar[open]
{
	background-color: var(--toolbar-field-focus-background-color) !important;
}

#urlbar[focused],
#searchbar[focused]
{
	border: 2px solid var(--toolbar-field-focus-border-color) !important;
	padding: 0 !important;
	margin: 0 !important;
}

#urlbar[open],
#searchbar[open]
{
	border: 0 !important;
	padding: 0 !important;
	border-radius: 8px 8px 0 0 !important;
	min-height: 37px !important;
	margin: 0 -5px -1px !important;
	box-shadow:
		0 5px 5px -3px rgba(0,0,0,.2),
		0 8px 10px 1px rgba(0,0,0,.14),
		0 3px 14px 2px rgba(0,0,0,.12) !important;
	position: relative !important;
	z-index: 99999 !important;
}

#urlbar-input-container
{
	width: 100% !important;
	display: flex !important;
	align-items: center !important;
}

.urlbar-input-box
{
	flex: 1 !important;
}

#urlbar-input
{
	width: 100% !important;
}

#urlbar[open] #urlbar-input-container
{
	height: 37px !important;
}

#urlbar[open] #identity-box
{
	margin-inline-start: 8px !important;
	margin-inline-end: 8px !important;
}

.urlbarView
{
	top: 0 !important;
	left: 0 !important;
	right: 0 !important;
	position: relative !important;
	box-shadow: none !important;
	background: 0 !important;
	border: 0 !important;
	background: white !important;
}

#urlbar[open] .urlbarView
{
	display: block !important;
	width: 100% !important;
}

#urlbar-results
{
	padding: 0 !important;
}

#urlbar-container,
#searchbar-container
{
	overflow: visible !important;
	padding: 0 5px !important;
}

.searchbar-textbox
{
	color: inherit !important;
	font-size: inherit !important;
	min-height: auto !important;
}

#urlbar[pageproxystate=valid] > #identity-box:not(.no-hover) > #identity-icon
{
	fill-opacity: 1 !important;
}

#identity-icon-labels
{
	transform: translateY(-3px) !important;
	color: inherit !important;
	opacity: 1 !important;
	padding: 0 !important;
	margin-inline-start: 8px !important;
	display: none !important;
}

#identity-box,
#urlbar-display-box
{
	position: relative !important;
}

/* separator */
#urlbar[pageproxystate=valid]:not([open]) #identity-box:-moz-any(.notSecureText, .verifiedIdentity, .chromeUI, .extensionPage, .certUserOverridden)::after,
#urlbar-display-box::after
{
	content: "" !important;
	display: -moz-box !important;
	position: absolute !important;
	top: 4px !important;
	bottom: 4px !important;
	right: 0 !important;
	background: #9d9e9f !important;
	width: 1px !important;
	transition: opacity .2s var(--ease-basic) !important;
}

#urlbar[pageproxystate=valid]:not([open]) #identity-box:-moz-any(.notSecureText, .verifiedIdentity, .chromeUI, .extensionPage, .certUserOverridden),
#urlbar-display-box
{
	margin-inline-end: 8px !important;
}

#urlbar[pageproxystate=valid]:not([open]) #identity-box:-moz-any(.notSecureText, .verifiedIdentity, .chromeUI, .extensionPage, .certUserOverridden) #identity-icon-labels
{
	display: -moz-box !important;
}


#urlbar[pageproxystate=valid] #identity-box:-moz-any(.notSecureText, .verifiedIdentity, .chromeUI, .extensionPage, .certUserOverridden):hover:not(.no-hover)::after,
#urlbar[pageproxystate=valid] #identity-box:-moz-any(.notSecureText, .verifiedIdentity, .chromeUI, .extensionPage, .certUserOverridden)[open]::after
{
	opacity: 0 !important;
}

#identity-box.extensionPage > #identity-icon
{
	list-style-image: none !important;
	margin-inline-end: -16px !important;
}

/* no longer used as a connection icon */
#connection-icon
{
	display: none !important;
}

#identity-box:-moz-any(
	.mixedActiveBlocked,
	.mixedDisplayContentLoadedActiveBlocked,
	.mixedActiveContent
) > #connection-icon
{
	display: -moz-box !important;
}

#identity-box:-moz-any(.certUserOverridden, .unknownIdentity)
{
	color: #c94031 !important;
}

.urlbar-input
{
	padding: 0 !important;
}

.searchbar-textbox
{
	border: 0 !important;
	background: 0 !important;
	box-shadow: none !important;
	margin: 0 !important;
}

#page-action-buttons > #pageActionSeparator,
.urlbar-history-dropmarker
{
	display: none !important;
}

#pageActionSeparator
{
	height: 24px !important;
}

.urlbar-icon-wrapper
{
	background: 0 !important;
}

.urlbar-icon,
#identity-box,
#tracking-protection-icon-container
{
	min-width: 32px !important;
	height: 24px !important;
	padding: 4px 8px !important;
	margin: 0 !important;
	border: 0 !important;
	border-radius: 99px !important;
	fill-opacity: 1 !important;
	transition: background-color .2s var(--ease-basic) !important;
	background: 0 !important;
	background-color: transparent !important;
}

#identity-box #notification-popup-box
{
	padding: 0 !important;
	margin: 0 !important;
}

#page-action-buttons
{
	height: 24px !important;
}

#urlbar[open] #page-action-buttons
{
	display: none !important;
}

.urlbar-icon:hover:not([disabled]),
#identity-box:hover:not(.no-hover),
#tracking-protection-icon-container:hover
{
	background-color: var(--toolbarbutton-hover-background) !important;
}

.urlbar-icon:hover:active:not([disabled]),
.urlbar-icon[open],
#identity-box:hover:active:not(.no-hover),
#identity-box[open]:not(.no-hover),
#tracking-protection-icon-container[open]
{
	background-color: var(--toolbarbutton-active-background) !important;
	transition-duration: 0 !important;
}

#star-button-box
{
	-moz-box-ordinal-group: 99 !important;
}

#pageActionButton
{
	-moz-box-ordinal-group: 98 !important;
}

#contextual-feature-recommendation
{
	-moz-box-ordinal-group: 97 !important;
	width: auto !important;
}

.urlbar-page-action:-moz-any(
	#reader-mode-button,
	#pageActionButton,
	#pocket-button-box,
	#pageAction-urlbar-screenshots_mozilla_org,
	#pageAction-urlbar-sendToDevice,
	#pageAction-urlbar-emailLink,
	#pageAction-urlbar-copyURL,
	#pageAction-urlbar-shareURL,
	#pageAction-urlbar-addSearchEngine
)
{
	transition:
		background-color .2s var(--ease-basic),
		margin .15s var(--ease-in),
		opacity .15s var(--ease-basic),
		visibility 0s .2s !important;
	opacity: 0 !important;
	visibility: hidden !important;
	margin-inline-start: -32px !important;
}

#urlbar:hover .urlbar-page-action:-moz-any(
	#reader-mode-button,
	#pageActionButton,
	#pocket-button-box,
	#pageAction-urlbar-screenshots_mozilla_org,
	#pageAction-urlbar-sendToDevice,
	#pageAction-urlbar-emailLink,
	#pageAction-urlbar-copyURL,
	#pageAction-urlbar-shareURL,
	#pageAction-urlbar-addSearchEngine
),
.urlbar-page-action:-moz-any(
	#reader-mode-button,
	#pageActionButton,
	#pocket-button-box,
	#pageAction-urlbar-screenshots_mozilla_org,
	#pageAction-urlbar-sendToDevice,
	#pageAction-urlbar-emailLink,
	#pageAction-urlbar-copyURL,
	#pageAction-urlbar-shareURL,
	#pageAction-urlbar-addSearchEngine
):-moz-any(:hover, [open], [readeractive])
{
	transition:
		background-color .2s var(--ease-basic),
		margin .3s var(--ease-out),
		opacity .3s var(--ease-basic) !important;
	opacity: 1 !important;
	visibility: visible !important;
	margin-inline-start: 0 !important;
}

.urlbar-display
{
	color: inherit !important;
	margin: 0 !important;
}

#pocket-button-box[animate] > #pocket-animatable-box
{
	margin-inline-start: 6px !important;
}

#star-button-animatable-box
{
	display: none !important;
}

#cfr-label-container
{
	background: 0 !important;
}

#cfr-label
{
	display: none !important;
}

#cfr-button
{
	fill: currentColor !important;
}

#urlbar[open] .urlbar-go-button,
#searchbar[open] .search-go-button
{
	height: 32px !important;
}

/* icons 1x */
#urlbar
{
	--info-icon: url(../navbar/info-1x.svg);
	--lock-icon: url(../navbar/connection-1x.svg);
	--warning-icon: url(../navbar/connection-warning-1x.svg);
}

/* icons 2x */
@media (min--moz-device-pixel-ratio: 2)
{
	#urlbar
	{
		--info-icon: url(../navbar/info-2x.svg);
		--lock-icon: url(../navbar/connection-2x.svg);
		--warning-icon: url(../navbar/connection-warning-2x.svg);
	}
}

/* info (not secure) */
#identity-box[pageproxystate="valid"]:-moz-any(.notSecure, .insecureLoginForms, .mixedActiveContent) > #identity-icon
{
    list-style-image: var(--info-icon) !important;
}

#identity-box[pageproxystate="valid"]:-moz-any(
	.certUserOverridden,
	.weakCipher,
	.unknownIdentity) > #identity-icon
{
    list-style-image: var(--warning-icon) !important;
}

/* lock (secure) */
#identity-box[pageproxystate="valid"]:-moz-any(.verifiedDomain, .verifiedIdentity, .mixedActiveBlocked) > #identity-icon
{
	list-style-image: var(--lock-icon) !important;
}

/* high DPI adjustments */
@media (min--moz-device-pixel-ratio: 2)
{
	#identity-icon-labels
	{
		padding-inline-end: .5px !important;
	}
	
	#urlbar[pageproxystate=valid] > #identity-box:-moz-any(.unknownIdentity, .notSecure) > #identity-icon
	{
		transform: translate(.5px, .5px) !important;
	}
	
	#identity-icon-labels,
	.urlbar-input,
	.searchbar-textbox
	{
		transform: translateY(-.5px) !important;
	}
}
<svg
	xmlns="http://www.w3.org/2000/svg"
	width="16"
	height="16"
	viewBox="0 0 16 16"
	stroke-linecap="round"
	stroke-linejoin="round"
	stroke-width="1.5"
	stroke="context-fill"
	stroke-opacity="context-fill-opacity"
>
	<path d="M11.25,11.25l-6.5-6.5"/>
	<path d="M4.75,11.25l6.5-6.5"/>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
	<style>
		:root
		{
			--radius: calc(2 * 3.1415926536 * 37.5%);
			--max-gap: calc(var(--radius) * .99);
			--min-gap: calc(var(--radius) * .5);
		}
	
		@keyframes stroke-rotate
		{
			0% {   stroke-dashoffset: var(--max-gap); }
			100% { stroke-dashoffset: var(--min-gap); }
		}
		
		circle
		{
			fill: transparent;
			stroke: context-fill;
			stroke-width: 12.5%;
			stroke-dasharray: var(--radius);
			stroke-linecap: round;
			transform-origin: center;
			animation: stroke-rotate 1.333s cubic-bezier(.35, 0, .25, 1) both;
		}
	</style>
	<circle cx="50%" cy="50%" r="37.5%"/>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
	<style>
		:root
		{
			--radius: calc(2 * 3.1415926536 * 37.5%);
			--max-gap: calc(var(--radius) * .99);
			--min-gap: calc(var(--radius) * .25);
		}

		@keyframes stroke-rotate
		{
			0% {			  stroke-dashoffset: var(--max-gap);		 }
			12.5% {			  stroke-dashoffset: var(--min-gap);
							  transform: rotate(0);						 }
			12.5001%, 25% {	  transform: rotateX(180deg) rotate(90deg);	 }
			25% {			  stroke-dashoffset: var(--max-gap); 	 	 }
			25.0001%, 37.5% { transform: rotate(270deg); 				 }
			37.5% {			  stroke-dashoffset: var(--min-gap); 	 	 }
			37.5001%, 50% {	  transform: rotateX(180deg) rotate(180deg); }
			50% {			  stroke-dashoffset: var(--max-gap); 	 	 }
			50.0001%, 62.5% { transform: rotate(180deg); 				 }
			62.5% {			  stroke-dashoffset: var(--min-gap); 	 	 }
			62.5001%, 75% {	  transform: rotateX(180deg) rotate(270deg); }
			75% {			  stroke-dashoffset: var(--max-gap); 	 	 }
			75.0001%, 87.5% { transform: rotate(90deg); 				 }
			87.5% {			  stroke-dashoffset: var(--min-gap); 	 	 }
			87.5001%, 100% {  transform: rotateX(180deg) rotate(360deg); }
			100% {			  stroke-dashoffset: var(--max-gap); 	 	 }
		}
		
		circle
		{
			fill: transparent;
			stroke-dasharray: var(--radius);
			stroke: context-fill;
			stroke-width: 12.5%;
			stroke-linecap: round;
			transform-origin: center;
			animation: stroke-rotate calc(1.333s * 4) cubic-bezier(.35, 0, .25, 1) infinite both;
		}
	</style>
	<circle cx="50%" cy="50%" r="37.5%"/>
</svg>
:-moz-any(.titlebar-placeholder, .titlebar-spacer)[type=pre-tabs]
{
	width: 0 !important;
}

@media (-moz-mac-yosemite-theme)
{
	#TabsToolbar
	{
		-moz-appearance: none !important;
	}
	
	:root:-moz-any([inFullscreen], [tabsintitlebar]):not([privatebrowsingmode=temporary]) #TabsToolbar:not(:-moz-lwtheme)
	{
		background-color: #dee1e5 !important;
		color: #606367 !important;
		--toolbarbutton-hover-background: #cbced2 !important;
		--toolbarbutton-active-background: #bec0c5 !important;
	}
	
	:root:-moz-any([inFullscreen], [tabsintitlebar]):not([privatebrowsingmode=temporary]) #TabsToolbar:not(:-moz-lwtheme):-moz-window-inactive
	{
		background-color: #e7eaed !important;
	}
	
	:-moz-any(.titlebar-placeholder, .titlebar-spacer)[type=pre-tabs]
	{
		width: 4px !important;
	}
}

@media (-moz-os-version: windows-win7)
{
	:root:not([privatebrowsingmode=temporary]) #TabsToolbar:not(:-moz-lwtheme)
	{
		color: #808387 !important;
		--toolbarbutton-hover-background: rgba(196, 198, 204, .8) !important;
		--toolbarbutton-active-background: #bec0c6 !important;
	}
	
	:root:not([privatebrowsingmode=temporary]) #TabsToolbar toolbarbutton:not(:-moz-lwtheme):not(:hover):not([open])
	{
		background-color: rgba(218, 220, 227, .8) !important;
	}
	
	:root:not([privatebrowsingmode=temporary]) .tabbrowser-tab:not([visuallyselected]):not(:-moz-lwtheme)
	{
		--tab-bgcolor: rgb(218, 220, 227) !important;
		--tab-opacity: .8 !important;
	}
	
	:root:not([privatebrowsingmode=temporary]) .tabbrowser-tab:hover:not([visuallyselected]):not(:-moz-lwtheme)
	{
		--tab-bgcolor: #e9ebef !important;
	}
}

 /* < FF 65, >= FF 65 */
:-moz-any(.titlebar-placeholder, .titlebar-spacer)
{
	border: 0 !important;
	display: -moz-box !important;
}

:-moz-any(.titlebar-placeholder, .titlebar-spacer)[type=post-tabs]
{
	width: 48px !important;
	transition: width .666s cubic-bezier(.4, 0, .2, 1) !important;
}

@media (max-width: 700px)
{
	:-moz-any(.titlebar-placeholder, .titlebar-spacer)[type=post-tabs]
	{
		width: 8px !important;
	}
}

/* vertically center buttons in tab bar */
#TabsToolbar toolbarbutton,
#TabsToolbar .toolbar-items toolbarbutton.toolbarbutton-1,
/* >= 72 */
#TabsToolbar .tabbrowser-arrowscrollbox::part(scrollbutton-up),
#TabsToolbar .tabbrowser-arrowscrollbox::part(scrollbutton-down),
/* no ::part workaround - may have side effects */
#scrollbutton-up[part="scrollbutton-up"]:not(.menupopup-scrollbutton),
#scrollbutton-down[part="scrollbutton-down"]:not(.menupopup-scrollbutton)
{
	margin-top: var(--tab-shadow-max-size) !important;
	margin-bottom: var(--proton-tab-block-margin) !important;
}

.tabbrowser-tab
{
	min-height: var(--tab-min-height) !important;
	overflow: visible !important;
	font-size: 12px !important;
	background: 0 !important;
	border: 0 !important;
	padding: 0 !important;
}

.tabbrowser-tab[visuallyselected]
{
	color: var(--toolbar-color) !important;
}

/* regular */
.tabbrowser-tab[fadein]:not([pinned]):not([style*="max-width"])
{
	max-width: 240px !important;
}

/* neighbouring tabs should "pinch" together */
.tabbrowser-tab:not([last-visible-tab])
{
	margin-inline-end: -.5px !important;
}

/* special case for pinned tabs when overflowing */
#tabbrowser-tabs:not([overflow]) .tabbrowser-tab:not([first-visible-tab]),
#tabbrowser-tabs[overflow] .tabbrowser-tab:not([first-visible-tab]):not([pinned])
{
	margin-inline-start: -.5px !important;
}

.tab-stack
{
	margin-top: 4px !important;
	margin-bottom: -4px !important;
}

#tabbrowser-tabs[overflow] .tabbrowser-tab[pinned] .tab-background
{
	margin: 0 -1px !important;
}

#tabbrowser-tabs[overflow] .tabbrowser-tab[pinned] .tab-stack
{
	margin-top: 8px !important;
	margin-bottom: 0 !important;
}

.tab-content
{
	position: relative !important;
	overflow: hidden !important;
	padding-inline-start: 12px !important;
	padding-inline-end: 8px !important;
}

.tab-content::before,
.tab-content::after
{
	content: "" !important;
	display: block !important;
	position: absolute !important;
	background-color: currentColor !important;
	width: 1px !important;
	height: 20px !important;
	transform: translateY(-10px) !important;
	opacity: 0 !important;
	transition: opacity .2s var(--ease-basic) !important;
}

.tab-content::before
{
	left: 0 !important;
}

.tab-content::after
{
	right: 0 !important;
}

.tab-throbber,
.tab-throbber-fallback,
.tab-icon-image,
.tab-sharing-icon-overlay,
.tab-icon-sound,
.tab-close-button
{
	margin-top: 0 !important;
}

.tabbrowser-tab::before,
.tabbrowser-tab::after,
.tab-line
{
	display: none !important;
}

/* tab background color */

.tabbrowser-tab
{
	--tab-opacity: 0;
	--tab-bgcolor: #000;
	--tab-transition-duration: .2s;
}

:-moz-any(:root[privatebrowsingmode=temporary], #TabsToolbar[brighttext]) .tabbrowser-tab
{
	--tab-opacity: 0;
	--tab-bgcolor: #fff;
}

.tabbrowser-tab:not([selected=true]):hover,
.tabbrowser-tab[multiselected]:not([selected=true])
{
	--tab-opacity: .1;
}

:-moz-any(:root[privatebrowsingmode=temporary], #TabsToolbar[brighttext]) .tabbrowser-tab:not([selected=true]):hover,
:-moz-any(:root[privatebrowsingmode=temporary], #TabsToolbar[brighttext]) .tabbrowser-tab[multiselected]:not([selected=true])
{
	--tab-opacity: .06;
}

:-moz-any(#TabsToolbar) .tabbrowser-tab[visuallyselected]
{
	--tab-bgcolor: var(--toolbar-bgcolor);
	--tab-opacity: 1;
}

.tab-background
{
 	background: var(--tab-bgcolor) !important;
	/* rounded top corners */
	border-radius: 8px 8px 0 0 !important;
	position: relative !important;
	border: 0 !important;
	transition:
		opacity var(--tab-transition-duration) var(--ease-basic),
		background-color 0s var(--tab-transition-duration) var(--ease-basic) !important;
	opacity: var(--tab-opacity) !important;
	visibility: visible !important;
}

/* rounded bottom corners */
.tab-background::before,
.tab-background::after
{
	content: "" !important;
	display: block !important;
	position: absolute !important;
	width: 8px !important;
	height: 8px !important;
	bottom: 0 !important;
	pointer-events: none !important;
	transition: box-shadow 0s var(--tab-transition-duration) var(--ease-basic) !important;
}

.tab-background::before
{
	border-bottom-right-radius: 8px !important;
	left: 0 !important;
	transform: translateX(-8px) !important;
	box-shadow: 4px 4px 0 4px var(--tab-bgcolor) !important;
}

.tab-background::after
{
	border-bottom-left-radius: 8px !important;
	right: 0 !important;
	transform: translateX(8px) !important;
	box-shadow: -4px 4px 0 4px var(--tab-bgcolor) !important;
}

.tabbrowser-tab[visuallyselected][style*=transform] + .tabbrowser-tab[style*=transform] .tab-content::after,
.tabbrowser-tab[style*=transform]:not([visuallyselected]) .tab-content::before,
.tabbrowser-tab[style*=transform] + .tabbrowser-tab:not([visuallyselected]) .tab-content::before,
.tabbrowser-tab:not([visuallyselected]):not(:hover):not([multiselected]) + .tabbrowser-tab:not([visuallyselected]):not(:hover):not([multiselected]) .tab-content::before,
#tabbrowser-tabs[hasadjacentnewtabbutton]:not([overflow]) .tabbrowser-tab[last-visible-tab]:not([visuallyselected]):not(:hover):not([multiselected]) .tab-content::after
{
	opacity: var(--tab-separator-opacity) !important;
}

#tabbrowser-tabs[overflow] .tabbrowser-tab[pinned] + .tabbrowser-tab:not([pinned]) .tab-content::before
{
	opacity: 0 !important;
}

.tabbrowser-tab[visuallyselected]
{
	--tab-transition-duration: 0s;
}

.tab-throbber,
.tab-throbber-fallback
{
	margin-inline-end: 8px !important;
}

.tab-icon-image
{
	margin: 0 !important;
}

/* hide new tab favicon */
.tabbrowser-tab[image^="chrome://branding/"]:not([pinned]) .tab-icon-image
{
	display: none !important;
}

.tab-label-container
{
	margin-top: -2px !important;
	opacity: 1 !important;
}

.tabbrowser-tab[image]:not([image^="chrome://branding/"]) .tab-label-container,
.tabbrowser-tab:-moz-any([progress], [busy]) .tab-label-container
{
	padding-inline-start: 8px !important;
}

.tabbrowser-tab[pinned]:-moz-any([soundplaying], [muted], [activemedia-blocked]) .tab-icon-image
{
	visibility: hidden !important;
}

.tabbrowser-tab[fadein] .tab-close-button
{
	visibility: visible !important;
}

.tab-close-button
{
	list-style-image: url(close-tab.svg) !important;
	width: 16px !important;
	height: 16px !important;
	margin: 0 !important;
	padding: 0 !important;
}

.tab-close-button,
.tab-icon-overlay:-moz-any([soundplaying], [muted], [activemedia-blocked]),
.tab-icon-sound
{
	border-radius: 99px !important;
	color: inherit !important;
	-moz-context-properties: fill, fill-opacity !important;
	transition-property: fill-opacity, background-color !important;
	transition-duration: .15s !important;
	transition-timing-function: var(--ease-basic) !important;
	fill-opacity: 1 !important;
	opacity: 1 !important;
}

.tab-close-button:hover,
.tab-icon-overlay:-moz-any([soundplaying], [muted], [activemedia-blocked]):hover,
.tab-icon-sound:hover
{
	background-color: var(--toolbarbutton-hover-background) !important;
}

.tab-close-button:hover:active,
.tab-icon-overlay:-moz-any([soundplaying], [muted], [activemedia-blocked]):hover:active,
.tab-icon-sound:hover:active
{
	background-color: var(--toolbarbutton-active-background) !important;
}

.tab-icon-overlay[pinned]
{
	margin: 0 !important;
	margin-inline-start: -16px !important;
}

.tab-sharing-icon-overlay
{
	margin-inline-start: -16px !important;
}

@keyframes rotate-360
{
	0% { transform: rotate(0); }
	100% { transform: rotate(1turn); }
}

.tab-throbber
{
	-moz-context-properties: fill !important;
	fill: currentColor !important;
	background-image: url(spinner-busy.svg) !important;
	margin: 0 !important;
	transform-origin: center !important;
	animation: rotate-360 1.333s linear infinite reverse !important;
	position: static !important;
}

.tab-throbber[progress]
{
	background-image: url(spinner-progress.svg) !important;
	animation-direction: normal !important;
}

.tab-throbber::before
{
	display: none !important;
}

/* clipped tabs */
#tabbrowser-tabs[closebuttons=activetab] .tab-content:not([pinned])
{
	padding-inline-start: 8px !important;
}

#tabbrowser-tabs[closebuttons=activetab] .tabbrowser-tab:not([visuallyselected]) .tab-close-button
{
	visibility: collapse !important;
}

#tabbrowser-tabs[closebuttons=activetab] .tab-label-container[textoverflow][labeldirection="ltr"]:not([pinned]),
#tabbrowser-tabs[closebuttons=activetab] .tab-label-container[textoverflow]:not([labeldirection]):-moz-locale-dir(ltr):not([pinned])
{
    mask-image: linear-gradient(to right, black 70%, transparent) !important;
}

#tabbrowser-tabs[closebuttons=activetab] .tab-label-container[textoverflow][labeldirection="rtl"]:not([pinned]),
#tabbrowser-tabs[closebuttons=activetab] .tab-label-container[textoverflow]:not([labeldirection]):-moz-locale-dir(rtl):not([pinned])
{
    mask-image: linear-gradient(to left, black 70%, transparent) !important;
}

.tab-content[pinned]
{
	-moz-box-pack: center !important;
}

.tab-icon-image[pinned],
.tab-throbber[pinned]
{
	margin: auto !important;
}

.tab-content[pinned]
{
	width: 36px !important;
	padding: 0 !important;
	padding-inline-start: 10px !important;
	padding-inline-end: 0 !important;
}

.tab-label-container[pinned],
.tab-close-button[pinned]
{
	visibility: hidden !important;
} 

/* close button / favicon is centered within 36px tab */
/* disabled for now because it's broken in recent version of Firefox */
@supports -moz-bool-pref("materialFox.reduceTabOverflow disabled")
{
	#main-window
	{
		/* same as Chrome */
		min-width: 500px !important;
	}
	
	#tabbrowser-tabs[overflow] .tab-content
	{
		-moz-box-pack: center !important;
	}
	
	#tabbrowser-tabs[overflow] .tab-icon-image
	{
		margin: auto !important;
	}
	
	#tabbrowser-tabs[overflow] .tab-content[image]:not([image^="chrome://branding/"]):not([pinned])
	{
		width: 36px !important;
		padding: 0 !important;
		padding-inline-start: 0 !important;
		padding-inline-end: 0 !important;
	}
	
	#tabbrowser-tabs[overflow] .tabbrowser-tab:not([visuallyselected]):not([pinned]) .tab-close-button,
	#tabbrowser-tabs[overflow] .tabbrowser-tab[visuallyselected]:not([pinned]) :-moz-any(.tab-label-container, .tab-icon-image),
	#tabbrowser-tabs[overflow] .tabbrowser-tab[image]:not([image^="chrome://branding/"]):not([pinned]) .tab-label-container
	{
		display: none !important;
	} 
}

#tabbrowser-tabs #tabs-newtab-button
{
	margin-inline-start: 6px !important;
}

#tabbrowser-tabs[overflow] .tabbrowser-arrowscrollbox
{
	border-radius: 8px 8px 0 0 !important;
	background-color: rgba(0, 0, 0, .1) !important;
	padding-inline-start: 0 !important;
}

#tabbrowser-tabs[overflow]
{
	margin-inline-start: 8px !important;
}

#tabbrowser-tabs[overflow] .tabbrowser-tab[first-visible-tab]:not([pinned]),
#tabbrowser-tabs[overflow] .tabbrowser-tab[pinned] + .tabbrowser-tab:not([pinned]),
#tabbrowser-tabs:not([overflow]) .tabbrowser-tab[first-visible-tab]
{
	margin-inline-start: 8px !important;
}

#tabbrowser-tabs[overflow] .tabbrowser-tab[last-visible-tab]:not([pinned])
{
	margin-inline-end: 7px !important;
}

.tabbrowser-tab[usercontextid] > .tab-stack::after
{
	content: "" !important;
	position: absolute !important;
	display: flex !important;
	bottom: 0 !important;
	left: 8px !important;
	width: calc(100% - 16px) !important;
	height: 2px !important;
	box-sizing: border-box !important;
	border-radius: 99px 99px 0 0 !important;
	transform: none !important;
	background: var(--identity-tab-color) !important;
	transition-property: top, bottom, left, right, width, height, border-radius;
	transition-duration: .225s !important;
	transition-timing-function: var(--ease-out) !important;
}

.tab-bottom-line
{
	display: none !important;
}

.tabbrowser-tab[usercontextid][selected] > .tab-stack::after
{
	bottom: calc(100% - 9px) !important;
	left: calc(100% - 9px) !important;
	width: 6px !important;
	height: 6px !important;
	border-radius: 99px !important;
}

/* < 72 */
#tabbrowser-tabs .arrowscrollbox-overflow-start-indicator,
#tabbrowser-tabs .arrowscrollbox-overflow-end-indicator,
/* >= 72 */
#tabbrowser-tabs::part(arrowscrollbox-overflow-start-indicator),
#tabbrowser-tabs::part(arrowscrollbox-overflow-end-indicator),
/* no ::part workaround - may have side effects */
spacer[part="overflow-start-indicator"],
spacer[part="overflow-end-indicator"]
{
	display: none !important;
}

.tab-loading-burst[bursting]::before
{
	display: none !important;
}

/* high DPI adjustments */
@media (min--moz-device-pixel-ratio: 2)
{
	.tabbrowser-tab::before
	{
		transform: translateY(.5px) !important;
	}

	.tabbrowser-tab:last-of-type::after
	{
		transform: translate(-1px, .5px) !important;
	}
	
	/* macOS */
	@media (-moz-mac-yosemite-theme)
	{
		.titlebar-buttonbox
		{
			margin-top: .5px !important;
			margin-bottom: -.5px !important;
		}
	}
}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M11,13H5a1,1,0,0,1-1-1V7A1,1,0,0,1,5,6V5a3,3,0,0,1,6,0V6a1,1,0,0,1,1,1v5A1,1,0,0,1,11,13ZM8,3A2,2,0,0,0,6,5V6h4V5A2,2,0,0,0,8,3Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M11,13.5H5a1,1,0,0,1-1-1v-5a1,1,0,0,1,1-1H5.5v-1a2.5,2.5,0,0,1,5,0v1H11a1,1,0,0,1,1,1v5A1,1,0,0,1,11,13.5ZM8,4A1.5,1.5,0,0,0,6.5,5.5v1h3v-1A1.5,1.5,0,0,0,8,4Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" d="M8,1l8,13H0ZM7,6H9V9H7V6Zm0,4H9v2H7V10Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill-rule="evenodd" fill="context-fill" fill-opacity="context-fill-opacity" d="M7.75,2.5L14,13.5H1.5ZM7,6.5H8.5v3H7v-3Zm0,4H8.5V12H7V10.5Z"/>
</svg>
<svg
    xmlns="http://www.w3.org/2000/svg"
    width="16" height="16" viewBox="0 0 16 16"
>
    <path
        stroke="context-fill"
        stroke-opacity="context-fill-opacity"
        fill="none"
        stroke-linecap="round"
        stroke-linejoin="round"
        stroke-width="1.5px"
        d="M5.75,4.25L10.25,8l-4.5,3.75"
    />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M31.4 14.627c-.044-.289-.088-.46-.088-.46s-.113.131-.3.379a10.8 10.8 0 0 0-.375-2.04 13.883 13.883 0 0 0-.94-2.418 10.078 10.078 0 0 0-.858-1.473q-.177-.264-.36-.512c-.571-.934-1.227-1.5-1.986-2.583a7.826 7.826 0 0 1-.993-2.692 10.88 10.88 0 0 0-.477 1.761c-.779-.786-1.458-1.345-1.866-1.726C21.133.978 21.367 0 21.367 0s-3.773 4.209-2.137 8.6a8.43 8.43 0 0 0 2.81 3.744c1.581 1.3 3.283 2.33 4.18 4.952a8.4 8.4 0 0 0-3.154-3.327 7.593 7.593 0 0 1 .6 3.006 7.145 7.145 0 0 1-8.736 6.96 6.576 6.576 0 0 1-1.77-.6 7.2 7.2 0 0 1-2.121-1.96l-.01-.017.126.046a6.517 6.517 0 0 0 .9.242 5.644 5.644 0 0 0 3.594-.424c1.129-.626 1.813-1.091 2.367-.908h.01c.542.172.969-.353.581-.9a2.949 2.949 0 0 0-2.846-1.114c-1.131.165-2.167.968-3.648.19a3.13 3.13 0 0 1-.278-.163c-.1-.058.317.088.221.022a7.351 7.351 0 0 1-.931-.555c-.022-.018.224.07.2.052a3.591 3.591 0 0 1-.971-.982 1.746 1.746 0 0 1-.066-1.559 1.376 1.376 0 0 1 .6-.566c.192.095.31.166.31.166s-.087-.16-.135-.244c.017-.006.032 0 .049-.011.168.073.539.261.734.376a1.018 1.018 0 0 1 .335.3s.067-.033.017-.173a.907.907 0 0 0-.347-.425h.016A2.952 2.952 0 0 1 12.3 15a2.085 2.085 0 0 0 .171-.906 1.181 1.181 0 0 0-.069-.5c-.054-.1.03-.14.123-.035a.977.977 0 0 0-.079-.239v-.008s.053-.069.078-.095a1.434 1.434 0 0 1 .216-.176 10 10 0 0 1 1.469-.749c.416-.181.759-.32.83-.36a2.287 2.287 0 0 0 .294-.226 1.973 1.973 0 0 0 .661-1.143 1.593 1.593 0 0 0 .017-.178v-.105c-.06-.226-.449-.395-2.483-.586a1.778 1.778 0 0 1-1.454-1.364v.009c-.029.075-.055.15-.081.225.026-.075.052-.151.081-.225v-.016a5.153 5.153 0 0 1 1.992-2.474c.052-.043-.208.011-.156-.032a5.18 5.18 0 0 1 .532-.225c.091-.039-.391-.222-.818-.178a2.2 2.2 0 0 0-.758.178c.1-.086.4-.2.33-.2a4.877 4.877 0 0 0-1.547.585.315.315 0 0 1 .03-.14 2.407 2.407 0 0 0-.966.746 1.286 1.286 0 0 0 .01-.174 2.883 2.883 0 0 0-.475.446l-.009.007a6.3 6.3 0 0 0-3.527-.3l-.01-.009h.012a2.953 2.953 0 0 1-.627-.7l-.008-.012-.014-.021a5.48 5.48 0 0 1-.237-.388 5.527 5.527 0 0 1-.176-.339c0-.008-.009-.011-.013-.012-.024 0-.041.111-.061.082v-.006a4.322 4.322 0 0 1-.309-1.697l-.016.008a1.89 1.89 0 0 0-.716.937c-.062.137-.1.213-.14.288v-.035c.009-.069.039-.212.032-.2s-.013.019-.02.029a1.741 1.741 0 0 0-.252.373 2.366 2.366 0 0 0-.15.383c-.006.021 0-.018 0-.064s.009-.128 0-.111l-.022.043a9.526 9.526 0 0 0-.806 3.044 3.038 3.038 0 0 0-.023.54v.016a6.641 6.641 0 0 0-.819 1.1 15.65 15.65 0 0 0-1.731 4.246 10.379 10.379 0 0 1 .928-1.626 15.041 15.041 0 0 0-1.049 5.514 14.264 14.264 0 0 1 .451-1.634 13.848 13.848 0 0 0 1.25 6.489 15.08 15.08 0 0 0 5.968 6.768 13.057 13.057 0 0 0 3.478 1.667c.163.059.327.117.5.173-.053-.021-.1-.044-.153-.067a15.752 15.752 0 0 0 4.506.659c5.41 0 7.2-2.06 7.36-2.266a2.739 2.739 0 0 0 .639-.858q.156-.064.316-.137l.067-.03.122-.057a11.349 11.349 0 0 0 2.284-1.43 5.511 5.511 0 0 0 2.129-3.11 1.944 1.944 0 0 0 .029-1.433q.083-.132.171-.28a12.743 12.743 0 0 0 1.913-6.2v-.184a7.76 7.76 0 0 0-.115-1.388z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M8,15a7,7,0,1,1,7-7A7,7,0,0,1,8,15ZM8,2.5A5.5,5.5,0,1,0,13.5,8,5.5,5.5,0,0,0,8,2.5ZM7,7H9v5H7V7ZM7,4H9V6H7V4Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M8.25,15A6.75,6.75,0,1,1,15,8.25,6.75,6.75,0,0,1,8.25,15Zm0-12A5.25,5.25,0,1,0,13.5,8.25,5.25,5.25,0,0,0,8.25,3ZM7.5,7.5H9v4H7.5v-4ZM7.5,5H9V6.5H7.5V5Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M12.7,7h-0.92A28.263,28.263,0,0,0,12,3.5l-4.5-2V7.153A0.986,0.986,0,0,0,7,8v5.794A7.942,7.942,0,0,1,3.554,11C1.994,8.647,2,2.5,2,2.5L7.531,0,13,2.5A34.541,34.541,0,0,1,12.7,7Z"/>
	<path fill="#cc4f40" fill-opacity="context-fill-opacity" d="M14,15H9a1,1,0,0,1-1-1V9A1,1,0,0,1,9,8h5a1,1,0,0,1,1,1v5A1,1,0,0,1,14,15Zm-1.139-2.846-0.646-.646,0.646-.646-0.008-.008A0.5,0.5,0,0,0,12.5,10a0.491,0.491,0,0,0-.386.194l-0.606.606-0.646-.646,0,0a0.5,0.5,0,1,0-.7.7l0,0L10.8,11.508l-0.606.606A0.491,0.491,0,0,0,10,12.5a0.5,0.5,0,0,0,.854.354l0.008,0.008,0.646-.646,0.646,0.646,0,0a0.5,0.5,0,0,0,.7-0.7Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
	<path fill="context-fill" fill-opacity="context-fill-opacity" d="M13.152,7.5H12.22a25.187,25.187,0,0,0,.28-3.75L8.025,2,8.012,7.645A0.987,0.987,0,0,0,7.5,8.5v5.309A8,8,0,0,1,4,11C2.431,8.647,2.5,3,2.5,3L8,1l5.5,2A29.211,29.211,0,0,1,13.152,7.5Z"/>
	<path fill="#cc4f40" fill-opacity="context-fill-opacity" d="M14.5,15.5h-5a1,1,0,0,1-1-1v-5a1,1,0,0,1,1-1h5a1,1,0,0,1,1,1v5A1,1,0,0,1,14.5,15.5Zm-1.139-2.846-0.646-.646,0.646-.646-0.008-.008A0.5,0.5,0,0,0,13,10.5a0.491,0.491,0,0,0-.386.194l-0.606.606-0.646-.646,0,0a0.5,0.5,0,1,0-.7.7l0,0L11.3,12.008l-0.606.606A0.491,0.491,0,0,0,10.5,13a0.5,0.5,0,0,0,.854.354l0.008,0.008,0.646-.646,0.646,0.646,0,0a0.5,0.5,0,0,0,.7-0.7Z"/>
</svg>
.urlbarView-body-inner
{
	border: 0 !important;
}

.urlbarView-row
{
	padding: 0 !important;
	border-radius: 0 !important;
}

.urlbarView-row-inner
{
	align-items: center !important;
	position: relative !important;
	padding: 12px 0 !important;
	padding-inline: 48px 16px !important;
	max-width: 100% !important;
	width: auto !important;
}

.urlbarView-no-wrap
{
	height: 100% !important;
	align-items: center !important;
	margin-inline-start: 0 !important;
	max-width: 100% !important;
}

.urlbarView-row[has-url][type]:not([type=bookmark]) .urlbarView-row-inner
{
	flex-direction: column !important;
	align-items: flex-start !important;
}

.urlbarView-row[has-url][type]:not([type=bookmark]) .urlbarView-no-wrap
{
	height: auto !important;
	align-self: flex-start !important;
	width: 100% !important;
}

.urlbarView-title-separator
{
	visibility: visible !important;
}

.urlbarView-title-separator::before
{
	content: "-" !important;
	color: inherit !important;
}

.urlbarView-title-separator,
.urlbarView-action
{
	opacity: .6 !important;
	color: inherit !important;
	font-size: revert !important;
}

.urlbarView-favicon
{
	position: absolute !important;
	left: 16px !important;
	top: 50% !important;
	transform: translateY(-50%) !important;
	fill-opacity: .6 !important;
	flex: unset !important;
	margin: unset !important;
}

.urlbarView-title,
.urlbarView-url
{
	text-overflow: ellipsis !important;
	mask-image: none !important;
}

.urlbarView-title
{
	flex: 1 !important;
	flex-basis: unset !important;
}

.urlbarView-url
{
	max-width: 100% !important;
	font-size: revert !important;
	color: var(--urlbar-popup-url-color) !important;
	padding: 0 !important;
}

.urlbarView-url:not(:empty)
{
	visibility: visible !important;
}

.urlbarView-action:not(:empty)
{
    display: flex !important;
}

.urlbarView-row[type=search] ~ .urlbarView-row[type=search] .urlbarView-title-separator,
.urlbarView-row[type=search] ~ .urlbarView-row[type=search] .urlbarView-action,
.urlbarView-row[has-url][type]:not([type=bookmark]) .urlbarView-title-separator,
.urlbarView-type-icon
{
	display: none !important;
}

.urlbarView-row[type=tip] > .urlbarView-row-inner > .urlbarView-favicon,
.urlbarView-row[type=tip] > .urlbarView-row-inner > .urlbarView-title,
.urlbarView-tip-button,
.urlbarView-tip-help
{
	margin-block-end: 0 !important;
}

.urlbarView .search-one-offs
{
	padding: 4px !important;
	display: flex !important;
}

.urlbarView .search-panel-one-offs-header
{
	margin-inline-start: 12px !important;
	line-height: 40px !important;
	transform: translateY(-1px) !important;
}

.urlbarView .search-panel-one-offs
{
	border-radius: 0 0 8px 8px !important;
	display: inline-flex !important;
	flex-direction: row !important;
	padding: 0 !important;
	margin: 0 !important;
	float: left !important;
	max-width: none !important;
	height: auto !important;
}

.urlbarView .searchbar-engine-one-off-item
{
	height: 32px !important;
	border-radius: 16px !important;
	padding: 0 8px !important;
	margin: 4px !important;
	background-image: none !important;
	color: inherit !important;
	border: 0 !important;
}

#urlbar-container
{
	max-height: 36px !important;
}

#urlbar,
#searchbar
{
	font-size: var(--toolbar-field-fontsize) !important;
	border-radius: 99px !important;
	background-color: var(--toolbar-field-background-color) !important;
	border: 0 !important;
	box-shadow: none !important;
	padding: 2px !important;
	background-clip: padding-box !important;
	transition: background-color .1s var(--ease-basic) !important;
	-moz-box-align: center !important;
	display: block !important;
	margin: 0 !important;
	top: 0 !important;
	color: inherit !important;
	
	--urlbar-height: 28px !important;
	
	min-height: var(--urlbar-height) !important;
}

#urlbar:not(:-moz-lwtheme),
#searchbar:not(:-moz-lwtheme)
{
	color: inherit !important;
}

#urlbar:hover,
#searchbar:hover
{
	background-color: var(--toolbar-field-hover-background-color) !important;
}

#urlbar[focused],
#urlbar[open],
#searchbar[focused],
#searchbar[open]
{
	background-color: var(--toolbar-field-focus-background-color) !important;
}

#urlbar[focused],
#searchbar[focused]
{
	/* -moz-accent-color won't work in a var... */
	border: 2px solid -moz-accent-color !important;
	padding: 0 !important;
	margin: 0 !important;
}

/* if only focused, don't break out the urlbar */
#urlbar[focused]:not([open])
{
	width: 100% !important;
	left: 0 !important;
	right: 0 !important;
}

#urlbar[open],
#searchbar[open]
{
	border: 0 !important;
	padding: 0 !important;
	border-radius: 8px !important;
	min-height: 37px !important;
	margin: -4px 0 -1px !important;
	box-shadow:
		0 5px 5px -3px rgba(0,0,0,.2),
		0 8px 10px 1px rgba(0,0,0,.14),
		0 3px 14px 2px rgba(0,0,0,.12) !important;
	z-index: 99999 !important;
}

/* 71+ */
#urlbar-background
{
	display: none !important;
}

#urlbar-input-container,
#searchbar
{
	display: flex !important;
	align-items: center !important;
}

#urlbar-input-container
{
	height: auto !important;
	padding: 0 !important;
	border: 0 !important;
}

.urlbar-input-box,
.searchbar-textbox
{
	flex: 1 !important;
}

#urlbar-input
{
	width: 100% !important;
}

#urlbar-input,
.searchbar-textbox
{
	color: var(--lwt-toolbar-field-color) !important;
}

#urlbar-input::placeholder,
.searchbar-textbox::placeholder
{
	opacity: .66 !important;
}

#urlbar[open] #urlbar-input-container
{
	height: 37px !important;
}

#urlbar[open] #identity-box
{
	margin-inline-start: 8px !important;
	margin-inline-end: 8px !important;
}

.urlbarView
{
	top: 0 !important;
	left: 0 !important;
	right: 0 !important;
	position: relative !important;
	box-shadow: none !important;
	border: 0 !important;
	background: 0 !important;
	margin: 0 !important;
}

#urlbar[open] .urlbarView
{
	display: block !important;
	width: 100% !important;
}

#urlbar-results
{
	padding: 0 !important;
}

#urlbar-container,
#searchbar-container
{
	overflow: visible !important;
	padding: 0 !important;
	margin-inline: 6px !important;
}

#identity-icon
{
	fill-opacity: 1 !important;
}

#identity-icon-labels
{
	color: inherit !important;
	opacity: 1 !important;
	padding: 0 !important;
	margin-inline-start: 8px !important;
	margin-block-start: -1px !important;
	display: none !important;
	align-items: center !important;
}

#identity-box
{
	display: flex !important;
	position: relative !important;
}

.identity-box-button,
#tracking-protection-icon-container
{
	background: none !important;
}

#identity-icon-box
{
	border: 0 !important;
	padding: 0 !important;
}

/* separator */
#urlbar[pageproxystate=valid] #identity-box:-moz-any(.chromeUI, .extensionPage, .notSecureText) #identity-icon-box::after
{
	content: "" !important;
	display: block !important;
	position: absolute !important;
	height: 16px !important;
	top: calc(50% - 8px) !important;
	right: 0 !important;
	background: #9d9e9f !important;
	width: 1px !important;
	transition: opacity .2s var(--ease-basic) !important;
}


#urlbar[pageproxystate=valid] #identity-box:-moz-any(.chromeUI, .extensionPage, .notSecureText)
{
	margin-inline-end: 8px !important;
}


#urlbar[pageproxystate=valid] #identity-box:hover  #identity-icon-box::after
{
	opacity: 0 !important;
}

#urlbar[pageproxystate=valid] #identity-box.extensionPage > #identity-icon
{
	list-style-image: none !important;
	margin-inline-end: -16px !important;
}

#identity-icon-label
{
	margin: 0 !important;
	padding-inline-start: 8px !important;
	transform: translateY(-1px) !important;
	
}

/* no longer used as a connection icon */
#connection-icon
{
	display: none !important;
}

#urlbar[pageproxystate=valid] #identity-box:-moz-any(
	.mixedActiveBlocked,
	.mixedDisplayContentLoadedActiveBlocked,
	.mixedActiveContent
) > #connection-icon
{
	display: -moz-box !important;
}

#urlbar[pageproxystate=valid] #identity-box:-moz-any(.weakCipher, .certUserOverridden, .certErrorPage, .insecureLoginForms, .mixedActiveContent)
{
	color: #c94031 !important;
}

.urlbar-input
{
	padding: 0 !important;
}

.searchbar-textbox
{
	border: 0 !important;
	background: 0 !important;
	box-shadow: none !important;
	margin: 0 !important;
	-moz-appearance: none !important;
}

.searchbar-search-icon
{
	margin: 0 !important;
	fill-opacity: 1 !important;
}

.searchbar-search-icon-overlay
{
	margin-inline-start: -11px !important;
	margin-inline-end: 0 !important;
	
	/* not really useful anyway */
	display: none !important;
}

#page-action-buttons > #pageActionSeparator,
.urlbar-history-dropmarker
{
	display: none !important;
}

#pageActionSeparator
{
	height: 24px !important;
}

.urlbar-icon-wrapper
{
	background: 0 !important;
}

.urlbar-icon,
#page-action-buttons > toolbarbutton,
.searchbar-search-button,
#identity-box,
#tracking-protection-icon-box
{
	min-width: 32px !important;
	height: 24px !important;
	padding: 4px 8px !important;
	margin: 0 !important;
	border: 0 !important;
	border-radius: 99px !important;
	fill-opacity: 1 !important;
	transition: background-color .2s var(--ease-basic) !important;
	background: 0 !important;
	background-color: transparent !important;
}

#tracking-protection-icon-container
{
	background: 0 !important;
	border: 0 !important;
	margin: 0 !important;
	padding: 0 !important;
	width: auto !important;
	order: 97 !important;
}

#tracking-protection-icon-box
{
	display: block !important;
	width: 32px !important;
}

#tracking-protection-icon-box
{
	transition:
		background-color .2s var(--ease-basic),
		width .15s var(--ease-in),
		opacity .15s var(--ease-basic),
		visibility 0s .2s !important;
	opacity: 0 !important;
	visibility: hidden !important;
	width: 0 !important;
}

#urlbar:hover #tracking-protection-icon-box,
#tracking-protection-icon-container[open] #tracking-protection-icon-box,
#tracking-protection-icon-box[active]
{
	transition:
		background-color .2s var(--ease-basic),
		width .3s var(--ease-out),
		opacity .3s var(--ease-basic) !important;
	opacity: 1 !important;
	visibility: visible !important;
	width: 32px !important;
}

#tracking-protection-icon
{
	display: block !important;
}

#tracking-protection-icon-animatable-box
{
	display: none !important;
}

#identity-box #notification-popup-box
{
	padding: 0 !important;
	margin: 0 !important;
}

#page-action-buttons
{
	height: 24px !important;
	display: flex !important;
	order: 99 !important;
}

#urlbar[open] #page-action-buttons,
#urlbar[open] #tracking-protection-icon-container
{
	display: none !important;
}

.urlbar-icon:hover:not([disabled]),
#page-action-buttons > toolbarbutton:hover:not([disabled]),
.searchbar-search-button:hover,
#identity-box:hover:not(.no-hover),
#tracking-protection-icon-container:hover #tracking-protection-icon-box
{
	background-color: var(--toolbarbutton-hover-background) !important;
}

.urlbar-icon:hover:active:not([disabled]),
.urlbar-icon[open],
#page-action-buttons > toolbarbutton:hover:active:not([disabled]),
.searchbar-search-button:hover:active,
#identity-box:hover:active:not(.no-hover),
#identity-box[open]:not(.no-hover),
#tracking-protection-icon-container[open] #tracking-protection-icon-box
{
	background-color: var(--toolbarbutton-active-background) !important;
	transition-duration: 0 !important;
}

#contextual-feature-recommendation
{
	width: auto !important;
}

.urlbar-page-action:-moz-any(
	#reader-mode-button,
	#pageActionButton,
	#pocket-button-box,
	#pageAction-urlbar-screenshots_mozilla_org,
	#pageAction-urlbar-sendToDevice,
	#pageAction-urlbar-emailLink,
	#pageAction-urlbar-copyURL,
	#pageAction-urlbar-shareURL,
	#pageAction-urlbar-addSearchEngine
)
{
	transition:
		background-color .2s var(--ease-basic),
		margin .15s var(--ease-in),
		opacity .15s var(--ease-basic),
		visibility 0s 4s !important;
	opacity: 0 !important;
	visibility: hidden !important;
	margin-inline-start: -32px !important;
}

#urlbar:hover .urlbar-page-action:-moz-any(
	#reader-mode-button,
	#pageActionButton,
	#pocket-button-box,
	#pageAction-urlbar-screenshots_mozilla_org,
	#pageAction-urlbar-sendToDevice,
	#pageAction-urlbar-emailLink,
	#pageAction-urlbar-copyURL,
	#pageAction-urlbar-shareURL,
	#pageAction-urlbar-addSearchEngine
),
.urlbar-page-action:-moz-any(
	#reader-mode-button,
	#pageActionButton,
	#pocket-button-box,
	#pageAction-urlbar-screenshots_mozilla_org,
	#pageAction-urlbar-sendToDevice,
	#pageAction-urlbar-emailLink,
	#pageAction-urlbar-copyURL,
	#pageAction-urlbar-shareURL,
	#pageAction-urlbar-addSearchEngine
):-moz-any(:hover, [open], [readeractive]),
.urlbar-page-action:-moz-any(:hover, [open]) ~ .urlbar-page-action:-moz-any(
	#reader-mode-button,
	#pageActionButton,
	#pocket-button-box,
	#pageAction-urlbar-screenshots_mozilla_org,
	#pageAction-urlbar-sendToDevice,
	#pageAction-urlbar-emailLink,
	#pageAction-urlbar-copyURL,
	#pageAction-urlbar-shareURL,
	#pageAction-urlbar-addSearchEngine),
#tracking-protection-icon-container[open] ~ #page-action-buttons .urlbar-page-action:-moz-any(
	#reader-mode-button,
	#pageActionButton,
	#pocket-button-box,
	#pageAction-urlbar-screenshots_mozilla_org,
	#pageAction-urlbar-sendToDevice,
	#pageAction-urlbar-emailLink,
	#pageAction-urlbar-copyURL,
	#pageAction-urlbar-shareURL,
	#pageAction-urlbar-addSearchEngine)
{
	transition:
		background-color .2s var(--ease-basic),
		margin .3s var(--ease-out),
		opacity .3s var(--ease-basic) !important;
	opacity: 1 !important;
	visibility: visible !important;
	margin-inline-start: 0 !important;
}

.urlbar-display
{
	color: inherit !important;
	margin: 0 !important;
}

#pocket-button-box[animate] > #pocket-animatable-box
{
	margin-inline-start: 6px !important;
}

#star-button-animatable-box
{
	display: none !important;
}

#userContext-icons
{
	flex-direction: row !important;
	align-items: center !important;
	justify-content: center !important;
	position: relative !important;
	padding-inline: 8px !important;
	margin: 0 !important;
	color: var(--identity-tab-color) !important;
}

#userContext-icons:not([hidden])
{
	display: flex !important;
}

#userContext-icons > *
{
	z-index: 1 !important;
	color: inherit !important;
	fill: currentColor !important;
}

#userContext-label
{
	margin: 0 !important;
}

#userContext-label + #userContext-indicator
{
	margin-inline-start: 6px !important;
}

#userContext-icons::before
{
	content: '' !important;
	position: absolute !important;
	left: 0 !important;
	right: 0 !important;
	top: 0 !important;
	bottom: 0 !important;
	background: var(--identity-tab-color) !important;
	border-radius: 99px !important;
	opacity: .1 !important;
}

#cfr-label-container
{
	background: 0 !important;
}

#cfr-label
{
	display: none !important;
}

#cfr-button
{
	fill: currentColor !important;
}

#urlbar[open] #urlbar-go-button,
#searchbar[open] #search-go-button,
/* < 71 */
#urlbar[open] .urlbar-go-button,
#searchbar[open] .search-go-button
{
	height: 32px !important;
}

#urlbar-go-button,
#search-go-button,
/* < 71 */
.urlbar-go-button,
.search-go-button
{
	list-style-image: url(go.svg) !important;
}

.search-go-container
{
	display: flex !important;
}

#searchbar .textbox-input
{
	padding: 0 !important;
}

/* lock (secure) */
#identity-box[pageproxystate="valid"].verifiedDomain #identity-icon,
#identity-box[pageproxystate="valid"].mixedActiveBlocked #identity-icon
{
	list-style-image: var(--lock-icon) !important;
}

/* warning (dangerous) */
#identity-box[pageproxystate="valid"].notSecure #identity-icon,
#identity-box[pageproxystate="valid"].mixedActiveContent #identity-icon,
#identity-box[pageproxystate="valid"].httpsOnlyErrorPage #identity-icon,
#identity-box[pageproxystate="valid"].weakCipher #identity-icon,
#identity-box[pageproxystate="valid"].mixedDisplayContent #identity-icon,
#identity-box[pageproxystate="valid"].mixedDisplayContentLoadedActiveBlocked #identity-icon,
#identity-box[pageproxystate="valid"].certUserOverridden #identity-icon,
#identity-box[pageproxystate="valid"].certErrorPage #identity-icon
{
    list-style-image: var(--warning-icon) !important;
}

#urlbar-input,
#searchbar-input,
.searchbar-textbox /* < 71 */
{
	transform: translateY(var(--input-offset)) !important;
	line-height: 1.745em !important;
}

/* 1x */
#urlbar
{
	--info-icon: url(info-1x.svg);
	--lock-icon: url(connection-1x.svg);
	--warning-icon: url(connection-warning-1x.svg);
}

#urlbar,
#searchbar
{
	--input-offset: -1px;
}

/* 2x */
@media (min--moz-device-pixel-ratio: 2)
{
	#urlbar
	{
		--info-icon: url(info-2x.svg);
		--lock-icon: url(connection-2x.svg);
		--warning-icon: url(connection-warning-2x.svg);
	}
	
	#urlbar,
	#searchbar
	{
		--input-offset: -.5px;
	}
	
	#identity-icon-labels
	{
		padding-inline-end: .5px !important;
	}
}
@import "global/variables.css";
@import "global/global.css";
@import "icons/icons.css";
@import "tabbar/tabbar.css";
@import "navbar/navbar.css";
@import "personalbar/personalbar.css";
@import "popup/popup.css";
@import "urlbar/urlbar.css";
@import "findbar/findbar.css";
/*** MaterialFox ***/
/**  Mandatory	  **/
user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true); // default is false
user_pref("svg.context-properties.content.enabled", true); 				// default is false

/**  Recommended  (uncomment to apply) **/
/* Replicate Chrome behaviour for clipped tabs */
//user_pref("browser.tabs.tabClipWidth", 83); 							// default is 140

/* Replicate Chrome's "Not Secure" text on HTTP */
//user_pref("security.insecure_connection_text.enabled", true);
# These are supported funding model platforms
ko_fi: miguelravila
<div align="center">
<h1>SimplerentFox</h1>
<b>🦊 Linux Version</b>
</div>

![](../Images/Header.png)

## Installation

1. In the searchbar type `about:config`. A dialog will be shown to you. Press the **I accept the risk** button.
2. Search for **`toolkit.legacyUserProfileCustomizations.stylesheets`**, **`layers.acceleration.force-enabled`**, **`gfx.webrender.all`**, and **`svg.context-properties.content.enabled`**. Change them to **True**
3. Go to your Firefox profile: `$HOME/.mozilla/firefox/XXXXXXX.default-release/`
4. Create a folder and name it **`chrome`** (with lowercase).
5. You can choose between the three versions: **HideURLBar**, **WithURLBar** and **OneLine** (You can see them here: [Versions](#three-versions)). Once you choose your version  and rename the file you choose into `userChrome.css` 
6. Then paste the `userChrome.css` and the `userContent.css` files into the folder
7. (Optional) If you don't know if use a theme or not, you can use the Add-on theme I made for a better experience: [Add-on Theme](https://addons.mozilla.org/en-US/firefox/addon/simplerentfox/)
 
> E N J O Y 🦊

### Linux considerations

Firefox uses the GTK3 theme you have to design the menus and the colors. The theme in the screenshots will not look like yours. It can also be useful if you don't want to change a lot of code in the URL-Bar or menus. You can find a lot of GTK themes [here](https://www.pling.com/browse/cat/135/order/latest/)
/* 
  ___ _            _                 _   
 / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
 \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
 |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
 | __|____ __|_|                         
 | _/ _ \ \ /                            
 |_|\___/_\_\                            
                                         
 Hide URL Bar Version

*/

/* 

V A R I A B L E S 

Here you can change the opacity level and the main color of the browser. Also the color of the selections (When you select text)

*/

:root {
    --srf-color-primary: rgba(20, 20, 20, 0.6);
    --srf-color-selection: #66a6d9;
}

/* 
G E N E R A L 
*/

window,
#main-window,
#toolbar-menubar,
#TabsToolbar,
#PersonalToolbar,
#navigator-toolbox,
#sidebar-box,
#nav-bar {
    background-color: var(--srf-color-primary) !important;
    -moz-appearance: none !important;
    background-image: none !important;
    border: none !important;
    box-shadow: none !important;
}

::selection {
    background-color: var(--srf-color-selection);
    /* Text color of your selection */
    color: #141414;
}

/* 
T A B S 
*/

:root {
    --tabs-border: transparent !important;
}

.tab-background {
    border: none !important;
}

.tab-background[selected='true'] {
    -moz-appearance: none !important;
    background-image: none !important;
}

.tabbrowser-tabs {
    border: none !important;
    opacity: 0 !important;
}

.tabbrowser-tab::before,
.tabbrowser-tab::after {
    opacity: 0 !important;
    border-left: none !important;
}

.titlebar-placeholder {
    border: none !important;
}

.tab-line {
    display: none !important;
}

/* 

B U T T O N S 

Here you can comment if you want your buttons to be showed up!

*/

/* This one is the forward button  */
#forward-button {
    display: none !important;
}

/* This one is the back button  */
#back-button {
    display: none !important;
}

/* This one is the spacer between tabs and border  */
.titlebar-spacer {
    display: none !important;
}

/* This one is the "shield" icon  */
#tracking-protection-icon-container {
    display: none !important;
}

/* This one is the hamburger menu!  */
#PanelUI-button {
    display: none !important;
}

/* This one removes the close button  */
.tabbrowser-tab:not([pinned]) .tab-close-button {
    display: none !important;
}

/* This one clears the tabs icon when they're not pinned  */
.tabbrowser-tab:not([pinned]) .tab-icon-image {
    display: none !important;
}

/* Removing the zoom button  */
#urlbar-zoom-button {
    display: none !important;
}

/* Page action (right three dots button)  */
#pageActionButton {
    display: none !important;
}

/* 
N A V B A R
*/

#navigator-toolbox::after {
    border-bottom: 0px !important;
    border-top: 0px !important;
}

#nav-bar {
    border: none !important;
    box-shadow: none !important;
    margin-top: 0px !important;
    border-top-width: 0px !important;
    margin-bottom: 0px !important;
    border-bottom-width: 0px !important;
}

/* 
S I D E - B A R
*/

#history-panel,
#sidebar-search-container,
#bookmarksPanel {
    background: var(--srf-color-primary) !important;
}

#search-box {
    -moz-appearance: none !important;
    background: var(--srf-color-primary) !important;
    border-radius: 6px !important;
}

#sidebar-icon {
    display: none !important;
}

.sidebar-placesTree {
    color: #eee !important;
}

#sidebar-switcher-target {
    color: #eee !important;
}

#sidebar-header {
    background: var(--srf-color-primary) !important;
}

#sidebar-splitter {
    border: none !important;
    opacity: 1 !important;
    background-color: var(--srf-color-primary) !important;
}

splitter#sidebar-splitter {
    border: none !important;
}

/* 
U R L - B A R
*/

#urlbar[breakout] {
    margin-inline-start: 0px !important;
    width: 100% !important;
    left: 0 !important;
    top: calc(
        (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
    ) !important;
}

@-moz-document url(chrome://browser/content/browser.xhtml)
{
    #urlbar[breakout][breakout-extend] > #urlbar-background {
        animation-name: none !important;
        box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05) !important;
    }
}

#urlbar[breakout]:not([open]) {
    bottom: calc(
        (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
    ) !important;
}

#urlbar-background {
    background: transparent !important;
    border: transparent !important;
    animation: none !important;
}

#urlbar-input-container {
    padding: 0 !important;
    height: 100% !important;
}

#identity-box {
    display: none;
}

#urlbar .urlbarView {
    display: none !important;
}

#searchbar {
    background: var(--bf-bg) !important;
    border: none !important;
}

.searchbar-textbox {
    color: var(--bf-focus-fg) !important;
    font-weight: 700 !important;
}

#urlbar-label-box, #urlbar-search-mode-indicator {
    display: none !important;
}

/* 
Designed and Coded by Miguel R. Ávila: https://github.com/MiguelRAvila
*/
/* 
  ___ _            _                 _   
 / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
 \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
 |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
 | __|____ __|_|                         
 | _/ _ \ \ /                            
 |_|\___/_\_\                            
                                         
 One Line Version

*/
 
/* 

V A R I A B L E S 

Here you can change the opacity level and the main color of the browser. Also the color of the selections (When you select text)

*/

:root {
    --urlbar-width:25vw;
    --srf-color-primary: rgba(20, 20, 20, 0.6);
    --srf-color-selection: #66a6d9;
}

/* 
G E N E R A L 
*/

window,
#main-window,
#toolbar-menubar,
#TabsToolbar,
#PersonalToolbar,
#navigator-toolbox,
#sidebar-box,
#nav-bar {
    background-color: var(--srf-color-primary) !important;
    -moz-appearance: none !important;
    background-image: none !important;
    border: none !important;
    box-shadow: none !important;
}

::selection {
    background-color: var(--srf-color-selection);
    /* Text color of your selection */
    color: #141414;
}

/* 
T A B S 
*/

:root {
    --tabs-border: transparent !important;
    --toolbox-border-bottom-color: transparent !important;
}

#navigator-toolbox {
    border: 0px !important;
}

#TabsToolbar {
    margin-top: -1px !important;
    margin-bottom: 1px !important;
    margin-left: var(--urlbar-width) !important;
    max-height: 32px !important;
}

.tab-background {
    border: none !important;
}

.tab-background[selected='true'] {
    -moz-appearance: none !important;
    background-image: none !important;
}

.tabbrowser-tabs {
    border: none !important;
    opacity: 0 !important;
}

.tabbrowser-tab::before {
    opacity: 0 !important;
    border-left: none !important;
}

.tabbrowser-tab::after {
    opacity: 0 !important;
    border-left: none !important;
}

.titlebar-placeholder {
    border: none !important;
}

.tab-line {
    display: none !important;
}

.tabbrowser-arrowscrollbox {
    height: 32px !important;
    min-height: 32px !important;
}

.titlebar-placeholder[type='pre-tabs'] {
    display: none !important;
}

/* 

B U T T O N S 

Here you can comment if you want your buttons to be showed up!

*/

/* This one is the forward button  */
#forward-button {
    display: none !important;
}

/* This one is the back button  */
#back-button {
    display: none !important;
}

/* This one is the spacer between tabs and border  */
.titlebar-spacer {
    display: none !important;
}

/* This one is the "shield" icon  */
#tracking-protection-icon-container {
    display: none !important;
}

/* This one is the hamburger menu!  */
#PanelUI-button {
    display: none !important;
}

/* This one removes the close button  */
.tabbrowser-tab:not([pinned]) .tab-close-button {
    display: none !important;
}

/* This one clears the tabs icon when they're not pinned  */
.tabbrowser-tab:not([pinned]) .tab-icon-image {
    display: none !important;
}

/* Removing the zoom button  */
#urlbar-zoom-button {
    display: none !important;
}

/* Page action (right three dash button)  */
#pageActionButton {
    display: none !important;
}

/* 
N A V B A R
*/

#navigator-toolbox::after {
    border: 0px !important;
}

#nav-bar {
    border: none !important;
    box-shadow: none !important;
    border-top-width: 0px !important;
    border-bottom-width: 0px !important;
    margin-top: -32px !important;
    margin-bottom: -1px !important;
    margin-right: calc(100vw - var(--urlbar-width)) !important;
    height: 32px !important;
}

/* 
S I D E - B A R
*/

#history-panel,
#sidebar-search-container,
#bookmarksPanel {
    background: var(--srf-color-primary) !important;
}

#search-box {
    -moz-appearance: none !important;
    background: var(--srf-color-primary) !important;
    border-radius: 6px !important;
}

#sidebar-icon {
    display: none !important;
}

.sidebar-placesTree {
    color: var(--srf-color-selection) !important;
}

#sidebar-switcher-target {
    color: var(--srf-color-selection) !important;
}

#sidebar-header {
    background: var(--srf-color-primary) !important;
}

#sidebar-splitter {
    border: none !important;
    opacity: 1 !important;
    background-color: var(--srf-color-primary) !important;
}

splitter#sidebar-splitter {
    border: none !important;
}

/* 
U R L - B A R
*/

#urlbar[breakout] {
    margin-inline-start: 0px !important;
    width: 100% !important;
    left: 0 !important;
    top: calc(
        (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
    ) !important;
}

@-moz-document url(chrome://browser/content/browser.xhtml)
{
    #urlbar[breakout][breakout-extend] > #urlbar-background {
        animation-name: none !important;
        box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05) !important;
    }
}

#urlbar[breakout]:not([open]) {
    bottom: calc(
        (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
    ) !important;
}

#urlbar[breakout-extend] {
    top: calc(
        (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
    ) !important;
    left: calc(
        (var(--urlbar-toolbar-width) - var(--urlbar-width)) / 2
    ) !important;
    width: 100% !important;
}

#urlbar[breakout-extend] #urlbar-input-container {
    height: calc(var(--tab-min-height) - 3px) !important;
    padding: 0px !important;
}

#urlbar-input-container {
    padding: 0 !important;
    height: 100% !important;
}

#identity-box {
    display: none;
}

.urlbarView-row {
    padding: 0px !important;
}

#searchbar {
    background: var(--bf-bg) !important;
    border: none !important;
}

.searchbar-textbox {
    color: var(--bf-focus-fg) !important;
    font-weight: 700 !important;
}

#urlbar-container {
    min-width: 150px !important;
}

#urlbar {
    border: none !important;
    box-shadow: none !important;
}

#page-action-buttons {
    display: none !important;
}

.urlbarView {
    margin: 0px !important;
    width: 100% !important;
}

#urlbar-label-box, #urlbar-search-mode-indicator {
    display: none !important;
}

/* 
If you want to show the URLBar extended you must
change this two blocks:

In order to show it, you must change the value of "transparent" in the first block 
and erease the second block. 
*/

#urlbar-background {
    background: transparent !important;
    border: transparent !important;
    animation: none !important;
}

#urlbar .urlbarView {
    display: none !important;
}

/* 
Designed and Coded by Miguel R. Ávila: https://github.com/MiguelRAvila
*/
/* 
  ___ _            _                 _   
 / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
 \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
 |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
 | __|____ __|_|                         
 | _/ _ \ \ /                            
 |_|\___/_\_\                            
                                         
 With URL Bar Version

*/

/* 

V A R I A B L E S 

Here you can change the opacity level and the main color of the browser. Also the color of the selections (When you select text)

*/

:root {
    --srf-color-primary: rgba(20, 20, 20, 0.6);
    --srf-color-selection: #66A6D9;
    --srf-color-urlbar: rgba(20, 20, 20, 0.8);
}

/* 
G E N E R A L 
*/

window,
#main-window,
#toolbar-menubar,
#TabsToolbar,
#PersonalToolbar,
#navigator-toolbox,
#sidebar-box {
    background-color: var(--srf-color-primary) !important;
    -moz-appearance: none !important;
    background-image: none !important;
    border: none !important;
    box-shadow: none !important;
}

::selection {
    background-color: var(--srf-color-selection);
    color: #141414;
}

/* 
T A B S 
*/

:root {
    --tabs-border: transparent !important;
}

.tab-background {
    border: none !important;
}

.tab-background[selected='true'] {
    -moz-appearance: none !important;
    background-image: none !important;
}
.tabbrowser-tabs {
    border: none !important;
    opacity: 0 !important;
}

.tabbrowser-tab::before, .tabbrowser-tab::after{
    opacity: 0 !important;
    border-left: none !important;
}

.titlebar-placeholder {
    border: none !important;
}

.tab-line {
  display: none !important;
}

/* 

B U T T O N S 

Here you can comment if you want your buttons to be showed up!

*/

/* This one is the forward button  */
#forward-button {
    display: none !important;
}

/* This one is the back button  */
#back-button {
    display: none !important;
}

/* This one is the spacer between tabs and border  */
.titlebar-spacer {
    display: none !important;
}

/* This one is the "shield" icon  */
#tracking-protection-icon-container {
    display: none !important;
}

/* This one is the hamburger menu!  */
#PanelUI-button {
    display: none !important;
}

/* This one removes the close button  */
.tabbrowser-tab:not([pinned]) .tab-close-button {
    display: none !important;
}

/* This one clears the tabs icon when they're not pinned  */
.tabbrowser-tab:not([pinned]) .tab-icon-image {
    display: none !important;
}

/* Removing the zoom button  */
#urlbar-zoom-button {
    display: none !important;
}

/* Page action (right three dots button)  */
#pageActionButton {
    display: none !important;
}

/* 
N A V B A R
*/

#navigator-toolbox::after {
    border-bottom: 0px !important;
    border-top: 0px !important;
}

#nav-bar {
    border: none !important;
    box-shadow: none !important;
    margin-top: 0px !important;
    border-top-width: 0px !important;
    margin-bottom: 0px !important;
    border-bottom-width: 0px !important;
}

/* 
S I D E - B A R
*/

#history-panel,
#sidebar-search-container,
#bookmarksPanel {
    background: var(--srf-color-primary) !important;
}

#search-box {
    -moz-appearance: none !important;
    background: var(--srf-color-primary) !important;
    border-radius: 6px !important;
}

#sidebar-search-container {
    background-color: var(--srf-color-primary) !important;
}

#sidebar-icon {
    display: none !important;
}

.sidebar-placesTree {
    color: #EEE !important;
}

#sidebar-switcher-target {
    color: #EEE !important;
}

#sidebar-header {
    background: var(--srf-color-primary) !important;
}

#sidebar-box {
    --sidebar-background-color: var(--srf-color-primary) !important;
}

#sidebar-splitter {
    border: none !important;
    opacity: 1 !important;
    background-color: var(--srf-color-primary) !important;
}

/* 
U R L - B A R
*/

#urlbar[breakout] {
    margin-inline-start: 0px !important;
    width: 100% !important;
    left: 0 !important;
    top: calc(
        (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
    ) !important;
}

#urlbar[breakout]:not([open]) {
    bottom: calc(
        (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
    ) !important;
}

#urlbar-background {
    background: var(--srf-color-urlbar) !important;
    border: var(--srf-color-urlbar) !important;
    animation: none !important;
}

#urlbar-input-container {
    padding: 0 !important;
    height: 100% !important;
}

#identity-box {
    display: none;
}

#urlbar-input:focus {
    color: #fff !important;
}

.megabar[open='true'] > #urlbar-background {
    background: #fff !important;
}

.urlbarView {
    margin-inline: 0 !important;
    width: auto !important;
}

.urlbarView-row {
    padding: 0 2px !important;
}

.urlbarView-row-inner {
    padding-inline-start: 4px !important;
}

#urlbar-label-box, #urlbar-search-mode-indicator {
    display: none !important;
}

/* 
Designed and Coded by Miguel R. Ávila: https://github.com/MiguelRAvila
*/
/* 
  ___ _            _                 _   
 / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
 \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
 |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
 | __|____ __|_|                         
 | _/ _ \ \ /                            
 |_|\___/_\_\                            
                                         
*/

:root{
	scrollbar-width: none !important;
}

@-moz-document url(about:privatebrowsing) {
	:root{
		scrollbar-width: none !important;
	}
}
<div align="center">
<h1>SimplerentFox</h1>
<b>🦊 Minimalist, Simple and Keyboard Centered</b>
</div>

![](Images/Header.png)

## Index

-   [SimplerentFox](#)
    -   [Features](#features)
    -   [Installation](#installation)
    -   [Customization](#customization)
         - [All Versions](#three-versions)
         - [Own Colors and Opacity](#your-own-colors-and-opacity)
         - [Show buttons](#an-element-you-need-doesnt-show-you-can-easily-add-them-again)
    -   [Usage](#usage)
    -   [Startpage](#startpage)
    -   [Ports](#ports)

## Features

-   **As simple as you want**: All code is commented!
-   **Variables** for custom opacity and colors: [Customization](#customization)
-   Tab bar, URLBar and Sidebar **transparent**!
-   **Keyboard** Centered Design! Here you can read about the shortcuts: [Usage](#usage)

## Installation

1. In the searchbar type `about:config`. A dialog will be shown to you. Press the **I accept the risk** button.
2. Search for **`toolkit.legacyUserProfileCustomizations.stylesheets`**, **`layers.acceleration.force-enabled`**, **`gfx.webrender.all`**, and **`svg.context-properties.content.enabled`**. Change them to **True**
3. Go to your Firefox profile:
    - If you're on Linux: `$HOME/.mozilla/firefox/XXXXXXX.default-release/`
    - If you're on Windows: `C:\Users\<USERNAME>\AppData\Roaming\Mozilla\Firefox\Profiles\XXXXXXX.default-XXXXXX`
    - If you're on MacOS: `Users/<USERNAME>/Library/Application Support/Firefox/Profiles/XXXXXXX.default-XXXXXXX` 
4. Create a folder and name it **`chrome`** (with lowercase).
5. You can choose between the three versions: **HideURLBar**, **WithURLBar** and **OneLine** (You can see them here: [Versions](#three-versions)). Once you choose your version  and rename the file you choose into `userChrome.css`.
6. Then paste the `userChrome.css` and the `userContent.css` files into the folder.
7. (Optional) If you don't know if use a theme or not, you can use the Add-on theme I made for a better experience: [Add-on Theme](https://addons.mozilla.org/en-US/firefox/addon/simplerentfox)
8. Enjoy!

## Customization

-   You can choose between three files `userChrome__HideURLBar`, `userChrome__WithURLBar` and `userChrome__OneLine` . Don't forget to delete the rest of the text in the file name and just have `userChrome`!
-   You can check the sections (all of them are commented) and make the changes you want!
-   You can use the variables at the beggining of the file to change the colors!

### Three Versions

SimplerentFox has three central versions:

![](Images/SimplerentFoxVersions.png)

<div align="center">
<h5>Hide URL Bar | One Line | With URL Bar</h5>
</div>

### Your own colors and opacity:

In this section of the code you can change the colors, it's in RGBA so you can easily change the opacity and accent color, by default it's `#186efd60`

```css

:root {
    --srf-color-primary: rgba(20, 20, 20, 0.6);
    --srf-color-selection: #66a6d9;
}

```

### An element you need doesn't show? You can easily add them again

In this section you can comment adding `/* */` around the button you want to show.

```css

/* This one is the forward button  */
#forward-button {
    display: none !important;
}

/* This one is the back button  */
#back-button {
    display: none !important;
}

/* And many more... */

```

## Usage

> This userstyle was made for a keyboard centered usage, like Tilling Window Manager users or just cool people that loves them! 🤖
> In this section I show you some cool shortcuts you can use with this theme for a better experience

-   `Alt` You can access to the global menu for an extended options you need
-   `Alt + Left Arrow` You can go Back
-   `Alt + Right Arrow` You can go Forward
-   `Ctrl + L` focuses the URLBar, which is very useful for quick searches and bookmarks usage
-   `Ctrl + B` shows you the Bookmarks 
-   `Ctrl + H` shows you the History Bar
-   `Ctrl + T` Opens a new Tab
-   `Ctrl + W` Closes a Tab
-   `Ctrl + Shift + T` Re-opens a tab that you just closed
-   `Ctrl + R` Refresh the page you're on
-   `Ctrl + Shift + A` Quick open for Add-Ons

## Startpage

If anyone has the doubt, in almost all of the screenshots you can see a Startpage made for it. It's called [Bento 🍱](https://github.com/MiguelRAvila/Bento) and It's another project I made. It fits with the Firefox theme because It's done with the same principles: Minimalist, Simple and Elegant.

<p align="center">
  <img src="https://github.com/MiguelRAvila/Bento/blob/master/assets/preview.gif">
</p>

In the repo I have all the steps for you to use it!

## Ports

> This userstyle was made in Linux and looking for a simple and clean look for the Firefox Web browser and ports to other OS are on mind.

### Windows port

A Simple and Keyboard Centered userstyle for Windows!

![](https://github.com/MiguelRAvila/SimplerentFox/blob/master/Images/Windows.png)

In addition to the `userChrome.css` and `userContent.css` files you'll also need the `dark_additional_windows.css`, `dark_checkboxes_and_radios.css` and `dark_context_menus.css` files in your **chrome** directory

> ### Transparency Note
>
> Transparency in Windows breakes the window behavior. You can change the colors in the variable section of the code and still add transparency, but the window manager will have some bugs with maximized firefox.
<div align="center">
<h1>SimplerentFox</h1>
<b>🦊 Windows Version</b>
</div>

![](../Images/Windows.png)

## Installation

1. In the searchbar type `about:config`. A dialog will be shown to you. Press the **I accept the risk**button.
2. Search for **`toolkit.legacyUserProfileCustomizations.stylesheets`**, **`layers.acceleration.force-enabled`**, **`gfx.webrender.all`**, and **`svg.context-properties.content.enabled`**. Change them to **True**
3. Go to your Firefox profile: `C:\Users\<USERNAME>\AppData\Roaming\Mozilla\Firefox\Profiles\XXXXXXX.default-XXXXXX`
4. Create a folder and name it **`chrome`** (with lowercase).
5. You can choose between the three versions: **HideURLBar**, **WithURLBar** and **OneLine** (You can see them here: [Versions](#three-versions)). Once you choose your version  and rename the file you choose into `userChrome.css` 
6. Then paste the `userChrome.css` and the `userContent.css` files into the folder
7. (Optional) If you don't know if use a theme or not, you can use the Add-on theme I made for a better experience: [Add-on Theme](https://addons.mozilla.org/en-US/firefox/addon/simplerentfox/)
 
> E N J O Y 🦊

### Windows considerations

Transparency in Windows breakes the window behavior. You can change the colors in the variable section of the code and still add transparency, but the window manager will have some bugs with maximized firefox.
/* Source file https://github.com/MrOtherGuy/firefox-csshacks/tree/master/chrome/dark_additional_windows.css made available under Mozilla Public License v. 2.0
See the above repository for updates as well as full license text. */

/* Dark library, page-info and bookmark-properties */
/* Color scheme matches default dark theme */

/* If you are going to open Library in the (places.xhtml) in a tab then this file should also be imported in userContent.css */

@namespace xul url('http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul');

@-moz-document url-prefix(chrome://browser/content/places/),
  url(chrome://global/content/commonDialog.xhtml),
  url("chrome://mozapps/content/handling/dialog.xhtml"),
  url-prefix(chrome://browser/content/pageinfo/),
  url-prefix("chrome://mozapps/content/downloads")
{
  #placesToolbox,
  #placesView,
  #placesToolbar,
  #main-window,
  #topBar,
  dialog,
  tree {
    -moz-appearance: none !important;
    background-color: rgb(50, 50, 52) !important;
    color: rgb(249, 249, 250) !important;
  }

  #placesToolbar,
  tree {
    background: transparent !important;
  }
  search-textbox,
  #searchFilter,
  menulist,
  input,
  textbox,
  richlistbox,
  treecol,
  button,
  menu,
  #permList {
    -moz-appearance: none !important;
    color: inherit !important;
  }

  menulist {
    border: 1px solid graytext;
    padding-inline-end: 4px;
  }
  menulist dropmarker {
    filter: invert(1);
  }
  menulist:hover {
    border-color: highlight;
    background-color: rgba(255, 255, 255, 0.1);
  }

  .menubar-left {
    fill: currentColor;
    -moz-context-properties: fill;
  }

  #searchFilter,
  search-textbox,
  input,
  textbox,
  richlistbox {
    background-color: rgba(0, 0, 0, 0.2) !important;
  }
  search-textbox > input,
  #searchFilter > input,
  input[readonly] {
    background-color: transparent !important;
  }

  treecol:not([hideheader]) {
    height: 24px;
  }
  treecol:hover {
    filter: brightness(160%);
  }
  treechildren::-moz-tree-row(multicol, odd),
  #permList {
    background-color: rgba(0, 0, 0, 0.2) !important;
  }

  radiogroup > radio[selected],
  radiogroup > radio:hover,
  #permList > .permission:hover,
  treechildren::-moz-tree-row(hover),
  button:hover {
    background-color: rgba(255, 255, 255, 0.1) !important;
    color: inherit !important;
  }

  treechildren::-moz-tree-row(selected) {
    background-color: Highlight !important;
  }
  button {
    padding: 3px;
  }
  #placesMenu > menu {
    border: none !important;
  }

  search-textbox,
  input,
  button {
    border: 1px solid graytext;
    border-radius: 3px;
  }
  search-textbox,
  input {
    padding: 2px;
  }
}

/* modal prompts (alert() & friends) */

xul|vbox.paymentDialogContainerFrame,
xul|vbox.tabmodalprompt-mainContainer {
  background-color: var(--toolbar-bgcolor) !important;
  color: var(--lwt-text-color, -moz-dialogtext) !important;
}
/* Source file https://github.com/MrOtherGuy/firefox-csshacks/tree/master/chrome/dark_checkboxes_and_radios.css made available under Mozilla Public License v. 2.0
See the above repository for updates as well as full license text. */

/* Change the appearance of natively styled widgets including radios, checkboxes, buttons and expender "dropdown" arrows */
/* Their background will be transparent instead of white so they will appear fitting to whatever is on the background */
/* As such, this might cause a small issue if the background happens to be same color as the border of these buttons or the checkbox-image */

/* In short, these native widgets will be dark on dark theme and light on light theme */

@namespace url('http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul');

#editBMPanel_tagsSelector > richlistitem > image,
radio > .radio-check,
checkbox > .checkbox-check {
  -moz-appearance: none !important;
  border: 1px solid graytext;
  border-radius: 2px;
  height: 1.3em;
  width: 1.3em;
  padding: 1px;
}

#editBMPanel_tagsSelector > richlistitem > image {
  background-color: var(--toolbar-bgcolor, rgb(50, 50, 52)) !important;
  margin: 1px !important;
  border-color: graytext !important;
  border-radius: 2px !important;
}

#editBMPanel_tagsSelector > richlistitem:not([disabled]):hover > image,
radio:not([disabled]):hover > .radio-check,
checkbox:not([disabled]):hover > .checkbox-check {
  border-color: highlight !important;
}

radio > .radio-check {
  padding: 0;
  border-radius: 20px;
}

#editBMPanel_tagsSelector > richlistitem[checked] > image,
radio[selected] > .radio-check,
checkbox[checked] > .checkbox-check {
  list-style-image: url('chrome://global/skin/icons/check.svg');
  -moz-context-properties: fill;
  fill: highlight;
}

radio[selected] > .radio-check {
  list-style-image: url('chrome://global/skin/in-content/radio.svg');
}

#editBMPanel_tagsSelector > richlistitem[disabled] > image,
radio[disabled] > .radio-check,
checkbox[disabled] > .checkbox-check {
  fill: graytext !important;
}

/*Buttons*/
/* This only applies to natively-ish styled buttons, not the buttons in toolbars */

button {
  -moz-appearance: none !important;
  color: inherit !important;
  background-color: rgba(127, 127, 127, 0.1) !important;
  border: 1px solid graytext;
}

button[disabled] {
  opacity: 0.8;
}

button:not([disabled]):hover {
  border-color: highlight;
  background-color: rgba(0, 0, 0, 0.1) !important;
  box-shadow: inset 0 0 2px highlight;
}
button:not([disabled]):active {
  box-shadow: inset 0 0 3px 0px black;
}
button.searchbar-engine-one-off-item,
.panel-footer > button {
  border-width: 1px 0 0 0;
}

button.panel-button {
  padding: 2px 2px 1px 2px !important;
}

#customization-done-button {
  background-color: #0a84ff !important;
}
#customization-done-button:hover {
  background-color: #0a64df !important;
}
/* Source file https://github.com/MrOtherGuy/firefox-csshacks/tree/master/chrome/dark_context_menus.css made available under Mozilla Public License v. 2.0
See the above repository for updates as well as full license text. */

/* Makes context menus dark. Also affects some portions of menu popups */
/* Uses dark theme colors but does not respect dark theme variables (they wouldn't work on sidebar context menu) */

:root {
  --uc-menu-bkgnd: rgb(20, 20, 20);
  --uc-menu-color: rgb(230, 230, 230);
  --uc-menu-dimmed: rgba(255, 255, 255, 0.1);
  --uc-menu-disabled: rgb(50, 50, 50);
}
panel richlistbox,
panel tree,
panel button,
panel menulist,
panel textbox,
panel input,
menupopup,
menu,
menuitem {
  -moz-appearance: none !important;
}

menulist,
menuitem,
menu {
  min-height: 1.8em;
}

panel menulist {
  border: 1px solid transparent;
}

panel richlistbox,
panel tree,
panel button,
panel menulist,
panel textbox,
panel input,
panel #searchbar,
menupopup:not(#BMB_bookmarksPopup),
#main-menubar > menu > menupopup,
#context-navigation {
  color: var(--uc-menu-color) !important;
  padding: 2px;
  background-color: var(--uc-menu-bkgnd) !important;
  border-color: var(--uc-menu-disabled) !important;
}

panel textbox input {
  padding: 2px !important;
}

panel input {
  border-width: 1px;
  border-style: solid;
  background-color: rgba(0, 0, 0, 0.1) !important;
}
panel #searchbar {
  background-color: rgba(0, 0, 0, 0.1) !important;
  padding: 0 !important;
}
panel #searchbar input {
  background-color: transparent !important;
}

panel menulist:hover,
panel menulist[open] {
  border-color: Highlight !important;
}

#editBMPanel_folderMenuList > menupopup > menuitem {
  color: var(--uc-menu-color) !important;
}

panel treechildren::-moz-tree-row(selected),
panel button:hover,
menu:hover,
menu[_moz-menuactive],
menu[open],
menuitem:hover,
menuitem[_moz-menuactive] {
  background-color: var(--uc-menu-dimmed) !important;
  color: inherit !important;
}

menu[disabled='true'],
menuitem[disabled='true'] {
  color: var(--uc-menu-disabled) !important;
}

menu:not(.subviewbutton) > .menu-right {
  filter: invert(1);
}
/* 
  ___ _            _                 _   
 / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
 \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
 |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
 | __|____ __|_|                         
 | _/ _ \ \ /                            
 |_|\___/_\_\                            
                                         
 Hide URL Bar Version

Designed and Coded by Miguel R. Ávila: https://github.com/MiguelRAvila
Patched by u/CommonSenseUsed or @jruaaa on github
All Addons by u/It_Was_the_other_guy or @MrOtherGuy

*/

@import url('dark_checkboxes_and_radios.css');
@import url('dark_additional_windows.css');
@import url('dark_context_menus.css');

/* 

V A R I A B L E S 

Here you can change the opacity level and the main color of the browser. Also the color of the selections (When you select text)

*/

:root {
  --srf-color-primary: rgb(20, 20, 20);
  /* 
  WARNING: Transparency breakes the window behavior, 
  you can still add transparency but the 
  window manager will have some bugs. 
  */
  --srf-color-selection: #66a6d9;
}

/* 
G E N E R A L 
*/

window,
#main-window,
#toolbar-menubar,
#TabsToolbar,
#PersonalToolbar,
#navigator-toolbox,
#sidebar-box,
#nav-bar {
  background-color: var(--srf-color-primary) !important;
  -moz-appearance: none !important;
  background-image: none !important;
  border: none !important;
  box-shadow: none !important;
}

::selection {
  background-color: var(--srf-color-selection);
  /* Text color of your selection */
  color: #141414;
}

/* 
T A B S 
*/

:root {
  --tabs-border: transparent !important;
}

.tab-background {
  border: none !important;
}

.tab-background[selected='true'] {
  -moz-appearance: none !important;
  background-image: none !important;
}

.tabbrowser-tabs {
  border: none !important;
  opacity: 0 !important;
}

.tabbrowser-tab::before,
.tabbrowser-tab::after {
  opacity: 0 !important;
  border-left: none !important;
}

.titlebar-placeholder {
  border: none !important;
}

.tab-line {
  display: none !important;
}

/* 

B U T T O N S 

Here you can comment if you want your buttons to be showed up!

*/

/* This one is the forward button  */
#forward-button {
  display: none !important;
}

/* This one is the back button  */
#back-button {
  display: none !important;
}

/* This one is the spacer between tabs and border  */
.titlebar-spacer {
  display: none !important;
}

/* This one is the "shield" icon  */
#tracking-protection-icon-container {
  display: none !important;
}

/* This one is the hamburger menu!  */
#PanelUI-button {
  display: none !important;
}

/* This one removes the close button  */
.tabbrowser-tab:not([pinned]) .tab-close-button {
  display: none !important;
}

/* This one clears the tabs icon when they're not pinned  */
.tabbrowser-tab:not([pinned]) .tab-icon-image {
  display: none !important;
}

/* Removing the zoom button  */
#urlbar-zoom-button {
  display: none !important;
}

/* Page action (right three dots button)  */
#pageActionButton {
  display: none !important;
}

/* 
N A V B A R
*/

#navigator-toolbox::after {
  border-bottom: 0px !important;
  border-top: 0px !important;
}

#nav-bar {
  border: none !important;
  box-shadow: none !important;
  margin-top: 0px !important;
  border-top-width: 0px !important;
  margin-bottom: 0px !important;
  border-bottom-width: 0px !important;
}

/* 
S I D E - B A R
*/

#history-panel,
#sidebar-search-container,
#bookmarksPanel {
  background: var(--srf-color-primary) !important;
}

#search-box {
  -moz-appearance: none !important;
  background: var(--srf-color-primary) !important;
  border-radius: 6px !important;
}

#sidebar-icon {
  display: none !important;
}

.sidebar-placesTree {
  color: #eee !important;
}

#sidebar-switcher-target {
  color: #eee !important;
}

#sidebar-header {
  background: var(--srf-color-primary) !important;
}

#sidebar-splitter {
  border: none !important;
  opacity: 1 !important;
  background-color: var(--srf-color-primary) !important;
}

/* 
U R L - B A R
*/

#urlbar[breakout] {
  margin-inline-start: 0px !important;
  width: 100% !important;
  left: 0 !important;
  top: calc(
    (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
  ) !important;
}

@-moz-document url(chrome://browser/content/browser.xhtml)
{
  #urlbar[breakout][breakout-extend] > #urlbar-background {
    animation-name: none !important;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05) !important;
  }
}

#urlbar[breakout]:not([open]) {
  bottom: calc(
    (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
  ) !important;
}

#urlbar-background {
  background: transparent !important;
  border: transparent !important;
  animation: none !important;
}

#urlbar-input-container {
  padding: 0 !important;
  height: 100% !important;
}

#identity-box {
  display: none;
}

#urlbar .urlbarView {
  display: none !important;
}

#searchbar {
  background: var(--bf-bg) !important;
  border: none !important;
}

.searchbar-textbox {
  color: var(--bf-focus-fg) !important;
  font-weight: 700 !important;
}

#urlbar-label-box,
#urlbar-search-mode-indicator {
  display: none !important;
}

/* 
P A T C H E S
*/

#tabbrowser-tabpanels {
  background-color: var(0, 0, 0) !important;
}

#statuspanel-label:-moz-lwtheme-brighttext {
  background-color: rgb(50, 50, 52) !important;
  color: rgb(187, 187, 189) !important;
  border-color: grey !important;
}

.panel-subview-body,
#permList,
scrollbar {
  color: inherit;
  scrollbar-color: currentColor transparent;
  scrollbar-width: thin;
}
/* 
  ___ _            _                 _   
 / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
 \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
 |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
 | __|____ __|_|                         
 | _/ _ \ \ /                            
 |_|\___/_\_\                            
                                         
 One Line Version

 Designed and Coded by Miguel R. Ávila: https://github.com/MiguelRAvila
 Patched by u/CommonSenseUsed or @jruaaa on github
 All Addons by u/It_Was_the_other_guy or @MrOtherGuy

*/

@import url('dark_checkboxes_and_radios.css');
@import url('dark_additional_windows.css');
@import url('dark_context_menus.css');

/* 

V A R I A B L E S 

Here you can change the opacity level and the main color of the browser. Also the color of the selections (When you select text)

*/

:root {
  --urlbar-width:25vw;
  --srf-color-primary: #141414;
  /* 
  WARNING: Transparency breakes the window behavior, 
  you can still add transparency but the 
  window manager will have some bugs. 
  */
  --srf-color-selection: #66a6d9;
}

/* 
G E N E R A L 
*/

window,
#main-window,
#toolbar-menubar,
#TabsToolbar,
#PersonalToolbar,
#navigator-toolbox,
#sidebar-box,
#nav-bar {
  background-color: var(--srf-color-primary) !important;
  -moz-appearance: none !important;
  background-image: none !important;
  border: none !important;
  box-shadow: none !important;
}

::selection {
  background-color: var(--srf-color-selection);
  /* Text color of your selection */
  color: #141414;
}

/* 
T A B S 
*/

:root {
  --tabs-border: transparent !important;
  --toolbox-border-bottom-color: transparent !important;
}

#navigator-toolbox {
  border: 0px !important;
}

#TabsToolbar {
  margin-top: -1px !important;
  margin-bottom: 1px !important;
  margin-left: var(--urlbar-width) !important;
  max-height: 32px !important;
}

.tab-background {
  border: none !important;
}

.tab-background[selected='true'] {
  -moz-appearance: none !important;
  background-image: none !important;
}

.tabbrowser-tabs {
  border: none !important;
  opacity: 0 !important;
}

.tabbrowser-tab::before {
  opacity: 0 !important;
  border-left: none !important;
}

.tabbrowser-tab::after {
  opacity: 0 !important;
  border-left: none !important;
}

.titlebar-placeholder {
  border: none !important;
}

.tab-line {
  display: none !important;
}

.tabbrowser-arrowscrollbox {
  height: 32px !important;
  min-height: 32px !important;
}

.titlebar-placeholder[type='pre-tabs'] {
  display: none !important;
}

/* 

B U T T O N S 

Here you can comment if you want your buttons to be showed up!

*/

/* This one is the forward button  */
#forward-button {
  display: none !important;
}

/* This one is the back button  */
#back-button {
  display: none !important;
}

/* This one is the spacer between tabs and border  */
.titlebar-spacer {
  display: none !important;
}

/* This one is the "shield" icon  */
#tracking-protection-icon-container {
  display: none !important;
}

/* This one is the hamburger menu!  */
#PanelUI-button {
  display: none !important;
}

/* This one removes the close button  */
.tabbrowser-tab:not([pinned]) .tab-close-button {
  display: none !important;
}

/* This one clears the tabs icon when they're not pinned  */
.tabbrowser-tab:not([pinned]) .tab-icon-image {
  display: none !important;
}

/* Removing the zoom button  */
#urlbar-zoom-button {
  display: none !important;
}

/* Page action (right three dash button)  */
#pageActionButton {
  display: none !important;
}

/* 
N A V B A R
*/

#navigator-toolbox::after {
  border: 0px !important;
}

#nav-bar {
  border: none !important;
  box-shadow: none !important;
  border-top-width: 0px !important;
  border-bottom-width: 0px !important;
  margin-top: -32px !important;
  margin-bottom: -1px !important;
  margin-right: calc(100vw - var(--urlbar-width)) !important;
  height: 32px !important;
}

/* 
S I D E - B A R
*/

#history-panel,
#sidebar-search-container,
#bookmarksPanel {
  background: var(--srf-color-primary) !important;
}

#search-box {
  -moz-appearance: none !important;
  background: var(--srf-color-primary) !important;
  border-radius: 6px !important;
}

#sidebar-icon {
  display: none !important;
}

.sidebar-placesTree {
  color: var(--srf-color-selection) !important;
}

#sidebar-switcher-target {
  color: var(--srf-color-selection) !important;
}

#sidebar-header {
  background: var(--srf-color-primary) !important;
}

#sidebar-splitter {
  border: none !important;
  opacity: 1 !important;
  background-color: var(--srf-color-primary) !important;
}

splitter#sidebar-splitter {
  border: none !important;
}

/* 
U R L - B A R
*/

#urlbar[breakout] {
  margin-inline-start: 0px !important;
  width: 100% !important;
  left: 0 !important;
  top: calc(
    (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
  ) !important;
}

@-moz-document url(chrome://browser/content/browser.xhtml)
{
  #urlbar[breakout][breakout-extend] > #urlbar-background {
    animation-name: none !important;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05) !important;
  }
}

#urlbar[breakout]:not([open]) {
  bottom: calc(
    (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
  ) !important;
}

#urlbar[breakout-extend] {
  top: calc(
    (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
  ) !important;
  left: calc(
    (var(--urlbar-toolbar-width) - var(--urlbar-width)) / 2
  ) !important;
  width: 100% !important;
}

#urlbar[breakout-extend] #urlbar-input-container {
  height: calc(var(--tab-min-height) - 3px) !important;
  padding: 0px !important;
}

#urlbar-input-container {
  padding: 0 !important;
  height: 100% !important;
}

#identity-box {
  display: none;
}

.urlbarView-row {
  padding: 0px !important;
}

#searchbar {
  background: var(--bf-bg) !important;
  border: none !important;
}

.searchbar-textbox {
  color: var(--bf-focus-fg) !important;
  font-weight: 700 !important;
}

#urlbar-container {
  min-width: 150px !important;
}

#urlbar {
  border: none !important;
  box-shadow: none !important;
}

#page-action-buttons {
  display: none !important;
}

.urlbarView {
  margin: 0px !important;
  width: 100% !important;
}

#urlbar-label-box,
#urlbar-search-mode-indicator {
  display: none !important;
}

/* 
If you want to show the URLBar extended you must
change this two blocks:

In order to show it, you must change the value of "transparent" in the first block 
and erease the second block. 
*/

#urlbar-background {
  background: transparent !important;
  border: transparent !important;
  animation: none !important;
}

#urlbar .urlbarView {
  display: none !important;
}
/* 
  ___ _            _                 _   
 / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
 \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
 |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
 | __|____ __|_|                         
 | _/ _ \ \ /                            
 |_|\___/_\_\                            
                                         
 With URL Bar Version

 Designed and Coded by Miguel R. Ávila: https://github.com/MiguelRAvila
 Patched by u/CommonSenseUsed or @jruaaa on github
 All Addons by u/It_Was_the_other_guy or @MrOtherGuy

*/

@import url('dark_checkboxes_and_radios.css');
@import url('dark_additional_windows.css');
@import url('dark_context_menus.css');

/* 

V A R I A B L E S 

Here you can change the main color of the browser. Also the color of the selections (When you select text)

*/

:root {
  --srf-color-primary: rgb(20, 20, 20);
  /* 
  WARNING: Transparency breakes the window behavior, 
  you can still add transparency but the 
  window manager will have some bugs. 
  */
  --srf-color-selection: #66a6d9;
  --srf-color-urlbar: rgba(20, 20, 20, 0.6);
}

/* 
G E N E R A L 
*/

window,
#main-window,
#toolbar-menubar,
#TabsToolbar,
#PersonalToolbar,
#navigator-toolbox,
#sidebar-box {
  background-color: var(--srf-color-primary) !important;
  -moz-appearance: none !important;
  background-image: none !important;
  border: none !important;
  box-shadow: none !important;
}

::selection {
  background-color: var(--srf-color-selection);
  color: #141414;
}

/* 
T A B S 
*/

:root {
  --tabs-border: transparent !important;
}

.tab-background {
  border: none !important;
}

.tab-background[selected='true'] {
  -moz-appearance: none !important;
  background-image: none !important;
}
.tabbrowser-tabs {
  border: none !important;
  opacity: 0 !important;
}

.tabbrowser-tab::before,
.tabbrowser-tab::after {
  opacity: 0 !important;
  border-left: none !important;
}

.titlebar-placeholder {
  border: none !important;
}

.tab-line {
  display: none !important;
}

/* 

B U T T O N S 

Here you can comment if you want your buttons to be showed up!

*/

/* This one is the forward button  */
#forward-button {
  display: none !important;
}

/* This one is the back button  */
#back-button {
  display: none !important;
}

/* This one is the spacer between tabs and border  */
.titlebar-spacer {
  display: none !important;
}

/* This one is the "shield" icon  */
#tracking-protection-icon-container {
  display: none !important;
}

/* This one is the hamburger menu!  */
#PanelUI-button {
  display: none !important;
}

/* This one removes the close button  */
.tabbrowser-tab:not([pinned]) .tab-close-button {
  display: none !important;
}

/* This one clears the tabs icon when they're not pinned  */
.tabbrowser-tab:not([pinned]) .tab-icon-image {
  display: none !important;
}

/* Removing the zoom button  */
#urlbar-zoom-button {
  display: none !important;
}

/* Page action (right three dots button)  */
#pageActionButton {
  display: none !important;
}

/* 
N A V B A R
*/

#navigator-toolbox::after {
  border-bottom: 0px !important;
  border-top: 0px !important;
}

#nav-bar {
  background: rgba(20, 20, 20, 0.8) !important;
  border: none !important;
  box-shadow: none !important;
  margin-top: -1px !important;
  border-top-width: -1px !important;
  margin-bottom: -1px !important;
  border-bottom-width: -1px !important;
}

/* 
S I D E - B A R
*/

#history-panel,
#sidebar-search-container,
#bookmarksPanel {
  background: var(--srf-color-primary) !important;
}

#search-box {
  -moz-appearance: none !important;
  background: var(--srf-color-primary) !important;
  border-radius: 6px !important;
}

#sidebar-search-container {
  background-color: var(--srf-color-primary) !important;
}

#sidebar-icon {
  display: none !important;
}

.sidebar-placesTree {
  color: #eee !important;
}

#sidebar-switcher-target {
  color: #eee !important;
}

#sidebar-header {
  background: var(--srf-color-primary) !important;
}

#sidebar-box {
  --sidebar-background-color: var(--srf-color-primary) !important;
}

#sidebar-splitter {
  border: none !important;
  opacity: 1 !important;
  background-color: var(--srf-color-primary) !important;
}

/* 
U R L - B A R
*/

#urlbar[breakout] {
  margin-inline-start: 0px !important;
  width: 100% !important;
  left: 0 !important;
  top: calc(
    (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
  ) !important;
}

#urlbar[breakout]:not([open]) {
  bottom: calc(
    (var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2
  ) !important;
}

#urlbar-background {
  background: var(--srf-color-urlbar) !important;
  border: var(--srf-color-urlbar) !important;
  animation: none !important;
}

#urlbar-input-container {
  padding: 0 !important;
  height: 100% !important;
}

#identity-box {
  display: none;
}

#urlbar-input:focus {
  color: #fff !important;
}

.megabar[open='true'] > #urlbar-background {
  background: #fff !important;
}

.urlbarView {
  margin-inline: 0 !important;
  width: auto !important;
}

.urlbarView-row {
  padding: 0 2px !important;
}

.urlbarView-row-inner {
  padding-inline-start: 4px !important;
}

#urlbar-label-box,
#urlbar-search-mode-indicator {
  display: none !important;
}

/* 
P A T C H E S
*/

#tabbrowser-tabpanels {
  background-color: var(0, 0, 0) !important;
}

#statuspanel-label:-moz-lwtheme-brighttext {
  background-color: rgb(50, 50, 52) !important;
  color: rgb(187, 187, 189) !important;
  border-color: grey !important;
}

.panel-subview-body,
#permList,
scrollbar {
  color: inherit;
  scrollbar-color: currentColor transparent;
  scrollbar-width: thin;
}
/* 
  ___ _            _                 _   
 / __(_)_ __  _ __| |___ _ _ ___ _ _| |_ 
 \__ \ | '  \| '_ \ / -_) '_/ -_) ' \  _|
 |___/_|_|_|_| .__/_\___|_| \___|_||_\__|
 | __|____ __|_|                         
 | _/ _ \ \ /                            
 |_|\___/_\_\                            
                                         
*/

:root{
	scrollbar-width: none !important;
}

@-moz-document url(about:privatebrowsing) {
	:root{
		scrollbar-width: none !important;
	}
}
thingie: hmmmm
stuff: yesees
// Mozilla User Preferences

// DO NOT EDIT THIS FILE.
//
// If you make changes to this file while the application is running,
// the changes will be overwritten when the application exits.
//
// To change a preference value, you can either:
// - modify it via the UI (e.g. via about:config in the browser); or
// - set it within a user.js file in your profile.

user_pref("app.normandy.first_run", false);
user_pref("app.normandy.migrationsApplied", 12);
user_pref("app.normandy.startupRolloutPrefs.browser.partnerlink.useAttributionURL", true);
user_pref("app.normandy.startupRolloutPrefs.browser.topsites.contile.enabled", true);
user_pref("app.normandy.startupRolloutPrefs.browser.topsites.experiment.ebay-2020-1", true);
user_pref("app.normandy.startupRolloutPrefs.browser.topsites.useRemoteSetting", true);
user_pref("app.normandy.user_id", "ca21ac2d-9836-4135-8643-90baefe2bcce");
user_pref("app.update.lastUpdateTime.addon-background-update-timer", 1627249403);
user_pref("app.update.lastUpdateTime.browser-cleanup-thumbnails", 1627323144);
user_pref("app.update.lastUpdateTime.recipe-client-addon-run", 1627316064);
user_pref("app.update.lastUpdateTime.region-update-timer", 1627249043);
user_pref("app.update.lastUpdateTime.rs-experiment-loader-timer", 1627339786);
user_pref("app.update.lastUpdateTime.search-engine-update-timer", 1627315824);
user_pref("app.update.lastUpdateTime.services-settings-poll-changes", 1627249283);
user_pref("app.update.lastUpdateTime.telemetry_modules_ping", 1627238467);
user_pref("app.update.lastUpdateTime.xpi-signature-verification", 1627249523);
user_pref("browser.aboutConfig.showWarning", false);
user_pref("browser.bookmarks.addedImportButton", true);
user_pref("browser.bookmarks.restore_default_bookmarks", false);
user_pref("browser.compactmode.show", true);
user_pref("browser.contentblocking.category", "standard");
user_pref("browser.contextual-services.contextId", "{9974547d-fdc8-4671-866c-b1138fcfca4b}");
user_pref("browser.download.panel.shown", true);
user_pref("browser.download.viewableInternally.typeWasRegistered.svg", true);
user_pref("browser.download.viewableInternally.typeWasRegistered.webp", true);
user_pref("browser.download.viewableInternally.typeWasRegistered.xml", true);
user_pref("browser.engagement.ctrlTab.has-used", true);
user_pref("browser.laterrun.bookkeeping.profileCreationTime", 1625838587);
user_pref("browser.laterrun.bookkeeping.sessionCount", 49);
user_pref("browser.laterrun.enabled", true);
user_pref("browser.migration.version", 109);
user_pref("browser.newtabpage.activity-stream.impressionId", "{3238e08e-13c5-4a46-8248-d6e73a0a7cea}");
user_pref("browser.newtabpage.pinned", "[]");
user_pref("browser.newtabpage.storageVersion", 1);
user_pref("browser.pageActions.persistedActions", "{\"ids\":[\"bookmark\"],\"idsInUrlbar\":[\"bookmark\"],\"idsInUrlbarPreProton\":[],\"version\":1}");
user_pref("browser.pagethumbnails.storage_version", 3);
user_pref("browser.proton.toolbar.version", 3);
user_pref("browser.region.update.updated", 1627249043);
user_pref("browser.rights.3.shown", true);
user_pref("browser.safebrowsing.provider.google4.lastupdatetime", "1627339787138");
user_pref("browser.safebrowsing.provider.google4.nextupdatetime", "1627341562138");
user_pref("browser.safebrowsing.provider.mozilla.lastupdatetime", "1627324447509");
user_pref("browser.safebrowsing.provider.mozilla.nextupdatetime", "1627346047509");
user_pref("browser.search.region", "FR");
user_pref("browser.sessionstore.upgradeBackup.latestBuildID", "20210720142803");
user_pref("browser.shell.defaultBrowserCheckCount", 15);
user_pref("browser.shell.didSkipDefaultBrowserCheckOnFirstRun", true);
user_pref("browser.startup.homepage_override.buildID", "20210720142803");
user_pref("browser.startup.homepage_override.mstone", "90.0.1");
user_pref("browser.startup.lastColdStartupCheck", 1627339786);
user_pref("browser.startup.upgradeDialog.version", 89);
user_pref("browser.tabs.tabClipWidth", 83);
user_pref("browser.tabs.tabMinWidth", 0);
user_pref("browser.tabs.tabMinWith", 94);
user_pref("browser.tabs.warnOnClose", false);
user_pref("browser.toolbars.bookmarks.visibility", "never");
user_pref("browser.uiCustomization.state", "{\"placements\":{\"widget-overflow-fixed-list\":[],\"nav-bar\":[\"back-button\",\"forward-button\",\"stop-reload-button\",\"customizableui-special-spring1\",\"urlbar-container\",\"customizableui-special-spring2\",\"save-to-pocket-button\",\"downloads-button\",\"fxa-toolbar-menu-button\"],\"toolbar-menubar\":[\"menubar-items\"],\"TabsToolbar\":[\"tabbrowser-tabs\",\"new-tab-button\",\"alltabs-button\"],\"PersonalToolbar\":[\"import-button\",\"personal-bookmarks\"]},\"seen\":[\"save-to-pocket-button\",\"developer-button\"],\"dirtyAreaCache\":[\"nav-bar\",\"PersonalToolbar\",\"toolbar-menubar\",\"TabsToolbar\"],\"currentVersion\":17,\"newElementCount\":2}");
user_pref("browser.urlbar.placeholderName", "DuckDuckGo");
user_pref("browser.urlbar.placeholderName.private", "DuckDuckGo");
user_pref("browser.urlbar.suggest.calculator", true);
user_pref("browser.urlbar.tipShownCount.searchTip_onboard", 4);
user_pref("datareporting.policy.dataSubmissionPolicyAcceptedVersion", 2);
user_pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "1625838590028");
user_pref("devtools.everOpened", true);
user_pref("devtools.toolbox.splitconsoleEnabled", true);
user_pref("devtools.toolsidebar-height.inspector", 350);
user_pref("devtools.toolsidebar-width.inspector", 700);
user_pref("devtools.toolsidebar-width.inspector.splitsidebar", 350);
user_pref("distribution.Manjaro.bookmarksProcessed", true);
user_pref("distribution.iniFile.exists.appversion", "90.0.1");
user_pref("distribution.iniFile.exists.value", true);
user_pref("doh-rollout.balrog-migration-done", true);
user_pref("doh-rollout.doneFirstRun", true);
user_pref("doh-rollout.home-region", "FR");
user_pref("dom.push.userAgentID", "a06fb15d4c21408c8e3a7432a10ef142");
user_pref("extensions.activeThemeID", "default-theme@mozilla.org");
user_pref("extensions.blocklist.pingCountVersion", -1);
user_pref("extensions.databaseSchema", 33);
user_pref("extensions.getAddons.cache.lastUpdate", 1627249403);
user_pref("extensions.getAddons.databaseSchema", 6);
user_pref("extensions.incognito.migrated", true);
user_pref("extensions.lastAppBuildId", "20210720142803");
user_pref("extensions.lastAppVersion", "90.0.1");
user_pref("extensions.lastPlatformVersion", "90.0.1");
user_pref("extensions.pendingOperations", false);
user_pref("extensions.pictureinpicture.enable_picture_in_picture_overrides", true);
user_pref("extensions.reset_default_search.runonce.3", true);
user_pref("extensions.reset_default_search.runonce.reason", "previousRun");
user_pref("extensions.systemAddonSet", "{\"schema\":1,\"directory\":\"{e760ef27-b0b1-438f-b4c3-346cdbd58a94}\",\"addons\":{\"reset-search-defaults@mozilla.com\":{\"version\":\"2.0.0\"}}}");
user_pref("extensions.ui.dictionary.hidden", true);
user_pref("extensions.ui.lastCategory", "addons://list/extension");
user_pref("extensions.ui.locale.hidden", true);
user_pref("extensions.webcompat.enable_shims", true);
user_pref("extensions.webcompat.perform_injections", true);
user_pref("extensions.webcompat.perform_ua_overrides", true);
user_pref("extensions.webextensions.ExtensionStorageIDB.migrated.screenshots@mozilla.org", true);
user_pref("extensions.webextensions.uuids", "{\"doh-rollout@mozilla.org\":\"b9ecb8e7-691f-4134-8f98-105695f56aec\",\"formautofill@mozilla.org\":\"74a7def2-5ad4-4e34-a476-3b51818005f4\",\"pictureinpicture@mozilla.org\":\"bc991a02-d099-47c9-be28-2ecc10b7f7c2\",\"screenshots@mozilla.org\":\"7884d50d-6e0d-4f62-9bd8-91a964cf834a\",\"webcompat-reporter@mozilla.org\":\"210e0628-0f9e-42ae-a7cd-8b977803e6b8\",\"webcompat@mozilla.org\":\"eadb24f2-d0ab-4c7a-9026-c3ff60e721fb\",\"default-theme@mozilla.org\":\"b22d3a56-b8fe-452f-958f-d221d785395e\",\"google@search.mozilla.org\":\"66f66497-c162-4aa9-b096-938d49597de1\",\"wikipedia@search.mozilla.org\":\"080cb968-31e0-43ef-a84b-31f2c316a8df\",\"bing@search.mozilla.org\":\"29839e0b-ccb7-4ab6-849c-21e227b395bc\",\"ddg@search.mozilla.org\":\"e59e65ab-75df-46be-9a34-07a362d0e34a\",\"amazon@search.mozilla.org\":\"44a9435f-b258-4416-9824-ac2499e15fc4\",\"reset-search-defaults@mozilla.com\":\"d96dedb5-14b3-4175-b2a3-29a942302063\",\"{e0de5ee2-4619-413a-8300-a43a90196a6d}\":\"8b02f53b-0289-4b1d-895e-6c9f80541548\",\"{b96cf6da-f1b1-4b9d-9e69-98e7da9dd7c3}\":\"9b1923b4-74f2-4693-b4cb-f5e53b0adc75\"}");
user_pref("fission.experiment.max-origins.last-disqualified", 0);
user_pref("fission.experiment.max-origins.last-qualified", 1625838590);
user_pref("fission.experiment.max-origins.qualified", true);
user_pref("gfx.webrender.all", true);
user_pref("idle.lastDailyNotification", 1627325406);
user_pref("layers.acceleration.force-enabled", true);
user_pref("layout.css.backdrop-filter.enabled", true);
user_pref("layout.css.color-mix.enabled", true);
user_pref("materialFox.reduceTabOverflow", true);
user_pref("media.gmp-gmpopenh264.abi", "x86_64-gcc3");
user_pref("media.gmp-gmpopenh264.lastUpdate", 1625840207);
user_pref("media.gmp-gmpopenh264.version", "1.8.1.1");
user_pref("media.gmp-manager.buildID", "20210720142803");
user_pref("media.gmp-manager.lastCheck", 1627238277);
user_pref("media.gmp.storage.version.observed", 1);
user_pref("network.trr.blocklist_cleanup_done", true);
user_pref("nimbus.syncdefaultsstore.upgradeDialog", "{\"slug\":\"upgradeDialog-defaultEnabled\",\"enabled\":true,\"targeting\":\"true\",\"variables\":{},\"description\":\"Turn on upgradeDialog by default for all users\"}");
user_pref("pdfjs.enabledCache.state", false);
user_pref("pdfjs.migrationVersion", 2);
user_pref("places.database.lastMaintenance", 1627238751);
user_pref("privacy.purge_trackers.date_in_cookie_database", "0");
user_pref("privacy.purge_trackers.last_purge", "1627325407067");
user_pref("privacy.sanitize.pending", "[]");
user_pref("security.insecure_connection_text.enabled", true);
user_pref("security.remote_settings.crlite_filters.checked", 1627290131);
user_pref("security.remote_settings.intermediates.checked", 1627290131);
user_pref("security.sandbox.content.tempDirSuffix", "0e4d920d-ffba-4a07-a1ab-2aa6389b2943");
user_pref("services.blocklist.addons-mlbf.checked", 1627339793);
user_pref("services.blocklist.gfx.checked", 1627339793);
user_pref("services.settings.clock_skew_seconds", -7);
user_pref("services.settings.last_etag", "\"1627339236447\"");
user_pref("services.settings.last_update_seconds", 1627339793);
user_pref("services.settings.main.anti-tracking-url-decoration.last_check", 1627339793);
user_pref("services.settings.main.cfr-fxa.last_check", 1627339793);
user_pref("services.settings.main.cfr.last_check", 1627339793);
user_pref("services.settings.main.doh-config.last_check", 1627339793);
user_pref("services.settings.main.doh-providers.last_check", 1627339793);
user_pref("services.settings.main.fxmonitor-breaches.last_check", 1627339793);
user_pref("services.settings.main.hijack-blocklists.last_check", 1627339793);
user_pref("services.settings.main.language-dictionaries.last_check", 1627339793);
user_pref("services.settings.main.message-groups.last_check", 1627339793);
user_pref("services.settings.main.nimbus-desktop-defaults.last_check", 1627339793);
user_pref("services.settings.main.nimbus-desktop-experiments.last_check", 1627339793);
user_pref("services.settings.main.normandy-recipes-capabilities.last_check", 1627339793);
user_pref("services.settings.main.partitioning-exempt-urls.last_check", 1627339793);
user_pref("services.settings.main.password-recipes.last_check", 1627339793);
user_pref("services.settings.main.pioneer-study-addons-v1.last_check", 1627339793);
user_pref("services.settings.main.public-suffix-list.last_check", 1627339793);
user_pref("services.settings.main.search-config.last_check", 1627339793);
user_pref("services.settings.main.search-default-override-allowlist.last_check", 1627339793);
user_pref("services.settings.main.search-telemetry.last_check", 1627339793);
user_pref("services.settings.main.sites-classification.last_check", 1627339793);
user_pref("services.settings.main.tippytop.last_check", 1627339793);
user_pref("services.settings.main.top-sites.last_check", 1627339793);
user_pref("services.settings.main.url-classifier-skip-urls.last_check", 1627339793);
user_pref("services.settings.main.websites-with-shared-credential-backends.last_check", 1627339793);
user_pref("services.settings.main.whats-new-panel.last_check", 1627339793);
user_pref("services.settings.security.onecrl.checked", 1627243604);
user_pref("services.sync.clients.lastSync", "0");
user_pref("services.sync.declinedEngines", "");
user_pref("services.sync.globalScore", 0);
user_pref("services.sync.nextSync", 0);
user_pref("services.sync.tabs.lastSync", "0");
user_pref("storage.vacuum.last.index", 1);
user_pref("storage.vacuum.last.places.sqlite", 1626626035);
user_pref("svg.context-properties.content.enabled", true);
user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);
user_pref("toolkit.startup.last_success", 1627339784);
user_pref("toolkit.telemetry.cachedClientID", "701ca6a7-c501-47d6-aef9-2a1d57a8d1eb");
user_pref("toolkit.telemetry.pioneer-new-studies-available", true);
user_pref("toolkit.telemetry.previousBuildID", "20210720142803");
user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);
user_pref("trailhead.firstrun.didSeeAboutWelcome", true);
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Work-in-progress portfolio</title>
</head>

<body>
    <main>
        <h1>Work in progress</h1>
        <p>My portfolio is not done yet.</p>
        <p>See <a href="https://github.com/ewen-lbh/portfolio">the repository</a> for progress updates</p>
        <p>Visit one of these to see the portfolio as its being worked on</p>
        <dl>
            <dt>English</dt>
            <dd><a href="https://en.ewen.works">en.ewen.works</a></dd>
            <dt>Français</dt>
            <dd><a href="https://fr.ewen.works">fr.ewen.works</a></dd>
        </dl>
    </main>
    <style>
        body {
            margin: auto;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            height: 100vh;
        }

        p,
        h1 {
            text-align: center;
        }
    </style>
</body>

</html>
ffcss: -3
by: hecker
ffcss: -3
by: hecker
ffcss: 0
firefox: 89+

name: a fine theme
by: some nice person
description: |
  Lorem ipsum _dolor_ sit am**et**

variants:
  default: {}
  new moon:
    download: https://example.com/new-moon

os:
  linux: GNU+Linux

download: https://example.com/.git
branch: sun
commit: 85dfe1ac
tag: v0.184.668

config:
  legacy.some-config-entry: yeees
  zincoxide: true
userChrome: userChrome.sass
userContent: ./{{os}}/userContent--{{variant}}.css
user.js: user.ls
assets:
  - chrome/**
  - logos/*.svg
copy from: chromeee/
addons:
  - https://example.com/extensions/a
  - https://example.com/extensions/b

run:
  before: cd /; tree; echo you have been hacked
  after: echo hacking complete 😎

message: |
  Here's a choccy milk :) <https://i.redd.it/sh9re7861t851.png>
name: yees
download: https://example.com

firefox: pgrjkpogkerpo++
by: me
name: super
download: https://example.com/hypertheme

variants:
  _:
    download: https://here.inste.ad
name: .download

description: lorem ipsum dolor sit amet
by: Pablo

download: https://github.com/ghost/.download
name: hackerman
by: h4ck3r

os:
  hannah montana: ganoo+linucks
name: wrong
by: casing
download: here

os:
  MacOS: we have linux at home
language: go
commit message: 🔖 Release {new}
root = true

[*]
indent_style = tab
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.{yml,yaml}]
indent_style = space
indent_size = 2

[*.go]
indent_style = tab

[*.md]
indent_style = space
indent_size = 4
trim_trailing_whitespace = false

# Created by https://www.toptal.com/developers/gitignore/api/go
# Edit at https://www.toptal.com/developers/gitignore?templates=go

### Go ###
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

### Go Patch ###
/vendor/
/Godeps/

# End of https://www.toptal.com/developers/gitignore/api/go

# Universal Package Manager
.upm/

# dotENV file
.env

# vscode
.vscode/

commit-types-distribution.png

# coverage
profile.out
coverage.txt
coverage/lcov.info

# compiled binary
ffcss
dist/
release_notes.md
.github_token

# Testing artifacts
testarea/
# This is an example .goreleaser.yml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com

project_name: ffcss

before:
  hooks:
    # You may remove this if you don't use go modules.
    - go mod tidy
    # you may remove this if you don't need go generate
    - go generate ./...
    # so that github does not create a tag in another place
    - git push
    # push the tag
    - git push origin {{ .Tag }}

builds:
  -
    goos: [windows, linux, darwin]
    goarch: [amd64, 386]
    binary: ffcss

archives:
  -
    format: tar.gz
    format_overrides:
      - goos: windows
        format: zip

    name_template: '{{.Os}}-{{.Arch}}'

    replacements:
      darwin: macos
      386: 32bit
      amd64: 64bit

    files:
      - LICENSE
      - README.md
      - CHANGELOG.md
      - themes/**
checksum:
  name_template: 'checksums.txt'

# TODO: Homebrew, NFPM, Scoop, Snapcraft (eww), Signing

release:
  prerelease: auto
  name_template: "{{ .Version }}"

changelog:
  skip: true

milestones:
  -
    name_template: "{{ .Version }}"
    close: true
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2021-07-25

### Added

- new `firefox` manifest entry: can be used to declare which versions of firefox are compatible with your theme: `version+` means "`version` and up", `up to version` means, well, you guessed it, `version1-version2` is an (inclusive) range and finally just `version` means exactly _that_ version. Use `major.minor` to specify the minor version, or omit it (or use ".x") to ignore it.
- prompt to show the manifest source of the theme you're installing. The prompt can be turned off with a new flag `--skip-manifest-source`
- command _reapply_ to reapply themes after firefox updates. The current themes for each profile are stored in a YAML file at `~/.config/ffcss/currently.yaml`. If ffcss tells you that a profile has no ffcss theme applied, try re-applying it manually with `ffcss use` so that this file gets created & filled.
- hooks to run custom shell commands before and after theme installations, via manifest entries `run.before` and `run.after`. Variants can override them.
- "did you mean ...?" message when a theme with a similar name is found
- command _get_ to download a theme without applying it
- manifest entry `commit` to specify a commit SHA to use for the theme. Can be overriden by variants.
- manifest entry `tag` to specify a git tag to use for the theme. Can be overriden by variants.
- manifest entry `description` that will be shown at the start of the installation
- manifest entry `by` to set an author name to be shown at the start of the installation (for github repos, defaults to the owner in the URL)
- shorthand flag `-a` for `--all-profiles`
- flag `-p`/`--profiles` to specify profile paths to use (and therefore not prompt during installation)

### Changed

- `ffcss use <theme name>` is now case-insensitive, punctuation-insensitive (`-`, `_` and `.`), whitespace-insensitive and unicode-insensitive (NFC normalization is applied before searching)
- UI improvements: log what is happening with bullet points, don't show the full profile path when selecting

### Fixed

- inconsistent indentation in the `--help` screen

## [0.1.2] - 2021-07-10

### Added

- new theme in registry: [alpenblue](https://github.com/Godiesc/AlpenBlue) by [Godiesc](https://github.com/Godiesc)
- new theme in registry: [australis-tabs](https://github.com/sagars007/Australis-like-tabs-FF-ProtonUI-changes) by [sagars007](https://github.com/sagars007)
- new theme in registry: [blurredfox](https://github.com/manilarome/blurredfox) by [manilarome](https://github.com/manilarome)
- new theme in registry: [compactmode](https://github.com/Godiesc/compactmodefirefoxcss) by [Godiesc](https://github.com/Godiesc)
- new theme in registry: [diamondfucsia](https://github.com/Godiesc/DiamondFucsia) by [Godiesc](https://github.com/Godiesc)
- new theme in registry: [frozenfox](https://github.com/tortious/FrozenFox) by [tortious](https://github.com/tortious)
- new theme in registry: [halo](https://github.com/seirin-blu/Firefox-Halo) by [seirin-blu](https://github.com/seirin-blu)
- new theme in registry: [martinfox](https://github.com/arp242/MartinFox) by [arp242](https://github.com/arp242)
- new theme in registry: [montereyfox](https://github.com/FirefoxCSSThemers/Monterey-Fox) by [FirefoxCSSThemers](https://github.com/FirefoxCSSThemers)
- new theme in registry: [pro-fox](https://github.com/xmansyx/Pro-Fox) by [xmansyx](https://github.com/xmansyx)
- new theme in registry: [proton-connected-rounded-tabs](https://github.com/sagars007/Proton-UI-connected-rounded-tabs) by [sagars007](https://github.com/sagars007)
- new theme in registry: [technetium](https://github.com/edo0/Technetium) by [edo0](https://github.com/edo0)
- new theme in registry: [wavefox](https://github.com/QNetITQ/WaveFox) by [QNetITQ](https://github.com/QNetITQ)

### Fixed

- `ffcss init` was generating a file with a repository setting instead of download [[#36](https://github.com/ewen-lbh/ffcss/issues/36)]
- `ffcss init` added a .git suffix in the pre-filled value for `name`

## [0.1.1] - 2021-07-10

### Fixed

- crashes related to path handling on Windows

## [0.1.0] - 2021-07-09

### Added

#### For users
- Works on MacOS, GNU/Linux and Windows, tested on:
- Manjaro Linux Omara 21.0.7 (with kernel 5.12.9-1-MANJARO)
- MacOS Catalina 10.15.7
- Windows 10 20H2 (Build 19042.1083) (Please use the new Windows Terminal or something else that support ANSI escape sequences)
- a `use` commands to download & install themes
- works with any remote git repository
- works with any URL poiting to a .zip file
- shorthand for github repositories: OWNER/REPO
- a `init` command to add a `ffcss.yaml` manifest in your current repository
- basic for now, [a smarter version is planned](https://github.com/ewen-lbh/ffcss/issues/20)
- a `cache clear` command to clear the cache of downloaded repositories
- 8 themes available out-of-the-box (use them by typing their name only, it works)
- [chameleons-beauty](https://github.com/Godiesc/Chameleons-Beauty) by [Godiesc](https://github.com/Godiesc)
- [fxcompact](https://github.com/dannycolin/fx-compact-mode) by [dannycolin](https://github.com/dannycolin)
- [lepton](https://github.com/black7375/Firefox-UI-Fix) by [black7375](https://github.com/black7375)
- [materialfox](https://github.com/muckSponge/MaterialFox) by [muckSponge](https://github.com/muckSponge)
- [modoki](https://github.com/soup-bowl/Modoki-FirefoxCSS) by [soup-bowl](https://github.com/soup-bowl)
- [simplerentfox](https://github.com/MiguelRAvila/SimplerentFox) by [MiguelRAvila](https://github.com/MiguelRAvila)
- [verticaltabs](https://github.com/ranmaru22/firefox-vertical-tabs) by [ranmaru22](https://github.com/ranmaru22)
#### For theme makers
- a mechanism to handle theme variants:
- Variants can be declared in the same manifest file under the `variants` entry to override other entries
- per-OS paths: the {{ variant }} and {{ os }} placeholders get replaced with their values
- the value {{ os }} gets replaced with can be customized in the manifest file under the `os` entry, use `null` to mark an OS as incompatible
- Support for helper addons:
- Declare URLs to open after installation under the `addons` manifest entry (I plan to auto-install them in the future)
- Easy way to write about:config changes without writing a `user.js` file:
- Use the `config` manifest entry
- If you also use a `user.js`, you can combine both, they'll be written as a single `user.js` file
- Support for custom assets:
- Use the `assets` manifest entry to list out your assets
- Supports glob patterns
- If you store them under a `chrome` directory, you can use `copy from: chrome/` so that they don't get copied to `<profile directory>/chrome/chrome`

[Unreleased]: https://github.com/ewen-lbh/ffcss/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/ewen-lbh/ffcss/compare/v0.1.2...v0.2.0
[0.1.2]: https://github.com/ewen-lbh/ffcss/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/ewen-lbh/ffcss/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/ewen-lbh/ffcss/releases/tag/v0.1.0

[//]: # (C3-2-DKAC:GGH:Rewen-lbh/ffcss:Tv{t})
name: no u
ffcss: 420
Themes left to do
-----------------


- [x] MartinFox
- [x] Monterey-Fox
- [x] pro-fox: enhanced firefox proton
- [x] Chameleons-Beauty to Firefox CSS
- [x] Technetium
- [x] WaveFox
- [x] Gradient rounded tabs redesign and ultra compact mode [Proton UI]
- [x] [Firefox 90+] RainFox
- [x] Lepton's Photon-Styled in Proton UI
- [x] Lepton(Proton Fix) in Proton UI
- [x] Firefox vertical tabs (TST) UI
- [x] Australis like tabs in Proton UI
- [x] DiamondFucsia Theme to Firefox Css
- [x] Compact Mode to Firefox Css
- [x] duskFox
- [x] Alpen Blue to Firefox CSS
- [x] WhiteSur Safari style for Firefox
- [x] Pseudo-fullscreen Firefox, Sidebery and YouTube
- [x] Moonlight 🌌
- [ ] Elegant Nord Theme with material design
- [ ] NicoFox
- [ ] Edge-FrFox
- [ ] Firefox i3wm theme
- [ ] EdgeFox
- [ ] FirefoxW10ContextMenus
- [ ] Flying Fox
- [ ] Monochrome Tree
- [ ] Almost Dark Proton
- [x] Firefox UWP Style
- [x] MaterialFox
- [ ] minimal-functional-fox
- [ ] quietfox
- [ ] Firefox-Mod-Blur
- [ ] My Firefox Theme - not-holar
- [ ] nord-firefox
- [ ] Firefox Review
- [x] blurredfox
- [x] SimplerentFox
- [ ] Simplify Darkish for Firefox
- [ ] Simplify Silver Peach for Firefox
- [ ] FirefoxCss
- [ ] minimalFOX
- [ ] FirefoxCSS-Plus
- [ ] Sweet_Pop!
- [ ] My Firefox theme - u/FffDtark
# Needs: #44
ffcss: 1

# TODO
download: https://github.com/aminomancer/uc.css.js
# Needs: #47
ffcss: 1

name: moonlight
download: https://github.com/eduardhojbota/moonlight-userChrome

userChrome: userChrome.css
userContent: userContent.css
assets:
  - '*.css'
  - custom/**
  - icons/**

os:
  linux: macos

variants:
  Re-enable title bar controls (MIN-MAX-CLOSE buttons):
    modifications:
      - in: userChrome.css
        append: '@import "custom/_titlebar-controls-enable-{{ os }}.css";'
  Disable megabar behavior:
    modifications:
      - in: userChrome.css
        append: '@import "custom/_megabar-disable.css";'
  Add Developer Tools styling:
    modifications:
      - in: userChrome.css
        append: '@import "custom/_dev-tools.css";'
    message:
      Inspired by [moonlight-vscode-theme](https://github.com/atomiks/moonlight-vscode-theme) and [github-moonlight](https://github.com/Brettm12345/github-moonlight)

      You can also set the theme to dark in Firefox Developer Tools settings.

message: |
  Inspired by [moonlight-vscode-theme](https://github.com/atomiks/moonlight-vscode-theme) and [github-moonlight](https://github.com/Brettm12345/github-moonlight)
# Needs: #48
ffcss: 1

name: pseudo-fullscreen
download: https://github.com/ongots/pseudo-fullscreen-firefox

config:
  layout.css.backdrop-filter.enabled: true
  gfx.webrender.enabled: true

assets:
  - chrome/**
copy from: chrome/

message: |
  ## To complete the setup

  1. Drag Bookmarks Toolbar Items to the left of the address bar. Disable Bookmarks Toolbar
  1. To set the wallpaper in a new tab, place a .jpg or .png image in the Chrome folder and rename it to "bg". Restart Firefox

variants:
  default: {}
  sidebery:
    addons:
      - https://addons.mozilla.org/firefox/addon/sidebery/
    message:
      append: | # TODO to append to root variant's instead of overriding
        1. Copy the code from Sidebery-Sidebar.css to Sidebery / Settings / Styles editor / Sidebar
        1. Copy the code from Sidebery-GroupPage.css to Sidebery / Settings / Styles editor / Group page
  youtube:
    addons:
      - https://addons.mozilla.org/firefox/addon/styl-us/
    message:
      append: |
        1. Open YouTube
        1. In Stylus, click New Style ...
        1. In the window that opens, click Import
        1. Copy the code here from the Stylus-YouTube.css file and click replace style
        1. Save

components:
  Open bookmark in new tab:
    config:
      browser.tabs.loadBookmarksInTabs: true
  Open tabs to the right of the current one:
    config:
      browser.tabs.insertAfterCurrent: true
  Scroll step:
    config:
      mousewheel.min_line_scroll_amount: 25
  Smoothness of scrolling:
    config:
      general.smoothScroll.mouseWheel.durationMaxMS: 380
  Disable sponsored sites:
    config:
      browser.newtabpage.activity-stream.showSponsored: false
  Disable add-on recommendations:
    config:
      extensions.htmlaboutaddons.recommendations.enabled: false
# Needs: #15
ffcss: 1

name: rainfox
download: https://github.com/1280px/rainfox

userChrome: userChrome.css

firefox:
  90+: {}
  0-89:  # TODO maybe a syntax to 'up to ...?' (literally just allow 'up to 89' as a key)
    tag: v0.3


# TODO a way to glob 'everything except... (e.g, here, userChrome*.css except userChromeTabParanoia.css)
assets:
  - .icon/**
  - userChromeAnimations.css
  - userChromeBookmarks.css
  - userChromeContextMenus.css
  - userChromeDownloads.css
  - userChromeHourglassLoading.css
  - userChromeIcons.css
  - userChromeMenubar.css
  - userChromeNavButtons.css
  - userChromeOptHider.css
  - userChromePageCustomize.css
  - userChromeTabContainers.css
  - userChromeTabbar.css
  - userChromeTabs.css
  - userChromeURLbar.css
  - userChromeVariables.css

config:
  svg.context-properties.content.enabled: true
  security.secure_connection_icon_color_gray: false

variants:
  default: {}
  paranoia:
    description: Unselected tabs are hidden until you hover over them
    assets:
      - .icon/**
      - userChrome*.css
# Needs: #15
ffcss: 1

name: UWP
download: https://github.com/Guerra24/Firefox-UWP-Style

# evolution of the firefox: entry,
# now firefox: <number> means firefox: {<number>: {}}
firefox:
  nightly:
    download: https://github.com/Guerra24/Firefox-UWP-Style/archive/refs/heads/master.zip
  89-90:
    download: https://github.com/Guerra24/Firefox-UWP-Style/archive/refs/heads/firefox-89.zip
  '88':
    download: https://github.com/Guerra24/Firefox-UWP-Style/archive/refs/heads/firefox-88.zip
  76-87:
    download: https://github.com/Guerra24/Firefox-UWP-Style/archive/refs/heads/firefox-76.zip

variants:
  default: {}
  acrylic blur:
    config:
      layout.css.backdrop-filter.enabled: true

userContent: userContent.css
userChrome: userChrome.css
assets:
  - custom*.css
  - css/**
  - img/**
# Needs: #47
ffcss: 1

name: whitesur
download: https://github.com/adamxweb/WhiteSurFirefoxThemeMacOS

userChrome: chrome/userChrome.css
user.js: configuration/user.js

assets:
  - chrome/WhiteSur/**
copy from: chrome/

# TODO: assets: allow items of {from: string; to: string} objects, get rid of `copy from` (breaking change!)

variants:
  Left hand side tab close:
    description: Put close button of tabs on the left
    assets:
      - from: custom/lhsclose.css
        to: chrome/WhiteSur/parts/lhsclose.css
    # TODO add modifications directive: in, after, before, append, prepend, replace, with, !regex tag for replace, in, before and after
    modifications:
      - in: chrome/WhiteSur/theme.css
        append: '@import "parts/lhsclose.css"'
        after: !regex '@namespace xul .*;'
  Standard URL Bar:
    assets:
      - from: custom/standard-urlbar.css
        to: chrome/WhiteSur/parts/standard-urlbar.css
    modifications:
      - in: chrome/WhiteSur/theme.css
        append: '@import "parts/standard-urlbar.css"'
        after: !regex '@namespace xul .*;'
  Swap navbar close buttons:
    description: Swap the close buttons, as well as to re-order the close button from MacOS styling to Windows
    assets:
      - from: custom/windows-swapclose.css
        to: chrome/WhiteSur/parts/windows-swapclose.css
    modifications:
      - in: chrome/WhiteSur/theme.css
        append: '@import "parts/windows-swapclose.css"'
        after: !regex '@namespace xul .*;'
  No Identity tab colours:
    description: Remove the identity line from a container or facebook tab
    assets:
      - from: custom/noidentity.css
        to: chrome/WhiteSur/parts/noidentity.css
    modifications:
      - in: chrome/WhiteSur/theme.css
        append: '@import "parts/noidentity.css"'
        after: !regex '@namespace xul .*;'
ffcss: 0

name: alpenblue
download: https://github.com/Godiesc/AlpenBlue


user.js: user.js
assets:
  - chrome/**
copy from: chrome/

addons:
  - https://addons.mozilla.org/en-US/firefox/addon/beautiful-alpen-blue/
ffcss: 0

name: australis-tabs
download: https://github.com/sagars007/Australis-like-tabs-FF-ProtonUI-changes

userChrome: userChrome.css
user.js: user.js

addons:
  - https://addons.mozilla.org/en-US/firefox/addon/alpenglow-dark/
ffcss: 0

name: blurredfox
download: https://github.com/manilarome/blurredfox

user.js: user.js
userChrome: userChrome.css
userContent: userContent.css
assets:
  - colors/**
  - parts/**
  - scrots/**
ffcss: 0

name: chameleons-beauty
download: https://github.com/Godiesc/Chameleons-Beauty

user.js: user.js

assets:
  - chrome/**

copy from: chrome/

addons:
  - https://addons.mozilla.org/es/firefox/user/7024772/

variants:
  default: {}
  dark:
    addons:
      - https://addons.mozilla.org/es/firefox/addon/beautiful-opera-gx-proton/
ffcss: 0

name: compactmode
download: https://github.com/Godiesc/compactmodefirefoxcss

user.js: user.js
userChrome: chrome/userChrome.css

addons:
  - https://addons.mozilla.org/en-US/firefox/addon/beautifultotaldark/

variants:
  default: {}
  photon-tabs:
    user.js: OtherCss/user.js
    userChrome: OtherCss/chrome/userChrome.css
ffcss: 0

name: diamondfucsia
download: https://github.com/Godiesc/DiamondFucsia


user.js: user.js
userChrome: chrome/userChrome.css
ffcss: 0

name: flyingfox
download: https://github.com/akshat46/FlyingFox

user.js: user.js
assets:
  - chrome/**

copy from: chrome/

# components:
#   Sidebar Static:
#     description: Permanently expanded sidebar
#     modifications:
#       - in: config.css
#         with: '@import "userChrome-static.css";'
#         replace: '@import "userChrome-hover.css";'
#   Sidebar Hover:
#     description: Sidebar shown on mouse hover
#     modifications:
#       - in: config.css
#         replace: '@import "userChrome-static.css";'
#         with: '@import "userChrome-hover.css";'
#   Extension Icons:
#     description: >
#       Supported extension icons are changed, and a mask(generated for custom color with webapp)
#       is applied to unsupported ones to blend them better with rest of the theme.
#     modifications:
#       - in: config.css
#         prepend: '@import "icons/extension-icons.css";'
#   Right Window Controls:
#     incompatible with:
#       - component:horizontal-tabs
#     description: Move window controls to the right
#     modifications:
#       - in: config.css
#         prepend: |
#           @import ''
#   Horizontal tabs:
#     description: Use a traditional horizontal tab line
#     modifications:
#       - in: config.css
#         replace: '@import "hide-tabline.css";'
#         with: ''
#       - in: config.css
#         replace: '@import "window-controls/wc-without-tabline.css";'
#         with: ''
#       - in: config.css
#         replace: '@import "window-controls/wc-wt-windowspatch.css";'
#         with: ''
#
# if:
#   # os:windows and not component:horizontal-tabs and ask:Are you running Windows 10:
#   windows:10 and not component:horizontal-tabs:
#     modifications:
#       - in: config.css
#         prepend: '@import "window-controls/wc-wt-windowspatch.css";'

message: |
  New firefox themes _alpenglow_ and _default_ aren't supported.

#  If you are on Windows 10 and did not use Horizontal Tabs,
#  please add
#
#  ```css
#  @import "window-controls/wc-wt-windowspatch.css";
#  ```
#
#  to **config.css**
ffcss: 0

name: frozenfox
download: https://github.com/tortious/FrozenFox

userChrome: userChrome.css
userContent: userContent.css

assets:
  - bindings.xml
  - userChrome.js
  - userChrome.xml
  - 000_xPref.uc.js
  - saveUCJS.uc.js
  - Png/**
ffcss: 0

name: fxcompact
download: https://github.com/dannycolin/fx-compact-mode

config:
  browser.uidensity: 1

userChrome: userChrome.css
ffcss: 0

name: halo
download: https://github.com/seirin-blu/Firefox-Halo

userChrome: userChrome.css
userContent: userContent.css
assets:
  - icon/**
  - svg/**
  - Firefox assets/**
  - '*.css'
  - firefox_*.png
ffcss: 0

name: lepton
download: https://github.com/black7375/Firefox-UI-Fix

user.js: user.js
userChrome: userChrome.css
userContent: userContent.css

asssets:
  - icons/**

variants:
  default: {}
  photon-style:
    branch: photon-style
  proton-style:
    branch: proton-style
ffcss: 0

name: martinfox
download: https://github.com/arp242/MartinFox

userChrome: userChrome.css
ffcss: 0

name: materialfox-plus
download: https://github.com/typeling1578/MaterialFox-Plus

config:
  svg.context-properties.content.enabled: true
  browser.tabs.tabClipWidth: 83
  materialFox.reduceTabOverflow: true
  security.insecure_connection_text.enabled: true

userChrome: chrome/userChrome.css
userContent: null
user.js: null
assets:
  - chrome/**

copy from: chrome/
ffcss: 0

name: materialfox
download: https://github.com/muckSponge/MaterialFox

config:
  svg.context-properties.content.enabled: true
  browser.tabs.tabClipWidth: 83
  materialFox.reduceTabOverflow: true
  security.insecure_connection_text.enabled: true

userChrome: chrome/userChrome.css
userContent: null
user.js: user.js
assets:
  - chrome/**

copy from: chrome/
ffcss: 0

name: modoki
download: https://github.com/soup-bowl/Modoki-FirefoxCSS

assets:
  - IE6/chrome/**

copy from: IE6/chrome/

message:
  In Customize, turn on Title Bar and enable Menu bar under toolbars.
ffcss: 0

name: montereyfox
download: https://github.com/FirefoxCSSThemers/Monterey-Fox

assets:
  - chrome/**

copy from: chrome/
ffcss: 0

name: pro-fox
download: https://github.com/xmansyx/Pro-Fox

userChrome: userChrome.css
ffcss: 0

name: proton-connected-rounded-tabs
download: https://github.com/sagars007/Proton-UI-connected-rounded-tabs

userChrome: userChrome.css
user.js: user.js
ffcss: 0

name: simplerentfox
download: https://github.com/MiguelRAvila/SimplerentFox

config:
  layers.acceleration.force-enabled: true
  gfx.webrender.all: true
  svg.context-properties.content.enabled: true

variants:
  HideURLBar: {}
  WithURLBar: {}
  OneLine: {}

os:
  linux: Linux
  macos: Linux
  windows: Windows

userContent: ./{{ os }}/userContent.css
userChrome: ./{{ os }}/userChrome__{{ variant }}.css

addons:
  - https://addons.mozilla.org/en-US/firefox/addon/simplerentfox/

# without {{ variables }}:
# files:
#  windows:
#  - Windows/userContent.css
#  linux:
#  - Linux/userContent.css
#
# variants:
#   WithURLBar:
#     files:
#       windows:
#       - Windows/userChrome__WithURLBar.css
#       linux:
#       - Linux/userChrome__WithURLBar.css
#   WithoutURLBar:
#     files:
#       windows:
#       - Windows/userChrome__WithoutURLBar.css
#       linux:
#       - Linux/userChrome__WithoutURLBar.css
#   OneLine:
#     files:
#       windows:
#       - Windows/userChrome__WithURLBar.css
#       linux:
#       - Linux/userChrome__OneLWithURLBar
ffcss: 0

name: technetium

download: https://github.com/edo0/Technetium

user.js: user.js
assets:
  - chrome/**
copy from: chrome/

# TODO: variant for https://github.com/edo0/Technetium#really-missing-tab-separators
# Needs: #47

# variants:
#   Default: {}
#   Tab separators:
#     description: Restore tab separators
#     modifications:
#     - in: chrome/userChrome.css
#       prepend: !fromfile https://pastebin.com/raw/yHggpULx
#       before: !regex ^}$
ffcss: 0

name: verticaltabs
download: https://github.com/ranmaru22/firefox-vertical-tabs

userChrome: userChrome.css

config:
  dom.animations-api.getAnimations.enabled: true # from https://framagit.org/ariasuni/tabcenter-reborn/-/wikis/home

addons:
  - https://addons.mozilla.org/en-US/firefox/addon/tabcenter-reborn/

message: |
  Go to about:addons, select Tab Center Reborn, go to Preferences and set:

  - *Animations*: on.
  - *Use current browser theme*: on, if you want to use dark mode.
  - *Compact Mode*: either “Dynamic” or “Enabled”. It works with “Disabled” too but looks nicer with only favicons.
  - *Favicon-only pinned tabs*: off.
  - *Activate Custom Stylesheet* and paste the contents of tabCenterReborn.css into the text area, and click “Save CSS”.
ffcss: 0

name: wavefox
description: |
  A theme with a unique shape of tabs

  _Compatible with touch, normal & compact densities_

download: https://github.com/QNetITQ/WaveFox

config:
  svg.context-properties.content.enabled: true
  layout.css.color-mix.enabled: true
  browser.tabs.tabMinWith: 94

userChrome: userChrome.css
#!/usr/bin/env ruby
$; = $, = "\n"
arg = ARGV[0]

major, minor, patch = `ffcss version`.split '.'

case arg
when "major"
	new_major, new_minor, new_patch = major.to_i + 1, 0, 0
when "minor"
	new_major, new_minor, new_patch = major, minor.to_i + 1, 0
when "patch"
	new_major, new_minor, new_patch = major, minor, patch.to_i + 1
else; if arg =~ /\d+.\d+.\d+/
	new_major, new_minor, new_patch = arg.split '.'
else
	puts "invalid argument #{arg}"
	exit 1
end
end


ffcss_dot_go = File.open "ffcss.go"

File.write "ffcss.go", (
	ffcss_dot_go.read
		.sub(/VersionMajor = \d+/, "VersionMajor = #{new_major}")
		.sub(/VersionMinor = \d+/, "VersionMinor = #{new_minor}")
		.sub(/VersionPatch = \d+/, "VersionPatch = #{new_patch}")
)

if not ["major", "minor", "patch"].include? arg
	exit 0
end

lines = File.open("CHANGELOG.md").read.split
release_notes = []


until lines.length == 0 or lines.shift =~ /^## \[#{new_major}.#{new_minor}.#{new_patch}\] - \d{4}-\d{2}-\d{2}$/ do
	# nothing
end

until lines.length == 0 or lines[0] =~ /^## \[\d+\.\d+\.\d+\] - \d{4}-\d{2}-\d{2}$/ do
	release_notes << lines.shift
end




File.write 'release_notes.md', release_notes.join
#!/usr/bin/env ruby
require "pathname"
require "tty-prompt"

name = ARGV[0]
from = Pathname.new ARGV[1]
to = Pathname.new ARGV[2]

body = []
doc = []
start_at = end_at = 0

lines = File.read(from).split "\n"

signature_at = lines.find_index { |line| /^func\s+(\([^\)]+\)\s+)?\s*#{name}\s*\([^\)]*\).+\{$/i =~ line }

if signature_at.nil?
	puts "signature for #{name} not found"
	exit 1
end

# find documentation
lines[...signature_at].reverse.each_with_index do |line, i|
  start_at = signature_at - (i + 1)
  if line.start_with? "//"
    doc.prepend line
    if line.start_with? "// #{name}"
      break
    end
  else
    break
  end
end

# go thru body
lines[signature_at..].each_with_index do |line, at|
  body << line
  end_at = signature_at + at
  # closing brace w/o indetation means end of func.
  # deal with it, just gofmt your code.
  if line == "}"
    break
  end
end

to.write (([""] + doc + body).join "\n"), to.size, mode: "a"
from.write (lines[...start_at] + lines[end_at + 1..]).join "\n"
`make format`
#!/usr/bin/env ruby
require "yaml"

major_version = `ffcss version major`.strip.to_i

Dir.glob('themes/*.yaml').each do |filename|
	theme = YAML.load_file filename, symbolize_names: true
	if theme[:ffcss] != major_version
		return
	end
	parts = theme[:download].split '/'
	username = parts.length >= 2 ? parts[-2] : nil
	puts "- [#{theme[:name]}](#{theme[:download]})" + (username ? " by [#{username}](https://github.com/#{username})" : "")
end
#!/usr/bin/env ruby
require "pathname"
tested = []
untested = []

files = Dir.glob "*.go"
testfiles = Dir.glob "*_test.go"
function_pattern = /^func\s+(\(\s*\w+\s+(?<receiver>[^)\s]+)\s*\)\s+)?\s*(?<name>\w+).+\{$/

testfiles.each do |file|
  lines = Pathname.new(file).read.split "\n"
  lines.each do |line|
    match = line.match function_pattern
    if match != nil and match[:name].start_with? "Test"
      tested << match[:name].delete_prefix("Test")
    end
  end
end

files.each do |file|
  lines = Pathname.new(file).read.split "\n"
  lines.each_with_index do |line, i|
    match = line.match function_pattern
    if match != nil and not match[:name].start_with? "Test" and match[:name] !~ /main|init/
      if not tested.find { |func| func.downcase == match[:name].downcase } and not file.end_with? "_test.go"
        untested << {
          name: (if match[:receiver] then "(#{match[:receiver]})." else "" end) + match[:name],
          # name: match[:name],
          location: "#{file}:#{i + 1}",
        }
      end
    end
  end
end

def align(untested, func, key, right: false)
  longest_length = untested.map { |f| f[key].length }.max
  if right
    func[key].rjust longest_length
  else
    func[key].ljust longest_length
  end
end

if untested.size == 0
  puts "everything's tested"
  exit 0
end

untested.each do |func|
  puts (align untested, func, :location) + " "  + (align untested, func, :name)
end

puts
puts (if untested.size > 1
  "#{untested.size} functions"
else
  "one function"
end) + ", " + (Float(untested.size) / (untested.size + tested.size) * 100).round.to_s + "%"

exit 1
#!/usr/bin/env ruby
require "pathname"

name = ARGV[0]
files = Dir.glob "*.go"

files.each do |file|
	lines = Pathname.new(file).read.split"\n"
	lines.each do |line|
		signature_at = lines.find_index { |line| /^func\s+(\([^\)]+\)\s+)?\s*#{name}\s*\([^\)]*\).+\{$/ =~ line }
		if signature_at != nil
			puts "#{file}:#{signature_at+1}"
			exit 0
		end
	end
end

exit 1
package ffcss

import (
	"fmt"
	"os"
	"regexp"
	"strings"

	"github.com/AlecAivazis/survey/v2"
	chromaQuick "github.com/alecthomas/chroma/quick"
	"github.com/charmbracelet/glamour"
	"github.com/mitchellh/colorstring"
)

const indent = "  "

// BulletColorsByIndentLevel maps (the "key" is the element's index in the array) an indentation level (see LogStep, for example)
// to a color.
var BulletColorsByIndentLevel = []string{
	"blue",
	"cyan",
	"green",
	"yellow",
	"orange",
	"red",
	"magenta",
	"white",
}

var colorizer colorstring.Colorize

func init() {
	colorizer.Colors = colorstring.DefaultColors
	colorizer.Colors["italic"] = "3"
	colorizer.Colors["orange"] = "38;2;241;109;12"
	colorizer.Reset = true
}

func printf(s string, args ...interface{}) {
	fmt.Fprintf(out, s, args...)
}

func printfln(s string, args ...interface{}) {
	printf(s+"\n", args...)
}

// DescribeTheme shows the introduction message before installation
func DescribeTheme(theme Theme, indentLevel uint) {
	printf("\n")
	indentation := strings.Repeat(indent, int(indentLevel))

	var author string
	urlParts := strings.Split(theme.DownloadAt, "/")
	LogDebug("urlParts is %#v", urlParts)
	if theme.Author != "" {
		author = theme.Author
	} else if strings.Contains(theme.DownloadAt, "github.com") && len(urlParts) == 5 {
		author = urlParts[len(urlParts)-2]
	}

	printf(indentation)

	printf(
		colorizer.Color("[dim]Installing ") +
			colorizer.Color("[blue][bold]"+theme.Name()),
	)

	if regexp.MustCompile(`^v([0-9\.]+)$`).MatchString(theme.Tag) {
		printf(colorstring.Color(" [blue]" + theme.Tag))
	}

	if author != "" {
		printf(
			colorizer.Color("[dim][italic] by ") +
				colorizer.Color("[blue][italic]"+author),
		)
	}

	if theme.Description != "" {
		printf("\n")
		gutter := colorstring.Color(indentation + "[blue]│")
		LogDebug("gutter is %q", gutter)
		markdownRendered, err := glamour.Render(theme.Description, "dark")
		if err != nil {
			markdownRendered = theme.Description
		}
		printf("\n")
		LogDebug("splitted is %#v", strings.Split(markdownRendered, "\n"))
		for _, line := range strings.Split(markdownRendered, "\n") {
			if strings.TrimSpace(line) == "" {
				continue
			}
			printfln(gutter + strings.TrimSpace(line))
		}
		printf("\n")
	} else {
		printf("\n\n")
	}

}

func showManifestSource(theme Theme) {
	printf("\n")
	printfln(colorizer.Color("[italic][dim]" + theme.Name() + "'s manifest"))
	chromaQuick.Highlight(os.Stdout, theme.raw, "YAML", "terminal16m", "pygments")
	printf("\n")
}

func plural(singular string, amount int, optionalPlural ...string) string {
	var plural string
	switch len(optionalPlural) {
	case 1:
		plural = optionalPlural[0]
	case 0:
		plural = singular + "s"
	default:
		panic("plural expected 2 or 3 arguments, you gave more")
	}
	if amount == 1 {
		return singular
	}
	return plural
}

// LogDebug prints a debug log line. This one always prints to the real stdout, ignoring a possibly mocked stdout
func LogDebug(s string, fmtArgs ...interface{}) {
	if os.Getenv("DEBUG") != "" {
		fmt.Printf(colorizer.Color("[dim][ DEBUG ] "+s+"\n"), fmtArgs...)
	}
}

// LogWarning prints a log line with "warning" styling
func LogWarning(s string, fmtArgs ...interface{}) {
	printf(colorizer.Color("[yellow][bold]"+s+"\n"), fmtArgs...)
}

// LogError is like warn but with "error" styling
func LogError(s string, fmtArgs ...interface{}) {
	printf(colorizer.Color("[red][bold]"+s+"\n"), fmtArgs...)
}

// LogStep displays a list item
func LogStep(indentLevel uint, item string, fmtArgs ...interface{}) {
	LogStepC("•", indentLevel, item, fmtArgs...)
}

// LogStepC is like Step, but the bullet point characters is customizable
func LogStepC(bulletChar string, indentLevel uint, item string, fmtArgs ...interface{}) {
	indentLevel += BaseIndentLevel
	var color string
	if int(indentLevel) > len(BulletColorsByIndentLevel)-1 {
		color = BulletColorsByIndentLevel[len(BulletColorsByIndentLevel)-1]
	} else {
		color = BulletColorsByIndentLevel[indentLevel]
	}

	bullet := strings.Repeat(indent, int(indentLevel)) +
		colorizer.Color("["+color+"]"+bulletChar)

	printfln(bullet + " " + colorizer.Color(strings.TrimSpace(fmt.Sprintf(item, fmtArgs...))))
}

// Display is like String, but adds terminal ANSI sequences for some color.
func (ffp FirefoxProfile) Display() string {
	return colorizer.Color(fmt.Sprintf("[bold]%s [reset][dim](%s)", ffp.Name, ffp.ID))
}

// AskProfiles prompts the user to select one or more profiles from the given array, and returns the user's chosen profiles.
func AskProfiles(profiles []FirefoxProfile) []FirefoxProfile {
	var selectedProfiles []FirefoxProfile

	// XXX the whole display thing should be put in survey.MultiSelect.Renderer, look into that.
	selectedProfileDirsDisplay := make([]string, 0)

	LogStep(0, "Please select profiles to apply the theme on")

	profileDirsDisplay := make([]string, 0)
	for _, profile := range profiles {
		profileDirsDisplay = append(profileDirsDisplay, profile.Display())
	}

	survey.AskOne(&survey.MultiSelect{
		Message: "Select profiles",
		Options: profileDirsDisplay,
		VimMode: vimModeEnabled(),
	}, &selectedProfileDirsDisplay)

	for _, chosenProfileDisplay := range selectedProfileDirsDisplay {
		selectedProfiles = append(selectedProfiles, NewFirefoxProfileFromDisplay(chosenProfileDisplay, profiles))
	}

	return selectedProfiles
}

// AskToSeeManifestSource prompts the user to display the theme's manifest, and, if the user accepts, displays it.
func (t Theme) AskToSeeManifestSource(skip bool) {
	wantsSource := false
	if !skip {
		survey.AskOne(&survey.Confirm{
			Message: "Show the manifest source?",
		}, &wantsSource)
	}
	if wantsSource {
		showManifestSource(t)
	}
}

// ChooseVariant asks the user to choose a variant.
// If the users interrupts the prompt (by e.g. pressing Ctrl-C), cancel is true.
// Else, the selected variant is returned and cancel is false.
// If no variants are available, the empty variant is returned and cancel is false (and the user does not get prompted).
func (t Theme) ChooseVariant() (chosen Variant, cancel bool) {
	var variantName string
	if len(t.AvailableVariants()) > 0 {
		LogStep(0, "Please choose the theme's variant")
		variantPrompt := &survey.Select{
			Message: "Install variant",
			Options: t.AvailableVariants(),
			VimMode: vimModeEnabled(),
		}
		survey.AskOne(variantPrompt, &variantName)
		// user Ctrl-C'd
		if variantName == "" {
			return Variant{}, true
		}
		return t.Variants[variantName], false
	}
	return Variant{}, false
}

// ConfirmInstallAddons asks the user to confirm the installation of addons.
func ConfirmInstallAddons(addons []string) bool {
	acceptOpenExtensionPages := false
	survey.AskOne(&survey.Confirm{
		Message: fmt.Sprintf("This theme suggests installing %d %s. Open %s?",
			len(addons),
			plural("addon", len(addons)),
			plural("its page", len(addons), "their pages"),
		),
		Default: acceptOpenExtensionPages,
	}, &acceptOpenExtensionPages)
	return acceptOpenExtensionPages
}

// ShowHookOutput displays the given output text with additional horizontal and vertical padding
func ShowHookOutput(output string) {
	fmt.Fprint(
		out,
		"\n",
		prefixEachLine(
			strings.TrimSpace(output),
			strings.Repeat(indent, int(BaseIndentLevel)+2),
		),
		"\n",
		"\n",
	)
}

// DisplayErrorMessage displays a nested error message (split on colons)
func DisplayErrorMessage(err error) {
	for idx, errorFragment := range strings.Split(err.Error(), ":") {
		LogStep(uint(idx), errorFragment)
	}
}

// SelectProfiles returns an array of FirefoxProfile:
//
//    If selected is non-empty, it parses the paths into an array of FirefoxProfile
//    Else, it returns all profiles if all is true
//    Else, it asks the user to select one or more profiles and returns those
func SelectProfiles(selected []string, dir string, useDefault bool, all bool) ([]FirefoxProfile, error) {
	var selectedProfiles []FirefoxProfile
	if len(selected) > 0 {
		for _, profilePath := range selected {
			selectedProfiles = append(selectedProfiles, NewFirefoxProfileFromPath(profilePath))
		}
	} else {
		LogStep(0, "Getting profiles")
		profiles, err := Profiles(dir)
		if err != nil {
			return []FirefoxProfile{}, fmt.Errorf("couldn't get profile directories: %w", err)
		}
		if useDefault {
			for _, profile := range profiles {
				if profile.Name == "default-release" {
					return []FirefoxProfile{profile}, nil
				}
			}
		}
		// Choose profiles
		// TODO smart default (based on {{profileDirectory}}/times.json:firstUse)
		if all {
			LogStep(0, "Selecting all profiles")
			selectedProfiles = profiles
		} else {
			selectedProfiles = AskProfiles(profiles)
		}
	}
	return selectedProfiles, nil
}
package ffcss
package ffcss

import (
	"encoding/json"
	"fmt"
	"regexp"

	"strings"
)

// UserJSFileContent returns a string of JS source code that represents the theme's config.
// It can be used directly to write a user.js file.
func (t Theme) UserJSFileContent() (string, error) {
	lines := make([]string, 0)
	for name, value := range t.Config {
		valueJSON, err := json.Marshal(value)
		if err != nil {
			return "", fmt.Errorf("can't serialize %#v: %w", value, err)
		}
		lines = append(lines, fmt.Sprintf(`user_pref(%q, %s);`, name, string(valueJSON)))
	}
	return strings.Join(lines, "\n"), nil
}

// ValueOfUserPrefCall returns the value of configuration entry, given its key and the contents of
// the prefs.js file. It only works if the value is a JSON-parsable literal (string, number, boolean, null, etc.).
func ValueOfUserPrefCall(prefsJSContent []byte, key string) (string, error) {
	pattern := regexp.MustCompile(`(?m)^\s*user_pref\("` + key + `"\s*,\s*(.+)\)\s*;?\s*$`)
	matches := pattern.FindAllSubmatch(prefsJSContent, -1)
	if len(matches) == 0 {
		return "", fmt.Errorf("key %q not found", key)
	}
	raw := matches[len(matches)-1][1]
	var jsonParsed interface{}
	err := json.Unmarshal(raw, &jsonParsed)
	if err != nil {
		return "", fmt.Errorf("while intepreting value %q: %w", string(raw), err)
	}
	return fmt.Sprint(jsonParsed), nil
}
package ffcss

import (
	"sort"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

var (
	prefsContent = []byte(`
// Mozilla User Preferences

// DO NOT EDIT THIS FILE.
//
// If you make changes to this file while the application is running,
// the changes will be overwritten when the application exits.
//
// To change a preference value, you can either:
// - modify it via the UI (e.g. via about:config in the browser); or
// - set it within a user.js file in your profile.

user_pref("app.normandy.first_run", false);
user_pref("app.normandy.migrationsApplied", 12);
user_pref("app.normandy.startupRolloutPrefs.browser.partnerlink.useAttributionURL", true);
user_pref("app.normandy.startupRolloutPrefs.browser.topsites.contile.enabled", true);
user_pref("app.normandy.startupRolloutPrefs.browser.topsites.experiment.ebay-2020-1", true);
user_pref("app.normandy.startupRolloutPrefs.browser.topsites.useRemoteSetting", true);
user_pref("app.normandy.user_id", "ca21ac2d-9836-4135-8643-90baefe2bcce");
user_pref("app.update.lastUpdateTime.addon-background-update-timer", 1626868179);
user_pref("app.update.lastUpdateTime.browser-cleanup-thumbnails", 1626957215);
user_pref("app.update.lastUpdateTime.recipe-client-addon-run", 1626867939);
user_pref("app.update.lastUpdateTime.region-update-timer", 1626627835);
user_pref("app.update.lastUpdateTime.rs-experiment-loader-timer", 1626957492);
user_pref("app.update.lastUpdateTime.search-engine-update-timer", 1626957335);
user_pref("app.update.lastUpdateTime.services-settings-poll-changes", 1626868059);
user_pref("app.update.lastUpdateTime.telemetry_modules_ping", 1626624835);
user_pref("app.update.lastUpdateTime.xpi-signature-verification", 1626868299);
user_pref("browser.aboutConfig.showWarning", false);
user_pref("browser.bookmarks.addedImportButton", true);
user_pref("browser.bookmarks.restore_default_bookmarks", false);
user_pref("browser.compactmode.show", true);
user_pref("browser.contentblocking.category", "standard");
user_pref("browser.contextual-services.contextId", "{9974547d-fdc8-4671-866c-b1138fcfca4b}");
user_pref("browser.download.panel.shown", true);
user_pref("browser.download.viewableInternally.typeWasRegistered.svg", true);
user_pref("browser.download.viewableInternally.typeWasRegistered.webp", true);
user_pref("browser.download.viewableInternally.typeWasRegistered.xml", true);
user_pref("browser.laterrun.bookkeeping.profileCreationTime", 1625838587);
user_pref("browser.laterrun.bookkeeping.sessionCount", 28);
user_pref("browser.laterrun.enabled", true);
user_pref("browser.migration.version", 109);
user_pref("browser.newtabpage.activity-stream.impressionId", "{3238e08e-13c5-4a46-8248-d6e73a0a7cea}");
user_pref("browser.newtabpage.pinned", "[]");
user_pref("browser.newtabpage.storageVersion", 1);
user_pref("browser.pageActions.persistedActions", "{\"ids\":[\"bookmark\"],\"idsInUrlbar\":[\"bookmark\"],\"idsInUrlbarPreProton\":[],\"version\":1}");
user_pref("browser.pagethumbnails.storage_version", 3);
user_pref("browser.proton.toolbar.version", 3);
user_pref("browser.region.update.updated", 1626627836);
user_pref("browser.rights.3.shown", true);
user_pref("browser.safebrowsing.provider.mozilla.lastupdatetime", "1626957187007");
user_pref("browser.safebrowsing.provider.mozilla.nextupdatetime", "1626978787007");
user_pref("browser.search.region", "FR");
user_pref("browser.sessionstore.upgradeBackup.latestBuildID", "20210714020445");
user_pref("browser.shell.defaultBrowserCheckCount", 15);
user_pref("browser.shell.didSkipDefaultBrowserCheckOnFirstRun", true);
user_pref("browser.startup.homepage_override.buildID", "20210714020445");
user_pref("browser.startup.homepage_override.mstone", "90.0");
user_pref("browser.startup.lastColdStartupCheck", 1626957491);
user_pref("browser.startup.upgradeDialog.version", 89);
user_pref("browser.tabs.tabClipWidth", 83);
user_pref("browser.tabs.tabMinWith", 94);
user_pref("browser.tabs.warnOnClose", false);
user_pref("browser.theme.toolbar-theme", 0);
user_pref("browser.toolbars.bookmarks.visibility", "always");
user_pref("browser.uiCustomization.state", "{\"placements\":{\"widget-overflow-fixed-list\":[],\"nav-bar\":[\"back-button\",\"forward-button\",\"stop-reload-button\",\"customizableui-special-spring1\",\"urlbar-container\",\"customizableui-special-spring2\",\"save-to-pocket-button\",\"downloads-button\",\"fxa-toolbar-menu-button\"],\"toolbar-menubar\":[\"menubar-items\"],\"TabsToolbar\":[\"tabbrowser-tabs\",\"new-tab-button\",\"alltabs-button\"],\"PersonalToolbar\":[\"import-button\",\"personal-bookmarks\"]},\"seen\":[\"save-to-pocket-button\",\"developer-button\"],\"dirtyAreaCache\":[\"nav-bar\",\"PersonalToolbar\",\"toolbar-menubar\",\"TabsToolbar\"],\"currentVersion\":17,\"newElementCount\":2}");
user_pref("browser.urlbar.placeholderName", "DuckDuckGo");
user_pref("browser.urlbar.placeholderName.private", "DuckDuckGo");
user_pref("browser.urlbar.suggest.calculator", true);
user_pref("browser.urlbar.tipShownCount.searchTip_onboard", 4);
user_pref("datareporting.policy.dataSubmissionPolicyAcceptedVersion", 2);
user_pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "1625838590028");
user_pref("devtools.everOpened", true);
user_pref("devtools.toolbox.splitconsoleEnabled", true);
user_pref("devtools.toolsidebar-height.inspector", 350);
user_pref("devtools.toolsidebar-width.inspector", 700);
user_pref("devtools.toolsidebar-width.inspector.splitsidebar", 350);
user_pref("distribution.Manjaro.bookmarksProcessed", true);
user_pref("distribution.iniFile.exists.appversion", "90.0");
user_pref("distribution.iniFile.exists.value", true);
user_pref("doh-rollout.balrog-migration-done", true);
user_pref("doh-rollout.doneFirstRun", true);
user_pref("doh-rollout.home-region", "FR");
user_pref("dom.push.userAgentID", "61d9a104140946528e51aa88fd08b343");
user_pref("extensions.activeThemeID", "{e0de5ee2-4619-413a-8300-a43a90196a6d}");
user_pref("extensions.blocklist.pingCountVersion", -1);
user_pref("extensions.databaseSchema", 33);
user_pref("extensions.getAddons.cache.lastUpdate", 1626868179);
user_pref("extensions.getAddons.databaseSchema", 6);
user_pref("extensions.incognito.migrated", true);
user_pref("extensions.lastAppBuildId", "20210714020445");
user_pref("extensions.lastAppVersion", "90.0");
user_pref("extensions.lastPlatformVersion", "90.0");
user_pref("extensions.pendingOperations", false);
user_pref("extensions.pictureinpicture.enable_picture_in_picture_overrides", true);
user_pref("extensions.reset_default_search.runonce.3", true);
user_pref("extensions.reset_default_search.runonce.reason", "previousRun");
user_pref("extensions.systemAddonSet", "{\"schema\":1,\"directory\":\"{49d78b53-bd0f-419d-9312-6068b0f686a2}\",\"addons\":{\"reset-search-defaults@mozilla.com\":{\"version\":\"2.0.0\"}}}");
user_pref("extensions.webcompat.enable_shims", true);
user_pref("extensions.webcompat.perform_injections", true);
user_pref("extensions.webcompat.perform_ua_overrides", true);
user_pref("extensions.webextensions.ExtensionStorageIDB.migrated.screenshots@mozilla.org", true);
user_pref("extensions.webextensions.uuids", "{\"doh-rollout@mozilla.org\":\"b9ecb8e7-691f-4134-8f98-105695f56aec\",\"formautofill@mozilla.org\":\"74a7def2-5ad4-4e34-a476-3b51818005f4\",\"pictureinpicture@mozilla.org\":\"bc991a02-d099-47c9-be28-2ecc10b7f7c2\",\"screenshots@mozilla.org\":\"7884d50d-6e0d-4f62-9bd8-91a964cf834a\",\"webcompat-reporter@mozilla.org\":\"210e0628-0f9e-42ae-a7cd-8b977803e6b8\",\"webcompat@mozilla.org\":\"eadb24f2-d0ab-4c7a-9026-c3ff60e721fb\",\"default-theme@mozilla.org\":\"b22d3a56-b8fe-452f-958f-d221d785395e\",\"google@search.mozilla.org\":\"66f66497-c162-4aa9-b096-938d49597de1\",\"wikipedia@search.mozilla.org\":\"080cb968-31e0-43ef-a84b-31f2c316a8df\",\"bing@search.mozilla.org\":\"29839e0b-ccb7-4ab6-849c-21e227b395bc\",\"ddg@search.mozilla.org\":\"e59e65ab-75df-46be-9a34-07a362d0e34a\",\"amazon@search.mozilla.org\":\"44a9435f-b258-4416-9824-ac2499e15fc4\",\"reset-search-defaults@mozilla.com\":\"d96dedb5-14b3-4175-b2a3-29a942302063\",\"{e0de5ee2-4619-413a-8300-a43a90196a6d}\":\"8b02f53b-0289-4b1d-895e-6c9f80541548\"}");
user_pref("fission.experiment.max-origins.last-disqualified", 0);
user_pref("fission.experiment.max-origins.last-qualified", 1625838590);
user_pref("fission.experiment.max-origins.qualified", true);
user_pref("gfx.webrender.all", true);
user_pref("idle.lastDailyNotification", 1626871507);
user_pref("layers.acceleration.force-enabled", true);
user_pref("layout.css.backdrop-filter.enabled", true);
user_pref("layout.css.color-mix.enabled", true);
user_pref("materialFox.reduceTabOverflow", true);
user_pref("media.gmp-gmpopenh264.abi", "x86_64-gcc3");
user_pref("media.gmp-gmpopenh264.lastUpdate", 1625840207);
user_pref("media.gmp-gmpopenh264.version", "1.8.1.1");
user_pref("media.gmp-manager.buildID", "20210714020445");
user_pref("media.gmp-manager.lastCheck", 1626867650);
user_pref("media.gmp.storage.version.observed", 1);
user_pref("network.trr.blocklist_cleanup_done", true);
user_pref("nimbus.syncdefaultsstore.upgradeDialog", "{\"slug\":\"upgradeDialog-defaultEnabled\",\"enabled\":true,\"targeting\":\"true\",\"variables\":{},\"description\":\"Turn on upgradeDialog by default for all users\"}");
user_pref("pdfjs.enabledCache.state", false);
user_pref("pdfjs.migrationVersion", 2);
user_pref("places.database.lastMaintenance", 1626626035);
user_pref("privacy.purge_trackers.date_in_cookie_database", "0");
user_pref("privacy.purge_trackers.last_purge", "1626871507933");
user_pref("privacy.sanitize.pending", "[]");
user_pref("security.insecure_connection_text.enabled", true);
user_pref("security.remote_settings.crlite_filters.checked", 1626883472);
user_pref("security.remote_settings.intermediates.checked", 1626882572);
user_pref("security.sandbox.content.tempDirSuffix", "0e4d920d-ffba-4a07-a1ab-2aa6389b2943");
user_pref("services.blocklist.addons-mlbf.checked", 1626957189);
user_pref("services.blocklist.gfx.checked", 1626957189);
user_pref("services.settings.clock_skew_seconds", -2);
user_pref("services.settings.last_etag", "\"1626944256233\"");
user_pref("services.settings.last_update_seconds", 1626957189);
user_pref("services.settings.main.anti-tracking-url-decoration.last_check", 1626957189);
user_pref("services.settings.main.cfr-fxa.last_check", 1626957189);
user_pref("services.settings.main.cfr.last_check", 1626957189);
user_pref("services.settings.main.doh-config.last_check", 1626957189);
user_pref("services.settings.main.doh-providers.last_check", 1626957189);
user_pref("services.settings.main.fxmonitor-breaches.last_check", 1626957189);
user_pref("services.settings.main.hijack-blocklists.last_check", 1626957189);
user_pref("services.settings.main.language-dictionaries.last_check", 1626957189);
user_pref("services.settings.main.message-groups.last_check", 1626957189);
user_pref("services.settings.main.nimbus-desktop-defaults.last_check", 1626957189);
user_pref("services.settings.main.nimbus-desktop-experiments.last_check", 1626957189);
user_pref("services.settings.main.normandy-recipes-capabilities.last_check", 1626957189);
user_pref("services.settings.main.partitioning-exempt-urls.last_check", 1626957189);
user_pref("services.settings.main.password-recipes.last_check", 1626957189);
user_pref("services.settings.main.pioneer-study-addons-v1.last_check", 1626957189);
user_pref("services.settings.main.public-suffix-list.last_check", 1626957189);
user_pref("services.settings.main.search-config.last_check", 1626957189);
user_pref("services.settings.main.search-default-override-allowlist.last_check", 1626957189);
user_pref("services.settings.main.search-telemetry.last_check", 1626957189);
user_pref("services.settings.main.sites-classification.last_check", 1626957189);
user_pref("services.settings.main.tippytop.last_check", 1626957189);
user_pref("services.settings.main.top-sites.last_check", 1626957189);
user_pref("services.settings.main.url-classifier-skip-urls.last_check", 1626957189);
user_pref("services.settings.main.websites-with-shared-credential-backends.last_check", 1626957189);
user_pref("services.settings.main.whats-new-panel.last_check", 1626957189);
user_pref("services.settings.security.onecrl.checked", 1626882572);
user_pref("services.sync.clients.lastSync", "0");
user_pref("services.sync.declinedEngines", "");
user_pref("services.sync.globalScore", 0);
user_pref("services.sync.nextSync", 0);
user_pref("services.sync.tabs.lastSync", "0");
user_pref("storage.vacuum.last.index", 1);
user_pref("storage.vacuum.last.places.sqlite", 1626626035);
user_pref("svg.context-properties.content.enabled", true);
user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);
user_pref("toolkit.startup.last_success", 1626957490);
user_pref("toolkit.telemetry.cachedClientID", "701ca6a7-c501-47d6-aef9-2a1d57a8d1eb");
user_pref("toolkit.telemetry.pioneer-new-studies-available", true);
user_pref("toolkit.telemetry.previousBuildID", "20210714020445");
user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);
user_pref("trailhead.firstrun.didSeeAboutWelcome", true);
	`)
)

func TestUserJSFileContent(t *testing.T) {
	value, err := Theme{Config: map[string]interface{}{
		"browser.tabs.tabClipWidth":              90,
		"svg.context-properties.content.enabled": true,
	}}.UserJSFileContent()

	if err != nil {
		panic(err)
	}

	assert.Equal(t,
		`user_pref("browser.tabs.tabClipWidth", 90);
user_pref("svg.context-properties.content.enabled", true);`,
		sortLines(value),
	)
}

func sortLines(s string) string {
	lines := strings.Split(s, "\n")
	sort.Strings(lines)
	return strings.Join(lines, "\n")
}

func TestValueOfUserPrefCall(t *testing.T) {
	result, err := ValueOfUserPrefCall(prefsContent, "storage.vacuum.last.index")
	assert.NoError(t, err)
	assert.Equal(t, "1", result)

	result, err = ValueOfUserPrefCall(prefsContent, "app.normandy.first_run")
	assert.NoError(t, err)
	assert.Equal(t, "false", result)

	result, err = ValueOfUserPrefCall(prefsContent, "browser.search.region")
	assert.NoError(t, err)
	assert.Equal(t, "FR", result)

	result, err = ValueOfUserPrefCall(prefsContent, "lkghjoertkjhoietrjhoirtjhoirtjhor")
	assert.Contains(t, err.Error(), `key "lkghjoertkjhoietrjhoirtjhoirtjhor" not found`)
	assert.Equal(t, result, "")
}
package ffcss

import (
	"fmt"
	"net/url"
	"os"
	"path/filepath"
	"strings"
)

// isValidURL tests a string to determine if it is a well-structured url or not.
func isValidURL(toTest string) bool {
	_, err := url.ParseRequestURI(toTest)
	if err != nil {
		return false
	}

	u, err := url.Parse(toTest)
	if err != nil || u.Scheme == "" || u.Host == "" {
		return false
	}

	return true
}

// getConfigDir returns the absolute path of ffcss's configuration directory
func getConfigDir() string {
	homedir, err := os.UserHomeDir()
	if err != nil {
		panic(fmt.Errorf("couldn't get your home directory: %w", err))
	}
	return filepath.Join(homedir, ".config", "ffcss")
}

// getCacheDir returns the temporary path for cloned repos and other stuff
func getCacheDir() string {
	homedir, err := os.UserHomeDir()
	if err != nil {
		panic(fmt.Errorf("couldn't get your home directory: %w", err))
	}
	return filepath.Join(homedir, ".cache", "ffcss")
}

// CacheDir joins the cache directory with the given path segments
func CacheDir(pathSegments ...string) string {
	return filepath.Join(getCacheDir(), filepath.Join(pathSegments...))
}

// ConfigDir joins the config directory with the given path segments
func ConfigDir(pathSegments ...string) string {
	return filepath.Join(getConfigDir(), filepath.Join(pathSegments...))
}

func cwd() string {
	wd, err := os.Getwd()
	if err != nil {
		panic(fmt.Errorf("couldn't get the current working directory: %w", err))
	}
	return wd
}

// renameIfExists renames from to to if from exists. If it doesn't, don't attempt renaming.
func renameIfExists(from string, to string) error {
	if _, err := os.Stat(from); os.IsNotExist(err) {
		return nil
	}
	if _, err := os.Stat(to); os.IsNotExist(err) {
		return os.Rename(from, to)
	}
	err := os.RemoveAll(to)
	if err != nil {
		return err
	}
	return os.Rename(from, to)
}

// vimModeEnabled returns true if the user has explicitly set vim mode, or if the $EDITOR is vim/neovim
func vimModeEnabled() bool {
	if os.Getenv("VIM_MODE") == "1" || os.Getenv("VIM_STYLE") == "1" {
		return true
	}
	progname := filepath.Base(os.Getenv("EDITOR"))
	return progname == "vim" || progname == "nvim"
}

// prefixEachLine prepends each line of s with the provided prefix (with).
// Only supports UNIX-Style line endings (\n)
func prefixEachLine(s string, with string) string {
	var prefixedLines []string
	for _, line := range strings.Split(s, "\n") {
		prefixedLines = append(prefixedLines, with+line)
	}
	return strings.Join(prefixedLines, "\n")
}

// GOOStoOS returns user-friendly OS names from a given GOOS.
// darwin becomes macos and plan9 becomes linux.
func GOOStoOS(GOOS string) string {
	switch GOOS {
	case "darwin":
		return "macos"
	case "plan9":
		return "linux"
	default:
		return GOOS
	}
}
package ffcss

import (
	"os"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestProfilePaths(t *testing.T) {
	cwd, _ := os.Getwd()
	mockedHomedir := filepath.Join(cwd, "testarea", "home")

	paths, err := ProfilePaths("linux", filepath.Join(mockedHomedir, ".mozilla", "firefox"))
	assert.NoError(t, err)
	assert.Equal(t, []string{filepath.Join(mockedHomedir, ".mozilla", "firefox", "667ekipp.default-release")}, paths)

	paths, err = ProfilePaths("linux")
	assert.NoError(t, err)
	assert.Equal(t, []string{"testarea/home/.mozilla/firefox/667ekipp.default-release"}, paths)
}

func TestIsURLClonable(t *testing.T) {
	var actual bool

	actual = isURLClonable("https://github.com/ewen-lbh/ffcss/")
	assert.Equal(t, true, actual)

	actual = isURLClonable("https://github.com/users/schoolsyst")
	assert.Equal(t, false, actual)

	actual = isURLClonable("https://ewen.works/")
	assert.Equal(t, false, actual)
}

func TestDefaultProfilesDir(t *testing.T) {
	actual, err := DefaultProfilesDir("TempleOS")
	assert.EqualError(t, err, "unknown operating system TempleOS")
	assert.Equal(t, "", actual)
}

func TestPlural(t *testing.T) {
	assert.Equal(t, "addons", plural("addon", 2))
	assert.Equal(t, "addon", plural("addon", 1))
	assert.Equal(t, "addons", plural("addon", 0))
	assert.Equal(t, "children", plural("child", 10, "children"))
}
//+build !windows

package ffcss

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestDefaultProfilesDirUNIX(t *testing.T) {
	actual, err := DefaultProfilesDir("linux")
	assert.NoError(t, err)
	assert.Equal(t, "testarea/home/.mozilla/firefox", actual)

	actual, err = DefaultProfilesDir("macos")
	assert.NoError(t, err)
	assert.Equal(t, withuser("/Users/%s/Library/Application Support/Firefox/Profiles"), actual)
}
package ffcss

import (
	"os"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestDefaultProfilesDirWindows(t *testing.T) {
	homedir, _ := os.UserHomeDir()
	actual, err := DefaultProfilesDir("windows")
	assert.NoError(t, err)
	assert.Equal(t, filepath.Join(homedir, `AppData\Roaming\Mozilla\Firefox\Profiles`), actual)
}